@lamentis/naome 1.4.4 → 1.4.6

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 (49) hide show
  1. package/Cargo.lock +2 -2
  2. package/crates/naome-cli/Cargo.toml +1 -1
  3. package/crates/naome-cli/src/main.rs +1 -0
  4. package/crates/naome-cli/src/task_commands/agent_snapshot.rs +19 -70
  5. package/crates/naome-cli/src/task_commands/commit_preflight.rs +29 -41
  6. package/crates/naome-cli/src/task_commands/common.rs +3 -3
  7. package/crates/naome-cli/src/task_commands/loop_control.rs +40 -9
  8. package/crates/naome-cli/src/task_commands/planner/checks.rs +4 -2
  9. package/crates/naome-cli/src/task_commands/preflight.rs +8 -2
  10. package/crates/naome-cli/src/task_commands/record.rs +9 -0
  11. package/crates/naome-cli/src/task_commands/single_pass_action.rs +215 -0
  12. package/crates/naome-cli/src/task_commands/single_pass_action_fields.rs +101 -0
  13. package/crates/naome-cli/src/task_commands.rs +2 -0
  14. package/crates/naome-cli/tests/task_cli_fast_flow.rs +147 -2
  15. package/crates/naome-cli/tests/task_cli_local_state.rs +59 -0
  16. package/crates/naome-cli/tests/task_cli_review_fixes.rs +107 -0
  17. package/crates/naome-core/Cargo.toml +1 -1
  18. package/crates/naome-core/src/information_architecture.rs +1 -1
  19. package/crates/naome-core/src/install_plan.rs +2 -2
  20. package/crates/naome-core/src/route/execution_baselines.rs +3 -1
  21. package/crates/naome-core/src/route/git_ops.rs +48 -1
  22. package/crates/naome-core/src/route/worktree_files.rs +36 -1
  23. package/crates/naome-core/src/task_ledger.rs +13 -2
  24. package/crates/naome-core/src/task_state/commit_gate.rs +29 -0
  25. package/crates/naome-core/src/task_state/completed_refresh.rs +10 -2
  26. package/crates/naome-core/src/task_state/task_diff_api.rs +17 -3
  27. package/crates/naome-core/src/task_state/types.rs +1 -0
  28. package/crates/naome-core/tests/information_architecture.rs +1 -4
  29. package/crates/naome-core/tests/install_plan.rs +7 -0
  30. package/crates/naome-core/tests/repo_support/mod.rs +4 -3
  31. package/crates/naome-core/tests/route_baseline.rs +104 -0
  32. package/crates/naome-core/tests/route_worktree.rs +47 -1
  33. package/crates/naome-core/tests/task_ledger.rs +141 -203
  34. package/crates/naome-core/tests/task_ledger_support/mod.rs +206 -0
  35. package/crates/naome-core/tests/task_state.rs +38 -1
  36. package/crates/naome-core/tests/task_state_compact.rs +1 -1
  37. package/installer/harness-file-ops.js +6 -1
  38. package/installer/harness-files.js +10 -1
  39. package/native/darwin-arm64/naome +0 -0
  40. package/native/linux-x64/naome +0 -0
  41. package/package.json +1 -1
  42. package/templates/naome-root/.naome/bin/check-harness-health.js +4 -4
  43. package/templates/naome-root/.naome/bin/check-task-state.js +4 -4
  44. package/templates/naome-root/.naome/manifest.json +5 -6
  45. package/templates/naome-root/AGENTS.md +3 -1
  46. package/templates/naome-root/docs/naome/agent-workflow.md +4 -3
  47. package/templates/naome-root/docs/naome/architecture.md +2 -2
  48. package/templates/naome-root/docs/naome/execution.md +6 -6
  49. package/templates/naome-root/docs/naome/task-ledger.md +15 -11
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.4.4"
79
+ version = "1.4.6"
80
80
  dependencies = [
81
81
  "naome-core",
82
82
  "serde_json",
@@ -84,7 +84,7 @@ dependencies = [
84
84
 
85
85
  [[package]]
86
86
  name = "naome-core"
87
- version = "1.4.4"
87
+ version = "1.4.6"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.4.4"
3
+ version = "1.4.6"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -197,6 +197,7 @@ fn find_harness_root(start: &Path) -> Option<PathBuf> {
197
197
  &[
198
198
  &[".naome", "task-state.json"],
199
199
  &[".naome", "tasks", "active.json"],
200
+ &[".naome", "manifest.json"],
200
201
  ],
201
202
  )
202
203
  }
@@ -8,6 +8,8 @@ use serde_json::{json, Value};
8
8
 
9
9
  use super::common::{agent_session, print_json_with_session};
10
10
  use super::planner;
11
+ pub(super) use super::single_pass_action::single_pass_next_action;
12
+ use super::single_pass_action_fields::{collect_csv, collect_string_array};
11
13
 
12
14
  pub(super) fn agent_snapshot(
13
15
  root: &Path,
@@ -27,6 +29,21 @@ pub(super) fn agent_snapshot(
27
29
  );
28
30
  let (safe_to_run, deferred) = planner::split_safe_commands(&planned);
29
31
  let can_commit = transition.allowed && status.agent_loop.can_commit;
32
+ let can_record_proof = super::record::can_record_receipts(
33
+ root,
34
+ &proof_plan.proof_recording.checks_to_record,
35
+ &proof_plan.proof_recording.paths,
36
+ )?;
37
+ let next_action = single_pass_next_action(
38
+ &status,
39
+ &proof_plan,
40
+ &transition,
41
+ &planned,
42
+ &safe_to_run,
43
+ &deferred,
44
+ can_commit,
45
+ can_record_proof,
46
+ );
30
47
  let value = json!({
31
48
  "schema": "naome.task.agent-snapshot.v1",
32
49
  "state": snapshot_state(&status),
@@ -63,7 +80,7 @@ pub(super) fn agent_snapshot(
63
80
  "canComplete": transition.allowed,
64
81
  "blockingFindings": transition.blocking_findings
65
82
  },
66
- "nextAction": snapshot_next_action(&status, &proof_plan, can_commit),
83
+ "nextAction": next_action,
67
84
  "agentLoop": status.agent_loop,
68
85
  "repairPlan": status.repair_plan,
69
86
  "findings": status.findings,
@@ -77,7 +94,7 @@ pub(super) fn agent_snapshot(
77
94
  Ok(())
78
95
  }
79
96
 
80
- fn merge_commands(left: Value, right: Vec<Value>) -> Vec<Value> {
97
+ pub(super) fn merge_commands(left: Value, right: Vec<Value>) -> Vec<Value> {
81
98
  let commands = left
82
99
  .as_array()
83
100
  .cloned()
@@ -125,17 +142,6 @@ fn merge_csv_field(existing: &mut Value, incoming: &Value, field: &str) {
125
142
  existing[field] = json!(values.into_iter().collect::<Vec<_>>().join(","));
126
143
  }
127
144
 
128
- fn collect_csv(value: Option<&Value>, values: &mut BTreeSet<String>) {
129
- if let Some(raw) = value.and_then(Value::as_str) {
130
- values.extend(
131
- raw.split(',')
132
- .map(str::trim)
133
- .filter(|part| !part.is_empty())
134
- .map(ToString::to_string),
135
- );
136
- }
137
- }
138
-
139
145
  fn merge_string_array_field(existing: &mut Value, incoming: &Value, field: &str) {
140
146
  let mut values = BTreeSet::new();
141
147
  collect_string_array(existing.get(field), &mut values);
@@ -143,17 +149,6 @@ fn merge_string_array_field(existing: &mut Value, incoming: &Value, field: &str)
143
149
  existing[field] = json!(values.into_iter().collect::<Vec<_>>());
144
150
  }
145
151
 
146
- fn collect_string_array(value: Option<&Value>, values: &mut BTreeSet<String>) {
147
- values.extend(
148
- value
149
- .and_then(Value::as_array)
150
- .into_iter()
151
- .flatten()
152
- .filter_map(Value::as_str)
153
- .map(ToString::to_string),
154
- );
155
- }
156
-
157
152
  fn snapshot_state(status: &naome_core::TaskStatusReportV1) -> &'static str {
158
153
  if status
159
154
  .findings
@@ -183,49 +178,3 @@ fn editable_paths(status: &naome_core::TaskStatusReportV1) -> Vec<String> {
183
178
  }
184
179
  status.scope.allowed_paths.clone()
185
180
  }
186
-
187
- fn snapshot_next_action(
188
- status: &naome_core::TaskStatusReportV1,
189
- proof_plan: &naome_core::TaskProofPlanReport,
190
- can_commit: bool,
191
- ) -> Value {
192
- let action_type = if !status.scope.out_of_scope_changed_paths.is_empty() {
193
- "repair_scope"
194
- } else if status
195
- .findings
196
- .iter()
197
- .any(|finding| finding.id.starts_with("task.git."))
198
- {
199
- "recover_git"
200
- } else if !status.proof.missing_checks.is_empty() || !status.proof.stale_checks.is_empty() {
201
- "run_checks"
202
- } else if !proof_plan.proof_recording.checks_to_record.is_empty() {
203
- "record_proof"
204
- } else if can_commit {
205
- "commit_ready"
206
- } else if status.state == "implementing" && status.agent_loop.can_continue_editing {
207
- "edit"
208
- } else {
209
- "none"
210
- };
211
- if can_commit && action_type == "commit_ready" {
212
- return json!({
213
- "type": "commit_ready",
214
- "reason": "Task is complete enough to commit; do not continue editing before commit.",
215
- "commands": [],
216
- "paths": status.scope.in_scope_changed_paths,
217
- "checkIds": [],
218
- "safeToExecute": false,
219
- "requiresUserApproval": true
220
- });
221
- }
222
- json!({
223
- "type": action_type,
224
- "reason": status.next_action_v2.reason,
225
- "commands": status.next_action_v2.commands,
226
- "paths": status.next_action_v2.paths,
227
- "checkIds": status.next_action_v2.check_ids,
228
- "safeToExecute": status.next_action_v2.safe_to_execute,
229
- "requiresUserApproval": status.next_action_v2.requires_user_approval
230
- })
231
- }
@@ -1,10 +1,12 @@
1
1
  use std::path::Path;
2
2
 
3
- use naome_core::{task_status_exit_code, task_status_report, task_transition_readiness};
4
- use naome_core::{TaskStatusFinding, TaskStatusReportV1};
5
- use serde_json::{json, Value};
3
+ use naome_core::{
4
+ task_status_exit_code, task_status_report, task_transition_readiness, TaskStatusReportV1,
5
+ };
6
+ use serde_json::json;
6
7
 
7
8
  use super::common::{agent_session, print_json_with_session};
9
+ use super::{agent_snapshot, planner};
8
10
 
9
11
  pub(super) fn commit_preflight(
10
12
  root: &Path,
@@ -12,8 +14,18 @@ pub(super) fn commit_preflight(
12
14
  ) -> Result<(), Box<dyn std::error::Error>> {
13
15
  let session = agent_session(args)?;
14
16
  let status = task_status_report(root)?;
17
+ let proof_plan = naome_core::task_proof_plan(root)?;
15
18
  let transition = task_transition_readiness(root, "complete")?;
16
19
  let would_pass = transition.allowed && status.agent_loop.can_commit;
20
+ let planned = agent_snapshot::merge_commands(
21
+ serde_json::to_value(&proof_plan.recommended_commands)?,
22
+ planner::planned_commands(
23
+ root,
24
+ &status.scope.in_scope_changed_paths,
25
+ Some(&status.proof),
26
+ ),
27
+ );
28
+ let (safe_to_run, deferred) = planner::split_safe_commands(&planned);
17
29
  let blocking = if would_pass {
18
30
  Vec::new()
19
31
  } else if !transition.blocking_findings.is_empty() {
@@ -28,7 +40,20 @@ pub(super) fn commit_preflight(
28
40
  "wouldPass": would_pass,
29
41
  "commitPaths": status.scope.in_scope_changed_paths,
30
42
  "blockingFindings": blocking,
31
- "nextAction": commit_preflight_next_action(would_pass, &blocking, &status)?,
43
+ "nextAction": agent_snapshot::single_pass_next_action(
44
+ &status,
45
+ &proof_plan,
46
+ &transition,
47
+ &planned,
48
+ &safe_to_run,
49
+ &deferred,
50
+ would_pass,
51
+ super::record::can_record_receipts(
52
+ root,
53
+ &proof_plan.proof_recording.checks_to_record,
54
+ &proof_plan.proof_recording.paths
55
+ )?
56
+ ),
32
57
  "agentInstruction": if would_pass { "Commit gate preflight is clean; use the normal NAOME commit path." } else { "Resolve blocking findings before committing." }
33
58
  }),
34
59
  session.as_deref(),
@@ -50,40 +75,3 @@ fn commit_preflight_exit_code(would_pass: bool, status: &TaskStatusReportV1) ->
50
75
  code
51
76
  }
52
77
  }
53
-
54
- fn commit_preflight_next_action(
55
- would_pass: bool,
56
- blocking: &[TaskStatusFinding],
57
- status: &TaskStatusReportV1,
58
- ) -> Result<Value, Box<dyn std::error::Error>> {
59
- if would_pass {
60
- return Ok(json!({
61
- "type": "commit_ready",
62
- "reason": "Task state, scope, proof, and transition checks are commit-ready.",
63
- "commands": [],
64
- "paths": status.scope.in_scope_changed_paths,
65
- "checkIds": [],
66
- "safeToExecute": false,
67
- "requiresUserApproval": true
68
- }));
69
- }
70
- if has_status_blocker(status) {
71
- return Ok(serde_json::to_value(&status.next_action_v2)?);
72
- }
73
- if let Some(primary) = blocking.first() {
74
- return Ok(json!({
75
- "type": "blocked",
76
- "reason": primary.message,
77
- "commands": [],
78
- "paths": primary.path.as_ref().map(|path| vec![path.clone()]).unwrap_or_default(),
79
- "checkIds": [],
80
- "safeToExecute": false,
81
- "requiresUserApproval": true
82
- }));
83
- }
84
- Ok(serde_json::to_value(&status.next_action_v2)?)
85
- }
86
-
87
- fn has_status_blocker(status: &TaskStatusReportV1) -> bool {
88
- task_status_exit_code(&status.findings, &status.proof) != 0
89
- }
@@ -1,12 +1,12 @@
1
1
  use std::fs;
2
2
  use std::path::Path;
3
3
 
4
+ use naome_core::read_task_state_projection;
4
5
  use serde_json::{json, Value};
5
6
 
6
7
  pub(super) fn read_task_state(root: &Path) -> Result<Value, Box<dyn std::error::Error>> {
7
- Ok(serde_json::from_str(&fs::read_to_string(
8
- root.join(".naome/task-state.json"),
9
- )?)?)
8
+ read_task_state_projection(root)?
9
+ .ok_or_else(|| "NAOME task-state projection is unavailable.".into())
10
10
  }
11
11
 
12
12
  pub(super) fn write_task_state(
@@ -4,6 +4,7 @@ use naome_core::{task_proof_plan, task_status_report, task_transition_readiness}
4
4
  use serde_json::json;
5
5
 
6
6
  use super::common::{agent_session, print_json_with_session, read_task_state, write_task_state};
7
+ use super::{agent_snapshot, planner};
7
8
 
8
9
  pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
9
10
  let session = agent_session(args)?;
@@ -11,13 +12,19 @@ pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
11
12
  let mut executed_steps = Vec::new();
12
13
 
13
14
  if execute_safe {
14
- let plan = task_proof_plan(root)?;
15
- for item in plan
16
- .repair_plan
17
- .iter()
18
- .filter(|item| item.kind == "rerun_check" && item.safe_to_execute)
19
- {
20
- if let Some(check_id) = item.check_ids.first() {
15
+ let status = task_status_report(root)?;
16
+ let proof_plan = task_proof_plan(root)?;
17
+ let planned = agent_snapshot::merge_commands(
18
+ serde_json::to_value(&proof_plan.recommended_commands)?,
19
+ planner::planned_commands(
20
+ root,
21
+ &status.scope.in_scope_changed_paths,
22
+ Some(&status.proof),
23
+ ),
24
+ );
25
+ let (safe_to_run, _deferred) = planner::split_safe_commands(&planned);
26
+ for command in safe_to_run {
27
+ if let Some(check_id) = command.get("checkId").and_then(serde_json::Value::as_str) {
21
28
  let step =
22
29
  super::check_run::run_check_by_id(root, check_id, true, session.as_deref())?;
23
30
  let failed = step
@@ -49,9 +56,33 @@ pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
49
56
  let status = task_status_report(root)?;
50
57
  let proof_plan = task_proof_plan(root)?;
51
58
  let transition = task_transition_readiness(root, "complete")?;
59
+ let planned = agent_snapshot::merge_commands(
60
+ serde_json::to_value(&proof_plan.recommended_commands)?,
61
+ planner::planned_commands(
62
+ root,
63
+ &status.scope.in_scope_changed_paths,
64
+ Some(&status.proof),
65
+ ),
66
+ );
67
+ let (safe_to_run, deferred) = planner::split_safe_commands(&planned);
68
+ let commit_ready = transition.allowed && status.agent_loop.can_commit;
69
+ let next_action = agent_snapshot::single_pass_next_action(
70
+ &status,
71
+ &proof_plan,
72
+ &transition,
73
+ &planned,
74
+ &safe_to_run,
75
+ &deferred,
76
+ commit_ready,
77
+ super::record::can_record_receipts(
78
+ root,
79
+ &proof_plan.proof_recording.checks_to_record,
80
+ &proof_plan.proof_recording.paths,
81
+ )?,
82
+ );
52
83
  let can_commit = json!({
53
84
  "schema": "naome.task.commit-readiness.v1",
54
- "allowed": transition.allowed && status.agent_loop.can_commit,
85
+ "allowed": commit_ready,
55
86
  "commitPaths": status.scope.in_scope_changed_paths,
56
87
  "blockingFindings": transition.blocking_findings,
57
88
  "agentLoop": status.agent_loop
@@ -65,7 +96,7 @@ pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
65
96
  "canTransition": transition,
66
97
  "canCommit": can_commit,
67
98
  "executedSteps": executed_steps,
68
- "nextAction": status.next_action_v2,
99
+ "nextAction": next_action,
69
100
  "agentInstruction": if execute_safe { "Executed only safe check/proof steps; no edits, git recovery, commit, push, or PR actions were performed." } else { "Read-only loop report; execute only safe plans explicitly marked safe." }
70
101
  }),
71
102
  session.as_deref(),
@@ -119,6 +119,9 @@ fn proof_reason(reasons: &BTreeSet<String>) -> &'static str {
119
119
 
120
120
  fn check_is_impacted(check: &VerificationCheck, path: &str) -> bool {
121
121
  let command = check.command.as_str();
122
+ if command == "git diff --check" {
123
+ return true;
124
+ }
122
125
  match impact_kind(path) {
123
126
  "changed_task_state" => {
124
127
  command.contains("check-task-state") || command.contains("test:task-state")
@@ -137,8 +140,7 @@ fn check_is_impacted(check: &VerificationCheck, path: &str) -> bool {
137
140
  || command.contains("test:")
138
141
  }
139
142
  _ => {
140
- command == "git diff --check"
141
- || command.contains("quality check")
143
+ command.contains("quality check")
142
144
  || command.contains("semantic check")
143
145
  || command.contains("arch validate")
144
146
  }
@@ -29,6 +29,11 @@ pub(super) fn preflight(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
29
29
  let recommended = planner::planned_commands(root, &normalized_paths, Some(&status.proof));
30
30
  let (safe, _deferred) = planner::split_safe_commands(&recommended);
31
31
  let blocked = !findings.is_empty() || !must_not_edit.is_empty();
32
+ let next_action_paths = if blocked {
33
+ must_not_edit.clone()
34
+ } else {
35
+ normalized_paths.clone()
36
+ };
32
37
  print_json_with_session(
33
38
  json!({
34
39
  "schema": "naome.task.preflight.v1",
@@ -40,8 +45,9 @@ pub(super) fn preflight(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
40
45
  "findings": findings,
41
46
  "nextAction": {
42
47
  "type": if blocked { "blocked" } else { "edit" },
43
- "reason": if blocked { "Preflight is blocked until every requested path is explicit and editable." } else { "All requested paths are editable." },
44
- "paths": must_not_edit,
48
+ "actionId": if blocked { "preflight.blocked" } else { "preflight.edit_allowed" },
49
+ "reasonCodes": if blocked { vec!["preflight-blocked"] } else { vec!["preflight-clean"] },
50
+ "paths": next_action_paths,
45
51
  "commands": [],
46
52
  "checkIds": [],
47
53
  "safeToExecute": !blocked,
@@ -98,6 +98,15 @@ pub(super) fn record_receipts_for_checks(
98
98
  Ok(true)
99
99
  }
100
100
 
101
+ pub(super) fn can_record_receipts(
102
+ root: &Path,
103
+ check_ids: &[String],
104
+ paths: &[String],
105
+ ) -> Result<bool, Box<dyn std::error::Error>> {
106
+ let (can_record, _findings, receipts) = recordable_receipts(root, check_ids, paths)?;
107
+ Ok(can_record && !receipts.is_empty())
108
+ }
109
+
101
110
  fn write_proof_batch(
102
111
  root: &Path,
103
112
  path_set_base: &str,
@@ -0,0 +1,215 @@
1
+ use naome_core::{TaskProofPlanReport, TaskStatusReportV1, TransitionReadinessReport};
2
+ use serde_json::{json, Value};
3
+
4
+ use super::single_pass_action_fields::{
5
+ action, check_ids, impacted_paths, primary_finding_id, reason_codes,
6
+ };
7
+
8
+ pub(super) fn single_pass_next_action(
9
+ status: &TaskStatusReportV1,
10
+ proof_plan: &TaskProofPlanReport,
11
+ transition: &TransitionReadinessReport,
12
+ planned: &[Value],
13
+ safe_to_run: &[Value],
14
+ deferred: &[Value],
15
+ can_commit: bool,
16
+ can_record_proof: bool,
17
+ ) -> Value {
18
+ human_review_action(transition)
19
+ .or_else(|| git_recovery_action(status))
20
+ .or_else(|| scope_drift_action(status))
21
+ .or_else(|| record_action(proof_plan, can_record_proof))
22
+ .or_else(|| proof_action(status, safe_to_run, deferred))
23
+ .or_else(|| transition_blocker_action(transition))
24
+ .unwrap_or_else(|| {
25
+ if can_commit {
26
+ commit_action(status)
27
+ } else if status.state == "implementing" && status.agent_loop.can_continue_editing {
28
+ edit_action(status)
29
+ } else {
30
+ no_action(status, planned)
31
+ }
32
+ })
33
+ }
34
+
35
+ fn human_review_action(transition: &TransitionReadinessReport) -> Option<Value> {
36
+ transition
37
+ .blocking_findings
38
+ .iter()
39
+ .find(|finding| finding.id == "task.transition.human_review_required")
40
+ .map(|finding| {
41
+ blocking_action(
42
+ "human_review",
43
+ "human_review.required",
44
+ &["human-review-required"],
45
+ finding.path.iter().cloned().collect(),
46
+ &finding.id,
47
+ )
48
+ })
49
+ }
50
+
51
+ fn git_recovery_action(status: &TaskStatusReportV1) -> Option<Value> {
52
+ status
53
+ .findings
54
+ .iter()
55
+ .any(|finding| finding.id.starts_with("task.git."))
56
+ .then(|| {
57
+ action(
58
+ "recover_git_state",
59
+ "git.recover_state",
60
+ &["git-recovery-required"],
61
+ &[],
62
+ Vec::new(),
63
+ &[],
64
+ false,
65
+ true,
66
+ primary_finding_id(status, "task.git.").as_deref(),
67
+ )
68
+ })
69
+ }
70
+
71
+ fn scope_drift_action(status: &TaskStatusReportV1) -> Option<Value> {
72
+ (!status.scope.out_of_scope_changed_paths.is_empty()).then(|| {
73
+ action(
74
+ "repair_scope",
75
+ "scope.repair_drift",
76
+ &["scope-drift"],
77
+ &[],
78
+ status.scope.out_of_scope_changed_paths.clone(),
79
+ &[],
80
+ false,
81
+ true,
82
+ primary_finding_id(status, "task.scope.out_of_scope_change").as_deref(),
83
+ )
84
+ })
85
+ }
86
+
87
+ fn proof_action(status: &TaskStatusReportV1, safe: &[Value], deferred: &[Value]) -> Option<Value> {
88
+ (!status.proof.missing_checks.is_empty() || !status.proof.stale_checks.is_empty()).then(|| {
89
+ let commands = if safe.is_empty() { deferred } else { safe };
90
+ action(
91
+ "run_checks",
92
+ "proof.run_required_checks",
93
+ &reason_codes(commands, &status.proof),
94
+ commands,
95
+ impacted_paths(commands, &status.scope.in_scope_changed_paths),
96
+ &check_ids(commands, &status.proof),
97
+ !safe.is_empty(),
98
+ safe.is_empty(),
99
+ primary_finding_id(status, "task.proof.")
100
+ .or_else(|| primary_finding_id(status, "task.scope.missing_evidence"))
101
+ .as_deref(),
102
+ )
103
+ })
104
+ }
105
+
106
+ fn record_action(proof_plan: &TaskProofPlanReport, can_record_proof: bool) -> Option<Value> {
107
+ can_record_proof.then(|| {
108
+ action(
109
+ "record_proof",
110
+ "proof.record_from_receipts",
111
+ &["proof-recording-available"],
112
+ &[json!({
113
+ "command": "node .naome/bin/naome.js task record-proof --from-proof-plan --json",
114
+ "cwd": ".",
115
+ "safeToExecute": true
116
+ })],
117
+ proof_plan.proof_recording.paths.clone(),
118
+ &proof_plan.proof_recording.checks_to_record,
119
+ true,
120
+ false,
121
+ None,
122
+ )
123
+ })
124
+ }
125
+
126
+ fn transition_blocker_action(transition: &TransitionReadinessReport) -> Option<Value> {
127
+ transition
128
+ .blocking_findings
129
+ .iter()
130
+ .find(|finding| unhandled_transition_blocker(&finding.id))
131
+ .map(|finding| {
132
+ blocking_action(
133
+ "blocked",
134
+ "transition.blocked",
135
+ &["transition-blocked"],
136
+ finding.path.iter().cloned().collect(),
137
+ &finding.id,
138
+ )
139
+ })
140
+ }
141
+
142
+ fn blocking_action(
143
+ action_type: &str,
144
+ action_id: &str,
145
+ reason_codes: &[&str],
146
+ paths: Vec<String>,
147
+ finding_id: &str,
148
+ ) -> Value {
149
+ action(
150
+ action_type,
151
+ action_id,
152
+ reason_codes,
153
+ &[],
154
+ paths,
155
+ &[],
156
+ false,
157
+ true,
158
+ Some(finding_id),
159
+ )
160
+ }
161
+
162
+ fn unhandled_transition_blocker(id: &str) -> bool {
163
+ !(id == "task.transition.human_review_required"
164
+ || id.starts_with("task.git.")
165
+ || id.starts_with("task.proof.")
166
+ || id == "task.scope.out_of_scope_change"
167
+ || id == "task.scope.missing_evidence"
168
+ || id == "task.transition.stale_proof")
169
+ }
170
+
171
+ fn commit_action(status: &TaskStatusReportV1) -> Value {
172
+ action(
173
+ "commit_ready",
174
+ "commit.ready",
175
+ &["commit-ready"],
176
+ &[],
177
+ status.scope.in_scope_changed_paths.clone(),
178
+ &[],
179
+ false,
180
+ true,
181
+ None,
182
+ )
183
+ }
184
+
185
+ fn edit_action(status: &TaskStatusReportV1) -> Value {
186
+ action(
187
+ "edit",
188
+ "edit.required",
189
+ &["edit-required"],
190
+ &[json!({
191
+ "command": "node .naome/bin/naome.js task preflight --path <path> --json",
192
+ "cwd": ".",
193
+ "safeToExecute": false
194
+ })],
195
+ status.scope.allowed_paths.clone(),
196
+ &[],
197
+ false,
198
+ true,
199
+ None,
200
+ )
201
+ }
202
+
203
+ fn no_action(status: &TaskStatusReportV1, planned: &[Value]) -> Value {
204
+ action(
205
+ "none",
206
+ "task.no_action",
207
+ &["no-action"],
208
+ planned,
209
+ status.scope.in_scope_changed_paths.clone(),
210
+ &[],
211
+ false,
212
+ false,
213
+ None,
214
+ )
215
+ }