Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 69 additions & 12 deletions apps/decodex/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ use crate::{
RadarRefreshQueueRequest, RadarRefreshReleaseDeltaRequest, RadarRenderSignalRequest,
RadarValidateRequest,
},
recovery::{self, ReviewHandoffDiagnoseRequest, ReviewHandoffRebindRequest},
recovery::{
self, LegacyCloseoutRecoveryRequest, ReviewHandoffDiagnoseRequest,
ReviewHandoffRebindRequest,
},
research_design::{
self, ResearchDesignCompileRequest, ResearchDesignOutcome, ResearchDesignPromoteRequest,
ResearchDesignRunInput,
Expand Down Expand Up @@ -749,6 +752,15 @@ impl RecoverCommand {
fn run(&self) -> Result<()> {
match &self.command {
RecoverSubcommand::ReviewHandoff(args) => args.run(self.project_config.as_path()),
RecoverSubcommand::LegacyCloseout(args) => recovery::run_legacy_closeout(
self.project_config.as_path(),
&LegacyCloseoutRecoveryRequest {
issue: args.issue.clone(),
pr_url: args.pr.clone(),
dry_run: args.dry_run,
manual_authority: args.manual_authority,
},
),
}
}
}
Expand Down Expand Up @@ -801,6 +813,22 @@ struct ReviewHandoffRebindCommand {
dry_run: bool,
}

#[derive(Debug, Args)]
struct LegacyCloseoutRecoveryCommand {
/// Issue identifier for the legacy cleanup-only worktree.
#[arg(value_name = "ISSUE")]
issue: String,
/// Merged pull request URL that proves the lane's terminal code lineage.
#[arg(long, value_name = "PR_URL")]
pr: String,
/// Validate without writing a Linear execution audit event.
#[arg(long)]
dry_run: bool,
/// Required for non-dry-run audited legacy closeout.
#[arg(long)]
manual_authority: bool,
}

#[derive(Debug, Args)]
struct ArchiveLinearCommand {
#[command(flatten)]
Expand Down Expand Up @@ -1539,6 +1567,8 @@ impl From<ResearchOutcomeArg> for ResearchDesignOutcome {
enum RecoverSubcommand {
/// Recover retained review lanes whose handoff marker is missing.
ReviewHandoff(ReviewHandoffRecoveryCommand),
/// Record an audited fallback closeout for a legacy cleanup-only worktree.
LegacyCloseout(LegacyCloseoutRecoveryCommand),
}

#[derive(Debug, Subcommand)]
Expand Down Expand Up @@ -1679,17 +1709,17 @@ mod tests {
use crate::cli::{
AccountCommand, AccountSubcommand, AccountUseCommand, AttemptCommand, Cli, Command,
CommitCommand, DiagnoseCommand, EvidenceCommand, LandCommand, LaneCommand,
LaneInspectCommand, LaneInterruptCommand, LaneSteerCommand, LaneSubcommand, ProbeCommand,
ProjectCommand, ProjectConfigArgs, ProjectSubcommand, RadarBackfillReleaseRangeCommand,
RadarBundleBuildCommand, RadarBundleCommand, RadarBundleSubcommand,
RadarBundleValidateCommand, RadarCommand, RadarLedgerCommand,
RadarLedgerIngestExistingCommand, RadarLedgerSubcommand, RadarLedgerSummaryCommand,
RadarRefreshReleaseDeltaCommand, RadarRefreshUpstreamQueueCommand,
RadarRenderSignalCommand, RadarSubcommand, RadarValidateCommand, RecoverCommand,
RecoverSubcommand, ResearchCommand, ResearchCompileCommand, ResearchOutcomeArg,
ResearchPromoteCommand, ResearchSubcommand, ReviewHandoffDiagnoseCommand,
ReviewHandoffRebindCommand, ReviewHandoffRecoveryCommand, ReviewHandoffRecoverySubcommand,
RunCommand, ServeCommand, StatusCommand,
LaneInspectCommand, LaneInterruptCommand, LaneSteerCommand, LaneSubcommand,
LegacyCloseoutRecoveryCommand, ProbeCommand, ProjectCommand, ProjectConfigArgs,
ProjectSubcommand, RadarBackfillReleaseRangeCommand, RadarBundleBuildCommand,
RadarBundleCommand, RadarBundleSubcommand, RadarBundleValidateCommand, RadarCommand,
RadarLedgerCommand, RadarLedgerIngestExistingCommand, RadarLedgerSubcommand,
RadarLedgerSummaryCommand, RadarRefreshReleaseDeltaCommand,
RadarRefreshUpstreamQueueCommand, RadarRenderSignalCommand, RadarSubcommand,
RadarValidateCommand, RecoverCommand, RecoverSubcommand, ResearchCommand,
ResearchCompileCommand, ResearchOutcomeArg, ResearchPromoteCommand, ResearchSubcommand,
ReviewHandoffDiagnoseCommand, ReviewHandoffRebindCommand, ReviewHandoffRecoveryCommand,
ReviewHandoffRecoverySubcommand, RunCommand, ServeCommand, StatusCommand,
};

#[test]
Expand Down Expand Up @@ -2585,4 +2615,31 @@ mod tests {
&& pr == "https://github.com/hack-ink/pubfi-mono-v2/pull/14"
));
}

#[test]
fn parses_legacy_closeout_manual_authority() {
let cli = Cli::parse_from([
"decodex",
"recover",
"legacy-closeout",
"PUB-718",
"--pr",
"https://github.com/hack-ink/pubfi-mono-v2/pull/14",
"--manual-authority",
]);

assert!(matches!(
cli.command,
Command::Recover(RecoverCommand {
command: RecoverSubcommand::LegacyCloseout(LegacyCloseoutRecoveryCommand {
issue,
pr,
dry_run: false,
manual_authority: true,
}),
..
}) if issue == "PUB-718"
&& pr == "https://github.com/hack-ink/pubfi-mono-v2/pull/14"
));
}
}
33 changes: 33 additions & 0 deletions apps/decodex/src/orchestrator/operator_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -9988,6 +9988,16 @@ <h4>${escapeHtml(title)}</h4>
"Post-land cleanup pending.",
};
}
if (worktree.provenance?.audit_required) {
return {
sortRank: 2,
tone: "tone-blocked",
label: "legacy cleanup audit",
summary:
worktree.ownership_reason ||
"Legacy worktree provenance is missing; verify terminal state before cleanup.",
};
}
return {
sortRank: 2,
tone: "tone-recovery",
Expand All @@ -10011,6 +10021,28 @@ <h4>${escapeHtml(title)}</h4>
`;
}

function renderWorktreeProvenanceFields(worktree) {
const provenance = worktree.provenance;
if (!provenance) {
return "";
}

const createdAt = unixEpochSecondsToIso(provenance.created_at_unix) || "unknown";
const updatedAt = unixEpochSecondsToIso(provenance.updated_at_unix) || "unknown";
const audit = provenance.audit_required ? field("Audit", "required") : "";
const nextAction = worktree.recovery_next_action
? field("Next action", worktree.recovery_next_action)
: "";

return `
${field("Provenance", displayToken(provenance.source || "unknown"))}
${field("Recorded", createdAt)}
${field("Refreshed", updatedAt)}
${audit}
${nextAction}
`;
}

function recoveryWorktreeShouldDefaultOpen(renderedWorktree) {
const role = renderedWorktree.role;

Expand Down Expand Up @@ -10075,6 +10107,7 @@ <h4>${escapeHtml(worktree.branch_name)}</h4>
${field("Ownership", displayToken(worktree.ownership || role.label))}
${field("Branch", worktree.branch_name)}
${field("Worktree path", worktree.worktree_path)}
${renderWorktreeProvenanceFields(worktree)}
${renderWorktreeHygieneFields(worktree)}
</div>
</article>
Expand Down
Loading