@lamentis/naome 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +17 -122
  3. package/crates/naome-cli/Cargo.toml +1 -1
  4. package/crates/naome-cli/src/main.rs +13 -0
  5. package/crates/naome-cli/src/task_commands/can_edit.rs +116 -0
  6. package/crates/naome-cli/src/task_commands/check_run/output.rs +34 -0
  7. package/crates/naome-cli/src/task_commands/check_run/receipts.rs +155 -0
  8. package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
  9. package/crates/naome-cli/src/task_commands/check_run.rs +192 -0
  10. package/crates/naome-cli/src/task_commands/common.rs +70 -0
  11. package/crates/naome-cli/src/task_commands/complete.rs +43 -0
  12. package/crates/naome-cli/src/task_commands/loop_control.rs +55 -0
  13. package/crates/naome-cli/src/task_commands/readiness.rs +44 -0
  14. package/crates/naome-cli/src/task_commands/record.rs +236 -0
  15. package/crates/naome-cli/src/task_commands/repair.rs +77 -0
  16. package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
  17. package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
  18. package/crates/naome-cli/src/task_commands.rs +80 -1
  19. package/crates/naome-cli/tests/task_cli.rs +58 -0
  20. package/crates/naome-cli/tests/task_cli_agent_controls.rs +210 -0
  21. package/crates/naome-cli/tests/task_cli_control.rs +126 -0
  22. package/crates/naome-cli/tests/task_cli_loop.rs +383 -0
  23. package/crates/naome-cli/tests/task_cli_loop_edit.rs +144 -0
  24. package/crates/naome-cli/tests/task_cli_support/mod.rs +178 -0
  25. package/crates/naome-core/Cargo.toml +1 -1
  26. package/crates/naome-core/src/lib.rs +7 -2
  27. package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
  28. package/crates/naome-core/src/task_state/mod.rs +12 -0
  29. package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
  30. package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
  31. package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
  32. package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
  33. package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
  34. package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
  35. package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
  36. package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
  37. package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
  38. package/crates/naome-core/src/task_state/status/control.rs +16 -0
  39. package/crates/naome-core/src/task_state/status/git.rs +133 -0
  40. package/crates/naome-core/src/task_state/status/model.rs +152 -0
  41. package/crates/naome-core/src/task_state/status/proof.rs +217 -0
  42. package/crates/naome-core/src/task_state/status/proof_read.rs +164 -0
  43. package/crates/naome-core/src/task_state/status/report.rs +148 -0
  44. package/crates/naome-core/src/task_state/status/report_context.rs +148 -0
  45. package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
  46. package/crates/naome-core/src/task_state/status/scope.rs +111 -0
  47. package/crates/naome-core/src/task_state/status/transition.rs +101 -0
  48. package/crates/naome-core/src/task_state/status.rs +23 -0
  49. package/crates/naome-core/src/task_state/status_output.rs +103 -0
  50. package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
  51. package/crates/naome-core/tests/task_state_support/states.rs +4 -0
  52. package/crates/naome-core/tests/task_status.rs +423 -0
  53. package/crates/naome-core/tests/task_status_git.rs +141 -0
  54. package/installer/context.js +1 -1
  55. package/installer/harness-verification.js +2 -6
  56. package/installer/manifest-state.js +2 -2
  57. package/installer/native.js +3 -31
  58. package/native/darwin-arm64/naome +0 -0
  59. package/native/linux-x64/naome +0 -0
  60. package/package.json +1 -1
  61. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  62. package/templates/naome-root/.naome/bin/check-task-state.js +4 -39
  63. package/templates/naome-root/.naome/bin/naome.js +2 -30
  64. package/templates/naome-root/.naome/manifest.json +2 -2
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.4.0"
3
+ version = "1.4.2"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -68,8 +68,13 @@ pub use task_ledger::{
68
68
  TaskLedgerProjection, TaskLedgerStatus,
69
69
  };
