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
8 changes: 4 additions & 4 deletions apps/decodex/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ struct LandCommand {
#[arg(long, conflicts_with = "authority", requires = "pr")]
manual_authority: bool,
/// Pull request URL to land. Required with `--manual-authority`; otherwise defaults to the
/// current review handoff marker.
/// current review lifecycle record.
#[arg(long, value_name = "URL")]
pr: Option<String>,
/// Additional related issues for the landed change record.
Expand Down Expand Up @@ -1009,7 +1009,7 @@ struct ReviewHandoffRebindCommand {
/// Pull request URL to bind after validation.
#[arg(long, value_name = "URL")]
pr: String,
/// Validate only; do not write runtime markers or tracker audit comments.
/// Validate only; do not write runtime lifecycle state or tracker audit comments.
#[arg(long)]
dry_run: bool,
}
Expand All @@ -1022,7 +1022,7 @@ struct ReviewHandoffAdoptCommand {
/// Pull request URL to adopt after validation.
#[arg(long, value_name = "URL")]
pr: String,
/// Validate only; do not write runtime markers or tracker audit comments.
/// Validate only; do not write runtime lifecycle state or tracker audit comments.
#[arg(long)]
dry_run: bool,
}
Expand Down Expand Up @@ -1833,7 +1833,7 @@ enum ResearchOutcomeArg {

#[derive(Debug, Subcommand)]
enum RecoverSubcommand {
/// Recover retained review lanes whose handoff marker is missing.
/// Recover retained review lanes whose lifecycle record is missing.
ReviewHandoff(ReviewHandoffRecoveryCommand),
/// Record an audited fallback closeout for a legacy cleanup-only worktree.
LegacyCloseout(LegacyCloseoutRecoveryCommand),
Expand Down
87 changes: 37 additions & 50 deletions apps/decodex/src/maintenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ fn maintain_runtime_protocol_events(
event_count: candidate.event_count,
last_event_at: candidate.last_event_at.clone(),
reason: format!(
"terminal run has no run lease, retained worktree, or review marker and its latest protocol event is older than {} days",
"terminal run has no run lease, retained worktree, or review lifecycle record and its latest protocol event is older than {} days",
policy.protocol_event_retention_days
),
});
Expand Down Expand Up @@ -633,9 +633,8 @@ fn protocol_event_compaction_candidates(
AND last.sequence_number = totals.last_sequence_number
LEFT JOIN leases run_lease ON run_lease.issue_id = attempts.issue_id
LEFT JOIN worktrees retained_worktree ON retained_worktree.issue_id = attempts.issue_id
LEFT JOIN review_handoffs review_handoff ON review_handoff.issue_id = attempts.issue_id
LEFT JOIN review_orchestrations review_orchestration
ON review_orchestration.issue_id = attempts.issue_id
LEFT JOIN review_lifecycle_records review_lifecycle
ON review_lifecycle.issue_id = attempts.issue_id
LEFT JOIN (
SELECT
issue_id,
Expand All @@ -650,8 +649,7 @@ fn protocol_event_compaction_candidates(
AND totals.last_created_at_unix < ?1
AND run_lease.issue_id IS NULL
AND retained_worktree.issue_id IS NULL
AND review_handoff.issue_id IS NULL
AND review_orchestration.issue_id IS NULL
AND review_lifecycle.issue_id IS NULL
AND human_stop_event.run_id IS NULL
ORDER BY totals.last_created_at_unix ASC, attempts.run_id ASC",
)?;
Expand Down Expand Up @@ -683,9 +681,8 @@ fn protected_protocol_run_count(connection: &Connection) -> Result<usize> {
JOIN protocol_events events ON events.run_id = attempts.run_id
LEFT JOIN leases run_lease ON run_lease.issue_id = attempts.issue_id
LEFT JOIN worktrees retained_worktree ON retained_worktree.issue_id = attempts.issue_id
LEFT JOIN review_handoffs review_handoff ON review_handoff.issue_id = attempts.issue_id
LEFT JOIN review_orchestrations review_orchestration
ON review_orchestration.issue_id = attempts.issue_id
LEFT JOIN review_lifecycle_records review_lifecycle
ON review_lifecycle.issue_id = attempts.issue_id
LEFT JOIN (
SELECT
issue_id,
Expand All @@ -698,8 +695,7 @@ fn protected_protocol_run_count(connection: &Connection) -> Result<usize> {
AND human_stop_event.run_id = attempts.run_id
WHERE run_lease.issue_id IS NOT NULL
OR retained_worktree.issue_id IS NOT NULL
OR review_handoff.issue_id IS NOT NULL
OR review_orchestration.issue_id IS NOT NULL
OR review_lifecycle.issue_id IS NOT NULL
OR human_stop_event.run_id IS NOT NULL
OR attempts.status NOT IN ('succeeded', 'failed', 'interrupted', 'terminated')",
[],
Expand Down Expand Up @@ -996,7 +992,7 @@ mod tests {
recorded_at TEXT NOT NULL,
recorded_at_unix INTEGER NOT NULL
);
CREATE TABLE review_handoffs (
CREATE TABLE review_lifecycle_records (
project_id TEXT NOT NULL,
issue_id TEXT NOT NULL,
branch_name TEXT NOT NULL,
Expand All @@ -1006,17 +1002,6 @@ mod tests {
target_base_ref_name TEXT,
pr_head_ref_name TEXT NOT NULL,
pr_head_oid TEXT NOT NULL,
updated_at TEXT NOT NULL,
updated_at_unix INTEGER NOT NULL,
PRIMARY KEY (project_id, issue_id, branch_name)
);
CREATE TABLE review_orchestrations (
project_id TEXT NOT NULL,
issue_id TEXT NOT NULL,
branch_name TEXT NOT NULL,
run_id TEXT NOT NULL,
attempt_number INTEGER NOT NULL,
pr_url TEXT NOT NULL,
head_sha TEXT NOT NULL,
phase TEXT NOT NULL,
request_comment_database_id INTEGER,
Expand All @@ -1025,9 +1010,14 @@ mod tests {
request_retry_count INTEGER NOT NULL,
external_round_count INTEGER NOT NULL,
auto_merge_enabled_at_unix_epoch INTEGER,
landing_state TEXT NOT NULL DEFAULT 'not_started',
closeout_state TEXT NOT NULL DEFAULT 'not_started',
repair_attempt_count INTEGER NOT NULL DEFAULT 0,
evidence_json TEXT NOT NULL DEFAULT '{}',
next_action TEXT NOT NULL DEFAULT '',
updated_at TEXT NOT NULL,
updated_at_unix INTEGER NOT NULL,
PRIMARY KEY (project_id, issue_id, branch_name, run_id, attempt_number)
PRIMARY KEY (project_id, issue_id, branch_name)
);";

#[test]
Expand Down Expand Up @@ -1069,10 +1059,20 @@ mod tests {

insert_attempt(&connection, "review-handoff-run", "review-issue", "succeeded");
insert_event(&connection, "review-handoff-run", 1, old);
insert_review_handoff(&connection, "review-issue", "review-handoff-run");
insert_review_lifecycle(
&connection,
"review-issue",
"review-handoff-run",
"request_pending",
);
insert_attempt(&connection, "cleanup-blocked-run", "cleanup-issue", "succeeded");
insert_event(&connection, "cleanup-blocked-run", 1, old);
insert_review_orchestration(&connection, "cleanup-issue", "cleanup-blocked-run");
insert_review_lifecycle(
&connection,
"cleanup-issue",
"cleanup-blocked-run",
"cleanup_blocked",
);
insert_attempt(&connection, "attention-run", "attention-issue", "failed");
insert_event(&connection, "attention-run", 1, old);
insert_linear_execution_event(
Expand Down Expand Up @@ -1342,41 +1342,28 @@ mod tests {
.expect("event should insert");
}

fn insert_review_handoff(connection: &Connection, issue_id: &str, run_id: &str) {
fn insert_review_lifecycle(connection: &Connection, issue_id: &str, run_id: &str, phase: &str) {
connection
.execute(
"INSERT INTO review_handoffs (
"INSERT INTO review_lifecycle_records (
project_id, issue_id, branch_name, run_id, attempt_number, pr_url,
target_base_ref_name, pr_head_ref_name, pr_head_oid, updated_at,
updated_at_unix
) VALUES (
'decodex', ?1, 'y/decodex-test', ?2, 1,
'https://github.com/hack-ink/decodex/pull/1', 'main',
'y/decodex-test', 'abc123', '2026-05-01T00:00:00Z', 0
)",
rusqlite::params![issue_id, run_id],
)
.expect("review handoff should insert");
}

fn insert_review_orchestration(connection: &Connection, issue_id: &str, run_id: &str) {
connection
.execute(
"INSERT INTO review_orchestrations (
project_id, issue_id, branch_name, run_id, attempt_number, pr_url,
head_sha, phase, request_comment_database_id,
target_base_ref_name, pr_head_ref_name, pr_head_oid, head_sha, phase,
request_comment_database_id,
request_created_at_unix_epoch, request_description_thumbs_up_count,
request_retry_count, external_round_count, auto_merge_enabled_at_unix_epoch,
landing_state, closeout_state, repair_attempt_count, evidence_json,
next_action,
updated_at, updated_at_unix
) VALUES (
'decodex', ?1, 'y/decodex-test', ?2, 1,
'https://github.com/hack-ink/decodex/pull/1', 'abc123',
'cleanup_blocked', NULL, NULL, NULL, 0, 0, NULL,
'https://github.com/hack-ink/decodex/pull/1', 'main',
'y/decodex-test', 'abc123', 'abc123', ?3, NULL, NULL, NULL, 0, 0, NULL,
'not_started', 'not_started', 0, '{}', '',
'2026-05-01T00:00:00Z', 0
)",
rusqlite::params![issue_id, run_id],
rusqlite::params![issue_id, run_id, phase],
)
.expect("review orchestration should insert");
.expect("review lifecycle should insert");
}

fn insert_linear_execution_event(
Expand Down
48 changes: 24 additions & 24 deletions apps/decodex/src/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub(crate) struct ReviewHandoffRebindRequest {
pub(crate) issue: String,
/// Pull request URL to bind.
pub(crate) pr_url: String,
/// Validate without writing markers or tracker audit comments.
/// Validate without writing a lifecycle record or tracker audit comments.
pub(crate) dry_run: bool,
}

Expand All @@ -74,7 +74,7 @@ pub(crate) struct ReviewHandoffAdoptRequest {
pub(crate) issue: String,
/// Pull request URL to adopt.
pub(crate) pr_url: String,
/// Validate without writing runtime markers or tracker audit comments.
/// Validate without writing runtime lifecycle state or tracker audit comments.
pub(crate) dry_run: bool,
}

Expand Down Expand Up @@ -124,8 +124,8 @@ struct ReviewHandoffDiagnostic {
local_head_oid: Option<String>,
worktree_clean: Option<bool>,
existing_pr_url: Option<String>,
existing_marker_head_oid: Option<String>,
existing_orchestration_head_oid: Option<String>,
existing_lifecycle_handoff_head_oid: Option<String>,
existing_lifecycle_phase_head_oid: Option<String>,
pr_base_ref: Option<String>,
pr_head_oid: Option<String>,
mismatched_field: Option<String>,
Expand Down Expand Up @@ -279,8 +279,8 @@ impl RebindMode {

fn summary_action(self) -> &'static str {
match self {
Self::RestoreMissingHandoff => "restored retained review handoff marker",
Self::RefreshExistingHandoff => "refreshed retained review handoff marker",
Self::RestoreMissingHandoff => "restored retained review lifecycle record",
Self::RefreshExistingHandoff => "refreshed retained review lifecycle record",
Self::CompleteExistingHandoffState => "completed retained review handoff state",
}
}
Expand Down Expand Up @@ -744,10 +744,10 @@ fn diagnose_issue_worktree(
local_head_oid,
worktree_clean,
existing_pr_url: existing_handoff.as_ref().map(|handoff| handoff.pr_url().to_owned()),
existing_marker_head_oid: existing_handoff
existing_lifecycle_handoff_head_oid: existing_handoff
.as_ref()
.map(|handoff| handoff.pr_head_oid().to_owned()),
existing_orchestration_head_oid: existing_orchestration
existing_lifecycle_phase_head_oid: existing_orchestration
.as_ref()
.map(|orchestration| orchestration.head_sha().to_owned()),
pr_base_ref: binding.pr_base_ref,
Expand Down Expand Up @@ -1060,7 +1060,7 @@ fn inspect_handoff_next_action(issue_identifier: &str, pr_url: &str) -> String {

fn rebind_refresh_next_action(issue_identifier: &str, pr_url: &str) -> String {
format!(
"Run `decodex recover review-handoff rebind {issue_identifier} --pr {pr_url} --dry-run`, then rerun without `--dry-run` to refresh the retained marker if validation passes."
"Run `decodex recover review-handoff rebind {issue_identifier} --pr {pr_url} --dry-run`, then rerun without `--dry-run` to refresh the retained lifecycle record if validation passes."
)
}

Expand Down Expand Up @@ -1088,7 +1088,7 @@ fn render_review_handoff_recovery_report(report: &ReviewHandoffRecoveryReport) -

for diagnostic in &report.diagnostics {
output.push_str(&format!(
"- issue: {}\n state: {}\n classification: {}\n reason: {}\n branch: {}\n worktree_path: {}\n local_branch: {}\n local_head: {}\n worktree_clean: {}\n existing_pr_url: {}\n existing_marker_head: {}\n existing_orchestration_head: {}\n pr_base_ref: {}\n pr_head: {}\n mismatched_field: {}\n active_label_present: {}\n next_action: {}\n",
"- issue: {}\n state: {}\n classification: {}\n reason: {}\n branch: {}\n worktree_path: {}\n local_branch: {}\n local_head: {}\n worktree_clean: {}\n existing_pr_url: {}\n existing_lifecycle_handoff_head: {}\n existing_lifecycle_phase_head: {}\n pr_base_ref: {}\n pr_head: {}\n mismatched_field: {}\n active_label_present: {}\n next_action: {}\n",
diagnostic.issue_identifier,
diagnostic.issue_state,
diagnostic.classification,
Expand All @@ -1099,8 +1099,8 @@ fn render_review_handoff_recovery_report(report: &ReviewHandoffRecoveryReport) -
optional_text(diagnostic.local_head_oid.as_deref()),
diagnostic.worktree_clean.map_or_else(|| String::from("unknown"), |clean| clean.to_string()),
optional_text(diagnostic.existing_pr_url.as_deref()),
optional_text(diagnostic.existing_marker_head_oid.as_deref()),
optional_text(diagnostic.existing_orchestration_head_oid.as_deref()),
optional_text(diagnostic.existing_lifecycle_handoff_head_oid.as_deref()),
optional_text(diagnostic.existing_lifecycle_phase_head_oid.as_deref()),
optional_text(diagnostic.pr_base_ref.as_deref()),
optional_text(diagnostic.pr_head_oid.as_deref()),
optional_text(diagnostic.mismatched_field.as_deref()),
Expand Down Expand Up @@ -1615,7 +1615,7 @@ fn validate_existing_handoff_refresh(
) -> Result<(String, i64, RebindMode)> {
if existing_handoff.pr_url() != landing_url(landing_state) {
eyre::bail!(
"Issue `{}` already has review handoff marker for branch `{}` and PR `{}`; refusing to rebind it to `{}`.",
"Issue `{}` already has a review lifecycle record for branch `{}` and PR `{}`; refusing to rebind it to `{}`.",
issue.identifier,
worktree.branch_name(),
existing_handoff.pr_url(),
Expand All @@ -1641,7 +1641,7 @@ fn validate_existing_handoff_refresh(
}

eyre::bail!(
"Issue `{}` already has a review handoff marker for branch `{}` and PR `{}` at head `{local_head_oid}`; no rebind is needed.",
"Issue `{}` already has a review lifecycle record for branch `{}` and PR `{}` at head `{local_head_oid}`; no rebind is needed.",
issue.identifier,
worktree.branch_name(),
existing_handoff.pr_url()
Expand Down Expand Up @@ -2275,7 +2275,7 @@ fn validate_adopt_absent_handoff_marker(
.is_some()
{
eyre::bail!(
"Issue `{}` already has a retained review handoff marker for branch `{branch}`; use `decodex land` or `decodex recover review-handoff rebind` instead.",
"Issue `{}` already has a retained review lifecycle record for branch `{branch}`; use `decodex land` or `decodex recover review-handoff rebind` instead.",
issue.identifier
);
}
Expand Down Expand Up @@ -2327,7 +2327,7 @@ fn apply_review_handoff_rebind(
if let Err(error) = write_rebind_audit(context, validation, &event)
.and_then(|()| context.state_store.record_linear_execution_event(&event))
{
context.state_store.clear_review_markers_for_handoff(
context.state_store.clear_review_lifecycle_for_handoff(
context.config.service_id(),
&validation.issue.id,
&handoff_marker,
Expand Down Expand Up @@ -2393,7 +2393,7 @@ fn apply_review_handoff_adopt(
if let Err(error) = local_state_write {
mark_adopt_attempt_failed(context, validation);

context.state_store.clear_review_markers_for_handoff(
context.state_store.clear_review_lifecycle_for_handoff(
context.config.service_id(),
&validation.issue.id,
&handoff_marker,
Expand All @@ -2410,7 +2410,7 @@ fn apply_review_handoff_adopt(
Err(error) => {
mark_adopt_attempt_failed(context, validation);

context.state_store.clear_review_markers_for_handoff(
context.state_store.clear_review_lifecycle_for_handoff(
context.config.service_id(),
&validation.issue.id,
&handoff_marker,
Expand All @@ -2429,7 +2429,7 @@ fn apply_review_handoff_adopt(
{
mark_adopt_attempt_failed(context, validation);

context.state_store.clear_review_markers_for_handoff(
context.state_store.clear_review_lifecycle_for_handoff(
context.config.service_id(),
&validation.issue.id,
&handoff_marker,
Expand Down Expand Up @@ -2667,7 +2667,7 @@ fn review_handoff_rebind_event(
format!("branch={}", validation.worktree.branch_name()),
format!("pr_url={pr_url}"),
format!("pr_head_sha={}", validation.local_head_oid),
format!("existing_review_handoff_marker={}", validation.mode.evidence_value()),
format!("existing_review_lifecycle_record={}", validation.mode.evidence_value()),
format!("needs_attention_label_repair={}", validation.clear_needs_attention_label),
]);
event.next_action = Some(String::from("continue retained post-review lifecycle"));
Expand Down Expand Up @@ -2722,7 +2722,7 @@ fn review_handoff_adopt_event(
"existing_retained_worktree_mapping={}",
validation.previous_worktree_mapping.is_some()
),
String::from("existing_review_handoff_marker=false"),
String::from("existing_review_lifecycle_record=false"),
]);
event.next_action = Some(String::from("continue retained post-review lifecycle"));

Expand Down Expand Up @@ -4252,8 +4252,8 @@ Test workflow.
record.pr_base_ref = Some(String::from("main"));
record.commit_sha = Some(String::from("0123456789abcdef0123456789abcdef01234567"));
record.validation_result = Some(String::from("passed"));
record.summary = Some(String::from("Explicit operator rebind restored marker."));
record.evidence = Some(vec![String::from("existing_review_handoff_marker=absent")]);
record.summary = Some(String::from("Explicit operator rebind restored lifecycle record."));
record.evidence = Some(vec![String::from("existing_review_lifecycle_record=absent")]);

records::validate_linear_execution_event_record(&record)
.expect("rebind event should validate");
Expand Down Expand Up @@ -4365,7 +4365,7 @@ Test workflow.
record.pr_base_ref = Some(String::from("main"));
record.commit_sha = Some(String::from("0123456789abcdef0123456789abcdef01234567"));
record.validation_result = Some(String::from("passed"));
record.summary = Some(String::from("Explicit operator rebind restored marker."));
record.summary = Some(String::from("Explicit operator rebind restored lifecycle record."));

let error = records::validate_linear_execution_event_record(&record)
.expect_err("rebind event without evidence should fail");
Expand Down
Loading