70
70
  pub use task_state::{
71
- completed_task_commit_paths, validate_task_state, TaskStateMode, TaskStateOptions,
72
- TaskStateReport,
71
+ completed_task_commit_paths, format_task_proof_plan, format_task_status,
72
+ task_evidence_fingerprint, task_proof_plan, task_status_exit_code, task_status_report,
73
+ task_transition_readiness, validate_task_state, AgentLoop, NextActionV2, PolicyHints,
74
+ ProofRecording, ProofRecordingAfterSuccess, RecoveryGuidance, RepairPlanItem, TaskFeedback,
75
+ TaskGitStatus, TaskModeStatus, TaskProofPlanReport, TaskProofStatus, TaskRecommendedCommand,
76
+ TaskScopeStatus, TaskStateMode, TaskStateOptions, TaskStateReport, TaskStatusFinding,
77
+ TaskStatusReportV1, TransitionReadinessReport,
73
78
  };
74
79
  pub use verification::seed_builtin_verification_checks;
75
80
  pub use verification_contract::validate_verification_contract;
@@ -0,0 +1,47 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use crate::models::NaomeError;
5
+
6
+ pub fn task_evidence_fingerprint(root: &Path, paths: &[String]) -> Result<String, NaomeError> {
7
+ let mut paths = paths.to_vec();
8
+ paths.sort();
9
+ let mut hash = Fnv64::new();
10
+ for path in paths {
11
+ hash.update(path.as_bytes());
12
+ hash.update(b"\0");
13
+ match fs::read(root.join(&path)) {
14
+ Ok(content) => {
15
+ hash.update(b"file:");
16
+ hash.update(content.len().to_string().as_bytes());
17
+ hash.update(b":");
18
+ hash.update(&content);
19
+ }
20
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
21
+ hash.update(b"missing");
22
+ }
23
+ Err(error) => return Err(NaomeError::from(error)),
24
+ }
25
+ hash.update(b"\0");
26
+ }
27
+ Ok(format!("fnv64:{:016x}", hash.finish()))
28
+ }
29
+
30
+ struct Fnv64(u64);
31
+
32
+ impl Fnv64 {
33
+ fn new() -> Self {
34
+ Self(0xcbf29ce484222325)
35
+ }
36
+
37
+ fn update(&mut self, bytes: &[u8]) {
38
+ for byte in bytes {
39
+ self.0 ^= u64::from(*byte);
40
+ self.0 = self.0.wrapping_mul(0x100000001b3);
41
+ }
42
+ }
43
+
44
+ fn finish(self) -> u64 {
45
+ self.0
46
+ }
47
+ }
@@ -8,6 +8,7 @@ mod completion;
8
8
  mod deleted_paths;
9
9
  mod diff;
10
10
  mod evidence;
11
+ mod evidence_fingerprint;
11
12
  mod git_io;
12
13
  mod git_parse;
13
14
  mod git_refs;
@@ -22,6 +23,8 @@ mod proof_types;
22
23
  mod push_gate;
23
24
  mod repair;
24
25
  mod shape;
26
+ mod status;
27
+ mod status_output;
25
28
  mod task_diff_api;
26
29
  mod task_records;
27
30
  mod task_references;
@@ -30,7 +33,16 @@ mod util;
30
33
 
31
34
  pub use api::validate_task_state;
32
35
  pub use completed_refresh::completed_task_harness_refresh_diff;
36
+ pub use evidence_fingerprint::task_evidence_fingerprint;
33
37
  pub(crate) use proof_model::{canonical_proof_check_ids, canonical_proofs};
38
+ pub use status::{
39
+ task_proof_plan, task_status_exit_code, task_status_report, task_transition_readiness,
40
+ AgentLoop, NextActionV2, PolicyHints, ProofRecording, ProofRecordingAfterSuccess,
41
+ RecoveryGuidance, RepairPlanItem, TaskFeedback, TaskGitStatus, TaskModeStatus,
42
+ TaskProofPlanReport, TaskProofStatus, TaskRecommendedCommand, TaskScopeStatus,
43
+ TaskStatusFinding, TaskStatusReportV1, TransitionReadinessReport,
44
+ };
45
+ pub use status_output::{format_task_proof_plan, format_task_status};
34
46
  pub use task_diff_api::{
35
47
  completed_task_commit_diff, completed_task_commit_paths, harness_refresh_diff,
36
48
  harness_refresh_with_unrelated_diff,
@@ -0,0 +1,76 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4
+ #[serde(rename_all = "camelCase")]
5
+ pub struct NextActionV2 {
6
+ #[serde(rename = "type")]
7
+ pub action_type: String,
8
+ pub reason: String,
9
+ pub check_ids: Vec<String>,
10
+ pub commands: Vec<String>,
11
+ pub paths: Vec<String>,
12
+ pub safe_to_execute: bool,
13
+ pub requires_user_approval: bool,
14
+ }
15
+
16
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17
+ #[serde(rename_all = "camelCase")]
18
+ pub struct AgentLoop {
19
+ pub state: String,
20
+ pub can_continue_editing: bool,
21
+ pub can_run_checks: bool,
22
+ pub can_record_proof: bool,
23
+ pub can_commit: bool,
24
+ pub must_do_next: Vec<String>,
25
+ pub must_not_do: Vec<String>,
26
+ }
27
+
28
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29
+ #[serde(rename_all = "camelCase")]
30
+ pub struct RepairPlanItem {
31
+ pub id: String,
32
+ pub kind: String,
33
+ pub reason: String,
34
+ pub paths: Vec<String>,
35
+ pub check_ids: Vec<String>,
36
+ pub commands: Vec<String>,
37
+ #[serde(skip_serializing_if = "Option::is_none")]
38
+ pub cwd: Option<String>,
39
+ pub safe_to_execute: bool,
40
+ pub requires_user_approval: bool,
41
+ }
42
+
43
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44
+ #[serde(rename_all = "camelCase")]
45
+ pub struct ProofRecording {
46
+ pub path_set_id: String,
47
+ pub paths: Vec<String>,
48
+ pub proof_batch_id: String,
49
+ pub checks_to_record: Vec<String>,
50
+ pub after_success: ProofRecordingAfterSuccess,
51
+ }
52
+
53
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54
+ #[serde(rename_all = "camelCase")]
55
+ pub struct ProofRecordingAfterSuccess {
56
+ pub record_proof: bool,
57
+ pub instructions: String,
58
+ }
59
+
60
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61
+ #[serde(rename_all = "camelCase")]
62
+ pub struct PolicyHints {
63
+ pub may_edit: Vec<String>,
64
+ pub must_inspect_before_edit: Vec<String>,
65
+ pub must_run_after_edit: Vec<String>,
66
+ pub forbidden_actions: Vec<String>,
67
+ }
68
+
69
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70
+ #[serde(rename_all = "camelCase")]
71
+ pub struct RecoveryGuidance {
72
+ pub id: String,
73
+ pub reason: String,
74
+ pub safe_next_action: String,
75
+ pub requires_user_approval: bool,
76
+ }
@@ -0,0 +1,87 @@
1
+ use super::super::agent_model::NextActionV2;
2
+ use super::super::model::{
3
+ TaskProofStatus, TaskRecommendedCommand, TaskScopeStatus, TaskStatusFinding,
4
+ };
5
+ use super::shared::{action_check_ids, has_finding, has_git_recovery};
6
+
7
+ pub(in crate::task_state::status) fn next_action_v2(
8
+ state: &str,
9
+ proof: &TaskProofStatus,
10
+ findings: &[TaskStatusFinding],
11
+ commands: &[TaskRecommendedCommand],
12
+ scope: &TaskScopeStatus,
13
+ ) -> NextActionV2 {
14
+ let (action_type, reason, paths, safe) = prioritized_action(state, proof, findings, scope);
15
+ NextActionV2 {
16
+ action_type: action_type.to_string(),
17
+ reason,
18
+ check_ids: action_check_ids(proof),
19
+ commands: commands
20
+ .iter()
21
+ .map(|command| command.command.clone())
22
+ .collect(),
23
+ paths,
24
+ safe_to_execute: safe,
25
+ requires_user_approval: !safe,
26
+ }
27
+ }
28
+
29
+ fn prioritized_action(
30
+ state: &str,
31
+ proof: &TaskProofStatus,
32
+ findings: &[TaskStatusFinding],
33
+ scope: &TaskScopeStatus,
34
+ ) -> (&'static str, String, Vec<String>, bool) {
35
+ if has_finding(findings, "task.state.conflict_markers") {
36
+ return (
37
+ "blocked",
38
+ "Task-state conflict markers must be resolved.".to_string(),
39
+ vec![],
40
+ false,
41
+ );
42
+ }
43
+ if has_git_recovery(findings) {
44
+ return (
45
+ "recover_git_state",
46
+ "Git recovery is required before task work can continue.".to_string(),
47
+ vec![],
48
+ false,
49
+ );
50
+ }
51
+ if has_finding(findings, "task.scope.out_of_scope_change") {
52
+ return (
53
+ "repair_scope",
54
+ "Out-of-scope changes must be removed or admitted.".to_string(),
55
+ scope.out_of_scope_changed_paths.clone(),
56
+ true,
57
+ );
58
+ }
59
+ if !proof.missing_checks.is_empty() || !proof.stale_checks.is_empty() {
60
+ return (
61
+ "rerun_checks",
62
+ "Required proof is missing or stale.".to_string(),
63
+ scope.in_scope_changed_paths.clone(),
64
+ true,
65
+ );
66
+ }
67
+ match state {
68
+ "idle" | "missing" => (
69
+ "create_task",
70
+ "No active task is in progress.".to_string(),
71
+ vec![],
72
+ true,
73
+ ),
74
+ "complete" => (
75
+ "none",
76
+ "Task is complete and proof is current.".to_string(),
77
+ vec![],
78
+ true,
79
+ ),
80
+ _ => (
81
+ "continue_editing",
82
+ "Active task is healthy.".to_string(),
83
+ scope.in_scope_changed_paths.clone(),
84
+ true,
85
+ ),
86
+ }
87
+ }
@@ -0,0 +1,32 @@
1
+ use super::super::model::{TaskProofStatus, TaskStatusFinding};
2
+ use super::shared::has_finding;
3
+
4
+ pub fn task_status_exit_code(findings: &[TaskStatusFinding], proof: &TaskProofStatus) -> i32 {
5
+ if has_finding(findings, "task.state.conflict_markers")
6
+ || has_finding(findings, "task.state.invalid_json")
7
+ || has_finding(findings, "task.state.active_task_missing")
8
+ {
9
+ 40
10
+ } else if findings.iter().any(|finding| {
11
+ matches!(
12
+ finding.id.as_str(),
13
+ "task.git.operation_in_progress"
14
+ | "task.git.admission_head_missing"
15
+ | "task.git.admission_head_not_reachable"
16
+ )
17
+ }) {
18
+ 30
19
+ } else if has_finding(findings, "task.scope.out_of_scope_change") {
20
+ 20
21
+ } else if has_finding(findings, "task.state.no_active_task_with_diff") {
22
+ 50
23
+ } else if has_finding(findings, "task.state.completed_task_has_diff") {
24
+ 60
25
+ } else if !proof.missing_checks.is_empty() {
26
+ 10
27
+ } else if !proof.stale_checks.is_empty() {
28
+ 11
29
+ } else {
30
+ 0
31
+ }
32
+ }
@@ -0,0 +1,70 @@
1
+ use super::super::agent_model::AgentLoop;
2
+ use super::super::model::{TaskProofStatus, TaskScopeStatus, TaskStatusFinding};
3
+ use super::shared::{action_check_ids, has_finding, has_git_recovery};
4
+
5
+ pub(in crate::task_state::status) fn agent_loop(
6
+ state: &str,
7
+ proof: &TaskProofStatus,
8
+ findings: &[TaskStatusFinding],
9
+ scope: &TaskScopeStatus,
10
+ ) -> AgentLoop {
11
+ let loop_state = agent_loop_state(state, proof, findings, scope);
12
+ AgentLoop {
13
+ can_continue_editing: matches!(loop_state.as_str(), "healthy" | "ready_to_commit"),
14
+ can_run_checks: !matches!(
15
+ loop_state.as_str(),
16
+ "blocked_by_scope_drift" | "blocked_by_git_state" | "blocked_by_task_state"
17
+ ),
18
+ can_record_proof: matches!(
19
+ loop_state.as_str(),
20
+ "blocked_by_missing_proof" | "blocked_by_stale_proof" | "ready_to_commit"
21
+ ),
22
+ can_commit: loop_state == "ready_to_commit",
23
+ must_do_next: must_do_next(&loop_state, proof),
24
+ must_not_do: must_not_do(&loop_state),
25
+ state: loop_state,
26
+ }
27
+ }
28
+
29
+ fn agent_loop_state(
30
+ state: &str,
31
+ proof: &TaskProofStatus,
32
+ findings: &[TaskStatusFinding],
33
+ scope: &TaskScopeStatus,
34
+ ) -> String {
35
+ if has_finding(findings, "task.state.conflict_markers") {
36
+ "blocked_by_task_state"
37
+ } else if has_git_recovery(findings) {
38
+ "blocked_by_git_state"
39
+ } else if has_finding(findings, "task.scope.out_of_scope_change") {
40
+ "blocked_by_scope_drift"
41
+ } else if has_finding(findings, "task.state.no_active_task_with_diff") {
42
+ "blocked_by_task_state"
43
+ } else if !proof.missing_checks.is_empty() {
44
+ "blocked_by_missing_proof"
45
+ } else if !proof.stale_checks.is_empty() {
46
+ "blocked_by_stale_proof"
47
+ } else if matches!(state, "idle" | "missing") {
48
+ "ready_for_new_task"
49
+ } else if state == "complete" || !scope.in_scope_changed_paths.is_empty() {
50
+ "ready_to_commit"
51
+ } else {
52
+ "healthy"
53
+ }
54
+ .to_string()
55
+ }
56
+
57
+ fn must_do_next(state: &str, proof: &TaskProofStatus) -> Vec<String> {
58
+ match state {
59
+ "blocked_by_missing_proof" | "blocked_by_stale_proof" => action_check_ids(proof),
60
+ value => vec![value.to_string()],
61
+ }
62
+ }
63
+
64
+ fn must_not_do(state: &str) -> Vec<String> {
65
+ let mut items = vec!["Do not bypass the NAOME commit gate.".to_string()];
66
+ if state != "ready_to_commit" {
67
+ items.push("Do not commit until task can-transition allows completion.".to_string());
68
+ }
69
+ items
70
+ }
@@ -0,0 +1,31 @@
1
+ use super::super::agent_model::PolicyHints;
2
+ use super::super::model::{TaskProofStatus, TaskScopeStatus, TaskStatusFinding};
3
+ use super::shared::action_check_ids;
4
+
5
+ pub(in crate::task_state::status) fn policy_hints(
6
+ scope: &TaskScopeStatus,
7
+ proof: &TaskProofStatus,
8
+ findings: &[TaskStatusFinding],
9
+ ) -> PolicyHints {
10
+ let may_edit = if scope.in_scope_changed_paths.is_empty() {
11
+ scope.allowed_paths.clone()
12
+ } else {
13
+ scope.in_scope_changed_paths.clone()
14
+ };
15
+ let mut forbidden_actions = vec![
16
+ "Do not bypass the NAOME commit gate.".to_string(),
17
+ "Do not commit before can-transition or can-commit allows it.".to_string(),
18
+ ];
19
+ if findings
20
+ .iter()
21
+ .any(|finding| finding.id == "task.scope.out_of_scope_change")
22
+ {
23
+ forbidden_actions.push("Do not keep out-of-scope changes in the task diff.".to_string());
24
+ }
25
+ PolicyHints {
26
+ may_edit,
27
+ must_inspect_before_edit: scope.out_of_scope_changed_paths.clone(),
28
+ must_run_after_edit: action_check_ids(proof),
29
+ forbidden_actions,
30
+ }
31
+ }
@@ -0,0 +1,25 @@
1
+ use super::super::agent_model::{ProofRecording, ProofRecordingAfterSuccess};
2
+ use super::super::model::{TaskProofStatus, TaskScopeStatus};
3
+ use super::shared::action_check_ids;
4
+
5
+ pub(in crate::task_state::status) fn proof_recording(
6
+ task_id: Option<&str>,
7
+ scope: &TaskScopeStatus,
8
+ proof: &TaskProofStatus,
9
+ ) -> ProofRecording {
10
+ let checks_to_record = action_check_ids(proof);
11
+ let proof_batch_id = format!(
12
+ "{}-proof",
13
+ task_id.unwrap_or("current-task").replace('_', "-")
14
+ );
15
+ ProofRecording {
16
+ path_set_id: "current-task-diff".to_string(),
17
+ paths: scope.in_scope_changed_paths.clone(),
18
+ proof_batch_id,
19
+ checks_to_record: checks_to_record.clone(),
20
+ after_success: ProofRecordingAfterSuccess {
21
+ record_proof: !checks_to_record.is_empty(),
22
+ instructions: "After each command exits 0, record one proof batch using pathSetId current-task-diff and the listed in-scope paths.".to_string(),
23
+ },
24
+ }
25
+ }
@@ -0,0 +1,19 @@
1
+ use super::super::agent_model::RecoveryGuidance;
2
+ use super::super::model::TaskStatusFinding;
3
+
4
+ pub(in crate::task_state::status) fn recovery_guidance(
5
+ findings: &[TaskStatusFinding],
6
+ ) -> Vec<RecoveryGuidance> {
7
+ findings
8
+ .iter()
9
+ .filter(|finding| {
10
+ finding.id.starts_with("task.git.") || finding.id == "task.state.conflict_markers"
11
+ })
12
+ .map(|finding| RecoveryGuidance {
13
+ id: finding.id.clone(),
14
+ reason: finding.message.clone(),
15
+ safe_next_action: finding.suggested_fix.clone(),
16
+ requires_user_approval: true,
17
+ })
18
+ .collect()
19
+ }
@@ -0,0 +1,125 @@
1
+ use super::super::agent_model::RepairPlanItem;
2
+ use super::super::model::{
3
+ TaskProofStatus, TaskRecommendedCommand, TaskScopeStatus, TaskStatusFinding,
4
+ };
5
+ use super::shared::action_check_ids;
6
+
7
+ pub(in crate::task_state::status) fn repair_plan(
8
+ proof: &TaskProofStatus,
9
+ findings: &[TaskStatusFinding],
10
+ commands: &[TaskRecommendedCommand],
11
+ scope: &TaskScopeStatus,
12
+ ) -> Vec<RepairPlanItem> {
13
+ let mut items = Vec::new();
14
+ items.extend(scope_repairs(findings));
15
+ items.extend(git_repairs(findings));
16
+ items.extend(check_repairs(commands));
17
+ if !proof.missing_checks.is_empty() || !proof.stale_checks.is_empty() {
18
+ items.push(RepairPlanItem {
19
+ id: "record_current_task_proof".to_string(),
20
+ kind: "record_proof".to_string(),
21
+ reason: "Record successful missing or stale checks against the current task diff."
22
+ .to_string(),
23
+ paths: scope.in_scope_changed_paths.clone(),
24
+ check_ids: action_check_ids(proof),
25
+ commands: Vec::new(),
26
+ cwd: None,
27
+ safe_to_execute: true,
28
+ requires_user_approval: false,
29
+ });
30
+ }
31
+ items
32
+ }
33
+
34
+ fn check_repairs(commands: &[TaskRecommendedCommand]) -> Vec<RepairPlanItem> {
35
+ commands
36
+ .iter()
37
+ .map(|command| RepairPlanItem {
38
+ id: format!("rerun_{}", command.check_id),
39
+ kind: "rerun_check".to_string(),
40
+ reason: command.reason.clone(),
41
+ paths: Vec::new(),
42
+ check_ids: vec![command.check_id.clone()],
43
+ commands: vec![command.command.clone()],
44
+ cwd: Some(command.cwd.clone()),
45
+ safe_to_execute: command.safe_to_execute,
46
+ requires_user_approval: !command.safe_to_execute,
47
+ })
48
+ .collect()
49
+ }
50
+
51
+ fn scope_repairs(findings: &[TaskStatusFinding]) -> Vec<RepairPlanItem> {
52
+ findings
53
+ .iter()
54
+ .filter(|finding| finding.id == "task.scope.out_of_scope_change")
55
+ .filter_map(|finding| {
56
+ let path = finding.path.clone()?;
57
+ Some(RepairPlanItem {
58
+ id: format!("remove_out_of_scope_change_{}", repair_path_slug(&path)),
59
+ kind: "git_revert_path".to_string(),
60
+ reason: finding.message.clone(),
61
+ paths: vec![path],
62
+ check_ids: Vec::new(),
63
+ commands: Vec::new(),
64
+ cwd: None,
65
+ safe_to_execute: true,
66
+ requires_user_approval: false,
67
+ })
68
+ })
69
+ .collect()
70
+ }
71
+
72
+ fn git_repairs(findings: &[TaskStatusFinding]) -> Vec<RepairPlanItem> {
73
+ findings
74
+ .iter()
75
+ .filter(|finding| {
76
+ matches!(
77
+ finding.id.as_str(),
78
+ "task.git.operation_in_progress"
79
+ | "task.git.admission_head_missing"
80
+ | "task.git.admission_head_not_reachable"
81
+ )
82
+ })
83
+ .map(|finding| RepairPlanItem {
84
+ id: if finding.id == "task.git.operation_in_progress" {
85
+ "finish_git_operation"
86
+ } else {
87
+ "restart_task"
88
+ }
89
+ .to_string(),
90
+ kind: if finding.id == "task.git.operation_in_progress" {
91
+ "finish_git_operation"
92
+ } else {
93
+ "restart_task"
94
+ }
95
+ .to_string(),
96
+ reason: finding.message.clone(),
97
+ paths: Vec::new(),
98
+ check_ids: Vec::new(),
99
+ commands: Vec::new(),
100
+ cwd: None,
101
+ safe_to_execute: false,
102
+ requires_user_approval: true,
103
+ })
104
+ .collect()
105
+ }
106
+
107
+ fn repair_path_slug(path: &str) -> String {
108
+ let slug = path
109
+ .chars()
110
+ .map(|character| {
111
+ if character.is_ascii_alphanumeric() {
112
+ character.to_ascii_lowercase()
113
+ } else {
114
+ '_'
115
+ }
116
+ })
117
+ .collect::<String>()
118
+ .trim_matches('_')
119
+ .to_string();
120
+ if slug.is_empty() {
121
+ "path".to_string()
122
+ } else {
123
+ slug
124
+ }
125
+ }
@@ -0,0 +1,25 @@
1
+ use super::super::model::{TaskProofStatus, TaskStatusFinding};
2
+
3
+ pub(super) fn action_check_ids(proof: &TaskProofStatus) -> Vec<String> {
4
+ proof
5
+ .missing_checks
6
+ .iter()
7
+ .chain(proof.stale_checks.iter())
8
+ .cloned()
9
+ .collect()
10
+ }
11
+
12
+ pub(super) fn has_finding(findings: &[TaskStatusFinding], id: &str) -> bool {
13
+ findings.iter().any(|finding| finding.id == id)
14
+ }
15
+
16
+ pub(super) fn has_git_recovery(findings: &[TaskStatusFinding]) -> bool {
17
+ findings.iter().any(|finding| {
18
+ matches!(
19
+ finding.id.as_str(),
20
+ "task.git.operation_in_progress"
21
+ | "task.git.admission_head_missing"
22
+ | "task.git.admission_head_not_reachable"
23
+ )
24
+ })
25
+ }
@@ -0,0 +1,16 @@
1
+ mod action;
2
+ mod exit_code;
3
+ mod loop_state;
4
+ mod policy;
5
+ mod proof_recording;
6
+ mod recovery;
7
+ mod repair;
8
+ mod shared;
9
+
10
+ pub(in crate::task_state::status) use action::next_action_v2;
11
+ pub use exit_code::task_status_exit_code;
12
+ pub(in crate::task_state::status) use loop_state::agent_loop;
13
+ pub(in crate::task_state::status) use policy::policy_hints;
14
+ pub(in crate::task_state::status) use proof_recording::proof_recording;
15
+ pub(in crate::task_state::status) use recovery::recovery_guidance;
16
+ pub(in crate::task_state::status) use repair::repair_plan;