@lamentis/naome 1.3.17 → 1.4.1

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 (55) hide show
  1. package/Cargo.lock +2 -2
  2. package/crates/naome-cli/Cargo.toml +1 -1
  3. package/crates/naome-cli/src/architecture_commands.rs +16 -3
  4. package/crates/naome-cli/src/main.rs +10 -1
  5. package/crates/naome-cli/src/task_commands/common.rs +32 -0
  6. package/crates/naome-cli/src/task_commands/readiness.rs +40 -0
  7. package/crates/naome-cli/src/task_commands/record.rs +134 -0
  8. package/crates/naome-cli/src/task_commands/repair.rs +30 -0
  9. package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
  10. package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
  11. package/crates/naome-cli/src/task_commands.rs +69 -1
  12. package/crates/naome-cli/tests/task_cli.rs +58 -0
  13. package/crates/naome-cli/tests/task_cli_agent_controls.rs +217 -0
  14. package/crates/naome-cli/tests/task_cli_control.rs +126 -0
  15. package/crates/naome-cli/tests/task_cli_support/mod.rs +150 -0
  16. package/crates/naome-core/Cargo.toml +1 -1
  17. package/crates/naome-core/src/architecture/output.rs +196 -0
  18. package/crates/naome-core/src/architecture/scan/cache.rs +1 -1
  19. package/crates/naome-core/src/architecture.rs +1 -0
  20. package/crates/naome-core/src/lib.rs +15 -9
  21. package/crates/naome-core/src/task_state/mod.rs +10 -0
  22. package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
  23. package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
  24. package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
  25. package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
  26. package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
  27. package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
  28. package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
  29. package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
  30. package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
  31. package/crates/naome-core/src/task_state/status/control.rs +16 -0
  32. package/crates/naome-core/src/task_state/status/git.rs +133 -0
  33. package/crates/naome-core/src/task_state/status/model.rs +150 -0
  34. package/crates/naome-core/src/task_state/status/proof.rs +167 -0
  35. package/crates/naome-core/src/task_state/status/proof_read.rs +150 -0
  36. package/crates/naome-core/src/task_state/status/report.rs +148 -0
  37. package/crates/naome-core/src/task_state/status/report_context.rs +126 -0
  38. package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
  39. package/crates/naome-core/src/task_state/status/scope.rs +111 -0
  40. package/crates/naome-core/src/task_state/status/transition.rs +73 -0
  41. package/crates/naome-core/src/task_state/status.rs +23 -0
  42. package/crates/naome-core/src/task_state/status_output.rs +103 -0
  43. package/crates/naome-core/tests/architecture_cache.rs +1 -1
  44. package/crates/naome-core/tests/architecture_config.rs +68 -1
  45. package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
  46. package/crates/naome-core/tests/task_state_support/states.rs +4 -0
  47. package/crates/naome-core/tests/task_status.rs +301 -0
  48. package/crates/naome-core/tests/task_status_git.rs +141 -0
  49. package/native/darwin-arm64/naome +0 -0
  50. package/native/linux-x64/naome +0 -0
  51. package/package.json +1 -1
  52. package/templates/naome-root/.naome/bin/check-harness-health.js +1 -1
  53. package/templates/naome-root/.naome/bin/check-task-state.js +1 -1
  54. package/templates/naome-root/.naome/manifest.json +2 -2
  55. package/templates/naome-root/docs/naome/architecture-fitness.md +23 -23
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.3.17"
79
+ version = "1.4.1"
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.3.17"
87
+ version = "1.4.1"
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.3.17"
3
+ version = "1.4.1"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -2,9 +2,9 @@ use std::fs;
2
2
  use std::path::{Path, PathBuf};
3
3
 
4
4
  use naome_core::{
5
- config_findings_for, format_architecture_explain, format_architecture_scan,
6
- format_architecture_validation, scan_architecture, validate_architecture,
7
- ArchitectureScanOptions, ARCHITECTURE_RULE_IDS,
5
+ architecture_validation_sarif_with_root, config_findings_for, format_architecture_explain,
6
+ format_architecture_scan, format_architecture_validation, scan_architecture,
7
+ validate_architecture, ArchitectureScanOptions, ARCHITECTURE_RULE_IDS,
8
8
  };
9
9
 
10
10
  use crate::architecture_init::architecture_init_config_text;
@@ -88,6 +88,19 @@ fn run_arch_validate(root: &Path, args: &[String]) -> Result<(), Box<dyn std::er
88
88
  root,
89
89
  scan_options(root, args, has_flag(args, "--changed-only")),
90
90
  )?;
91
+ if has_flag(args, "--sarif") {
92
+ let output =
93
+ serde_json::to_string_pretty(&architecture_validation_sarif_with_root(&report, root))?;
94
+ if let Some(path) = option_value(args, "--output") {
95
+ fs::write(root.join(path), output)?;
96
+ } else {
97
+ println!("{output}");
98
+ }
99
+ if report.status == "fail" {
100
+ std::process::exit(1);
101
+ }
102
+ return Ok(());
103
+ }
91
104
  let json = has_flag(args, "--json") || has_flag(args, "--agent-feedback");
92
105
  if json {
93
106
  if has_flag(args, "--agent-feedback") {
@@ -37,6 +37,15 @@ const HELP: &str = r#"Usage:
37
37
  naome sync [--package-root <path>] [--installer-js <path>]
38
38
  naome install-plan [--harness-version <version>]
39
39
  naome seed-verification
40
+ naome task status [--json] [--exit-code]
41
+ naome task proof-plan [--json] [--exit-code]
42
+ naome task can-transition --to complete [--json]
43
+ naome task can-commit --json
44
+ naome task repair --plan <id> --dry-run --json
45
+ naome task record-proof --from-proof-plan [--dry-run] --json
46
+ naome task request-scope --path <path> --reason <reason> --json
47
+ naome task timeline --json
48
+ naome task loop-snapshot --json
40
49
  naome task render-state [--write] [--json]
41
50
  naome task migrate-ledger [--write] [--json]
42
51
  naome refresh-integrity [--root <path>] [--json]
@@ -62,7 +71,7 @@ const HELP: &str = r#"Usage:
62
71
  naome arch init [--config <path>] [--json]
63
72
  naome arch explain [--config <path>] [--json]
64
73
  naome arch scan [--config <path>] [--changed-only] [--write] [--output <path>] [--json]
65
- naome arch validate [--config <path>] [--changed-only] [--agent-feedback] [--json]
74
+ naome arch validate [--config <path>] [--changed-only] [--agent-feedback] [--json|--sarif] [--output <path>]
66
75
  naome workflow search-profile [--json]
67
76
  naome workflow agent-plan [--json]
68
77
  naome workflow context-delta [--read-path <path>...] [--json]
@@ -0,0 +1,32 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ 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
+ )?)?)
10
+ }
11
+
12
+ pub(super) fn print_json(value: Value) -> Result<(), Box<dyn std::error::Error>> {
13
+ println!("{}", serde_json::to_string_pretty(&value)?);
14
+ Ok(())
15
+ }
16
+
17
+ pub(super) fn value_after<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
18
+ args.windows(2)
19
+ .find(|window| window[0] == flag)
20
+ .map(|window| window[1].as_str())
21
+ }
22
+
23
+ pub(super) fn repeated_values(args: &[String], flag: &str) -> Vec<String> {
24
+ let mut values = args
25
+ .windows(2)
26
+ .filter(|window| window[0] == flag)
27
+ .map(|window| window[1].clone())
28
+ .collect::<Vec<_>>();
29
+ values.sort();
30
+ values.dedup();
31
+ values
32
+ }
@@ -0,0 +1,40 @@
1
+ use std::collections::BTreeSet;
2
+ use std::path::Path;
3
+
4
+ use naome_core::{task_status_report, task_transition_readiness};
5
+ use serde_json::json;
6
+
7
+ use super::common::print_json;
8
+
9
+ pub(super) fn can_commit(root: &Path, _args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
10
+ let status = task_status_report(root)?;
11
+ let transition = task_transition_readiness(root, "complete")?;
12
+ let required = commit_requirements(&status, &transition.blocking_findings);
13
+ print_json(json!({
14
+ "schema": "naome.task.commit-readiness.v1",
15
+ "allowed": transition.allowed && status.agent_loop.can_commit && required.is_empty(),
16
+ "commitPaths": status.scope.in_scope_changed_paths,
17
+ "blockingFindings": transition.blocking_findings,
18
+ "requiredBeforeCommit": required,
19
+ "agentLoop": status.agent_loop
20
+ }))
21
+ }
22
+
23
+ fn commit_requirements(
24
+ status: &naome_core::TaskStatusReportV1,
25
+ blockers: &[naome_core::TaskStatusFinding],
26
+ ) -> Vec<String> {
27
+ let mut requirements = BTreeSet::new();
28
+ for check in status
29
+ .proof
30
+ .missing_checks
31
+ .iter()
32
+ .chain(status.proof.stale_checks.iter())
33
+ {
34
+ requirements.insert(format!("Run and record required check: {check}."));
35
+ }
36
+ for finding in blockers {
37
+ requirements.insert(finding.suggested_fix.clone());
38
+ }
39
+ requirements.into_iter().collect()
40
+ }
@@ -0,0 +1,134 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use naome_core::task_proof_plan;
5
+ use serde_json::{json, Value};
6
+
7
+ use super::common::{print_json, read_task_state};
8
+
9
+ pub(super) fn record_proof(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
10
+ if !args.iter().any(|arg| arg == "--from-proof-plan") {
11
+ return Err("naome task record-proof requires --from-proof-plan".into());
12
+ }
13
+ let plan = task_proof_plan(root)?;
14
+ let dry_run = args.iter().any(|arg| arg == "--dry-run");
15
+ let recorded = !dry_run && !plan.proof_recording.checks_to_record.is_empty();
16
+ let recorded_ids = if recorded {
17
+ write_proof_batch(root, &plan.proof_recording)?
18
+ } else {
19
+ RecordedProof {
20
+ path_set_id: plan.proof_recording.path_set_id.clone(),
21
+ proof_batch_id: plan.proof_recording.proof_batch_id.clone(),
22
+ }
23
+ };
24
+ if dry_run && !plan.proof_recording.checks_to_record.is_empty() {
25
+ print_json(json!({
26
+ "schema": "naome.task.record-proof.v1",
27
+ "dryRun": dry_run,
28
+ "recorded": recorded,
29
+ "pathSetId": recorded_ids.path_set_id,
30
+ "paths": plan.proof_recording.paths,
31
+ "proofBatchId": recorded_ids.proof_batch_id,
32
+ "checksRecorded": plan.proof_recording.checks_to_record,
33
+ "agentInstruction": "Record proof only after the listed checks exited 0 for the current task diff."
34
+ }))?;
35
+ return Ok(());
36
+ }
37
+ print_json(json!({
38
+ "schema": "naome.task.record-proof.v1",
39
+ "dryRun": dry_run,
40
+ "recorded": recorded,
41
+ "pathSetId": recorded_ids.path_set_id,
42
+ "paths": plan.proof_recording.paths,
43
+ "proofBatchId": recorded_ids.proof_batch_id,
44
+ "checksRecorded": plan.proof_recording.checks_to_record,
45
+ "agentInstruction": "Record proof only after the listed checks exited 0 for the current task diff."
46
+ }))
47
+ }
48
+
49
+ struct RecordedProof {
50
+ path_set_id: String,
51
+ proof_batch_id: String,
52
+ }
53
+
54
+ fn write_proof_batch(
55
+ root: &Path,
56
+ recording: &naome_core::ProofRecording,
57
+ ) -> Result<RecordedProof, Box<dyn std::error::Error>> {
58
+ let path = root.join(".naome/task-state.json");
59
+ let mut state = read_task_state(root)?;
60
+ let checked_at = state
61
+ .get("updatedAt")
62
+ .and_then(Value::as_str)
63
+ .unwrap_or("1970-01-01T00:00:00.000Z")
64
+ .to_string();
65
+ let active = state
66
+ .get_mut("activeTask")
67
+ .and_then(Value::as_object_mut)
68
+ .ok_or("task-state has no activeTask object")?;
69
+ let path_sets = active
70
+ .entry("proofPathSets")
71
+ .or_insert_with(|| json!({}))
72
+ .as_object_mut()
73
+ .ok_or("activeTask.proofPathSets must be an object")?;
74
+ let path_set_id = unique_path_set_id(path_sets, &recording.path_set_id);
75
+ path_sets.insert(path_set_id.clone(), json!(recording.paths));
76
+ let batches = active
77
+ .entry("proofBatches")
78
+ .or_insert_with(|| json!([]))
79
+ .as_array_mut()
80
+ .ok_or("activeTask.proofBatches must be an array")?;
81
+ let proof_batch_id = unique_proof_batch_id(batches, &recording.proof_batch_id);
82
+ batches.push(proof_batch(
83
+ recording,
84
+ &checked_at,
85
+ &path_set_id,
86
+ &proof_batch_id,
87
+ ));
88
+ fs::write(path, format!("{}\n", serde_json::to_string_pretty(&state)?))?;
89
+ Ok(RecordedProof {
90
+ path_set_id,
91
+ proof_batch_id,
92
+ })
93
+ }
94
+
95
+ fn proof_batch(
96
+ recording: &naome_core::ProofRecording,
97
+ checked_at: &str,
98
+ path_set_id: &str,
99
+ proof_batch_id: &str,
100
+ ) -> Value {
101
+ json!({
102
+ "id": proof_batch_id,
103
+ "checkedAt": checked_at,
104
+ "evidencePathSet": path_set_id,
105
+ "proofs": recording.checks_to_record.iter().map(|check_id| {
106
+ json!({ "checkId": check_id, "exitCode": 0 })
107
+ }).collect::<Vec<_>>()
108
+ })
109
+ }
110
+
111
+ fn unique_path_set_id(path_sets: &serde_json::Map<String, Value>, base: &str) -> String {
112
+ unique_id(base, |candidate| path_sets.contains_key(candidate))
113
+ }
114
+
115
+ fn unique_proof_batch_id(batches: &[Value], base: &str) -> String {
116
+ unique_id(base, |candidate| {
117
+ batches
118
+ .iter()
119
+ .any(|batch| batch.get("id").and_then(Value::as_str) == Some(candidate))
120
+ })
121
+ }
122
+
123
+ fn unique_id(base: &str, exists: impl Fn(&str) -> bool) -> String {
124
+ if !exists(base) {
125
+ return base.to_string();
126
+ }
127
+ for index in 2.. {
128
+ let candidate = format!("{base}-{index}");
129
+ if !exists(&candidate) {
130
+ return candidate;
131
+ }
132
+ }
133
+ unreachable!("unbounded proof id suffix search must find an available id")
134
+ }
@@ -0,0 +1,30 @@
1
+ use std::path::Path;
2
+
3
+ use naome_core::task_status_report;
4
+ use serde_json::json;
5
+
6
+ use super::common::{print_json, value_after};
7
+
8
+ pub(super) fn repair_preview(
9
+ root: &Path,
10
+ args: &[String],
11
+ ) -> Result<(), Box<dyn std::error::Error>> {
12
+ if !args.iter().any(|arg| arg == "--dry-run") {
13
+ return Err("naome task repair requires --dry-run in v1.4.1".into());
14
+ }
15
+ let plan_id = value_after(args, "--plan").ok_or("naome task repair requires --plan <id>")?;
16
+ let status = task_status_report(root)?;
17
+ let plan = status
18
+ .repair_plan
19
+ .iter()
20
+ .find(|item| item.id == plan_id)
21
+ .cloned();
22
+ print_json(json!({
23
+ "schema": "naome.task.repair-preview.v1",
24
+ "planId": plan_id,
25
+ "found": plan.is_some(),
26
+ "wouldExecute": false,
27
+ "plan": plan,
28
+ "agentInstruction": "Review this dry-run output and execute only safe commands explicitly allowed by NAOME."
29
+ }))
30
+ }
@@ -0,0 +1,24 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::json;
4
+
5
+ use super::common::{print_json, repeated_values, value_after};
6
+
7
+ pub(super) fn request_scope(
8
+ _root: &Path,
9
+ args: &[String],
10
+ ) -> Result<(), Box<dyn std::error::Error>> {
11
+ let requested_paths = repeated_values(args, "--path");
12
+ if requested_paths.is_empty() {
13
+ return Err("naome task request-scope requires at least one --path".into());
14
+ }
15
+ let reason = value_after(args, "--reason").unwrap_or("No reason provided.");
16
+ print_json(json!({
17
+ "schema": "naome.task.scope-request.v1",
18
+ "wouldMutate": false,
19
+ "requestedPaths": requested_paths,
20
+ "reason": reason,
21
+ "suggestedFix": "Ask for an explicit task-scope revision before editing these paths.",
22
+ "agentInstruction": "Do not edit requested paths until a human or deterministic task revision admits them."
23
+ }))
24
+ }
@@ -0,0 +1,71 @@
1
+ use std::path::Path;
2
+
3
+ use naome_core::task_status_report;
4
+ use serde_json::{json, Value};
5
+
6
+ use super::common::{print_json, read_task_state};
7
+
8
+ pub(super) fn timeline(root: &Path, _args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
9
+ let status = task_status_report(root)?;
10
+ let state = read_task_state(root)?;
11
+ let active = state.get("activeTask").unwrap_or(&Value::Null);
12
+ let mut events = admission_events(active);
13
+ events.extend(proof_events(active));
14
+ events.push(json!({ "type": "current_diff", "paths": status.scope.changed_paths }));
15
+ print_json(json!({
16
+ "schema": "naome.task.timeline.v1",
17
+ "state": status.state,
18
+ "taskId": status.task_id,
19
+ "events": events
20
+ }))
21
+ }
22
+
23
+ pub(super) fn loop_snapshot(
24
+ root: &Path,
25
+ _args: &[String],
26
+ ) -> Result<(), Box<dyn std::error::Error>> {
27
+ let status = task_status_report(root)?;
28
+ print_json(json!({
29
+ "schema": "naome.task.loop-snapshot.v1",
30
+ "taskId": status.task_id,
31
+ "state": status.state,
32
+ "status": {
33
+ "agentLoop": status.agent_loop,
34
+ "nextActionV2": status.next_action_v2,
35
+ "findings": status.findings,
36
+ "repairPlan": status.repair_plan,
37
+ "policyHints": status.policy_hints,
38
+ "recoveryGuidance": status.recovery_guidance
39
+ },
40
+ "proof": status.proof
41
+ }))
42
+ }
43
+
44
+ fn admission_events(active: &Value) -> Vec<Value> {
45
+ active
46
+ .get("admission")
47
+ .map(|admission| {
48
+ vec![json!({
49
+ "type": "admission",
50
+ "gitHead": admission.get("gitHead").cloned().unwrap_or(Value::Null),
51
+ "checkedAt": admission.get("checkedAt").cloned().unwrap_or(Value::Null)
52
+ })]
53
+ })
54
+ .unwrap_or_default()
55
+ }
56
+
57
+ fn proof_events(active: &Value) -> Vec<Value> {
58
+ active
59
+ .get("proofBatches")
60
+ .and_then(Value::as_array)
61
+ .into_iter()
62
+ .flatten()
63
+ .map(|batch| {
64
+ json!({
65
+ "type": "proof_batch",
66
+ "id": batch.get("id").cloned().unwrap_or(Value::Null),
67
+ "checkedAt": batch.get("checkedAt").cloned().unwrap_or(Value::Null)
68
+ })
69
+ })
70
+ .collect()
71
+ }
@@ -1,15 +1,83 @@
1
1
  use std::path::Path;
2
2
 
3
- use naome_core::{migrate_task_state_to_ledger, render_task_state_from_ledger};
3
+ mod common;
4
+ mod readiness;
5
+ mod record;
6
+ mod repair;
7
+ mod scope_request;
8
+ mod timeline;
9
+
10
+ use naome_core::{
11
+ format_task_proof_plan, format_task_status, migrate_task_state_to_ledger,
12
+ render_task_state_from_ledger, task_proof_plan, task_status_exit_code, task_status_report,
13
+ task_transition_readiness,
14
+ };
4
15
 
5
16
  pub fn run_task_command(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
6
17
  match args.get(1).map(String::as_str) {
7
18
  Some("render-state") => render_state(root, args),
8
19
  Some("migrate-ledger") => migrate_ledger(root, args),
20
+ Some("status") => task_status(root, args),
21
+ Some("proof-plan") => proof_plan(root, args),
22
+ Some("can-transition") => can_transition(root, args),
23
+ Some("repair") => repair::repair_preview(root, args),
24
+ Some("record-proof") => record::record_proof(root, args),
25
+ Some("request-scope") => scope_request::request_scope(root, args),
26
+ Some("can-commit") => readiness::can_commit(root, args),
27
+ Some("timeline") => timeline::timeline(root, args),
28
+ Some("loop-snapshot") => timeline::loop_snapshot(root, args),
9
29
  _ => Err("unknown task command".into()),
10
30
  }
11
31
  }
12
32
 
33
+ fn task_status(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
34
+ let report = task_status_report(root)?;
35
+ if args.iter().any(|arg| arg == "--json") {
36
+ println!("{}", serde_json::to_string_pretty(&report)?);
37
+ } else {
38
+ print!("{}", format_task_status(&report));
39
+ }
40
+ exit_if_requested(args, task_status_exit_code(&report.findings, &report.proof));
41
+ Ok(())
42
+ }
43
+
44
+ fn proof_plan(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
45
+ let report = task_proof_plan(root)?;
46
+ if args.iter().any(|arg| arg == "--json") {
47
+ println!("{}", serde_json::to_string_pretty(&report)?);
48
+ } else {
49
+ print!("{}", format_task_proof_plan(&report));
50
+ }
51
+ exit_if_requested(args, task_status_exit_code(&report.findings, &report.proof));
52
+ Ok(())
53
+ }
54
+
55
+ fn can_transition(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
56
+ let Some(target) = args
57
+ .windows(2)
58
+ .find(|window| window[0] == "--to")
59
+ .map(|window| window[1].as_str())
60
+ else {
61
+ return Err("naome task can-transition requires --to complete".into());
62
+ };
63
+ let report = task_transition_readiness(root, target)?;
64
+ if args.iter().any(|arg| arg == "--json") {
65
+ println!("{}", serde_json::to_string_pretty(&report)?);
66
+ } else {
67
+ println!(
68
+ "NAOME task transition {target}: {}",
69
+ if report.allowed { "allowed" } else { "blocked" }
70
+ );
71
+ }
72
+ Ok(())
73
+ }
74
+
75
+ fn exit_if_requested(args: &[String], code: i32) {
76
+ if args.iter().any(|arg| arg == "--exit-code") {
77
+ std::process::exit(code);
78
+ }
79
+ }
80
+
13
81
  fn render_state(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
14
82
  let write = args.iter().any(|arg| arg == "--write");
15
83
  let json = args.iter().any(|arg| arg == "--json");
@@ -0,0 +1,58 @@
1
+ use std::process::Command;
2
+
3
+ use serde_json::{json, Value};
4
+
5
+ mod task_cli_support;
6
+
7
+ use task_cli_support::{fixture_root, init_git, task_state, write_fixture_file};
8
+
9
+ #[test]
10
+ fn task_status_json_reports_scope_and_next_action() {
11
+ let root = fixture_root(task_state());
12
+ init_git(&root);
13
+ write_fixture_file(&root, "README.md", "changed\n");
14
+
15
+ let output = Command::new(env!("CARGO_BIN_EXE_naome"))
16
+ .args(["task", "status", "--json"])
17
+ .current_dir(&root)
18
+ .output()
19
+ .unwrap();
20
+
21
+ assert_success(&output);
22
+ let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
23
+ assert_eq!(payload["schema"], "naome.task.status.v1");
24
+ assert_eq!(
25
+ payload["scope"]["inScopeChangedPaths"],
26
+ json!(["README.md"])
27
+ );
28
+ assert_eq!(payload["nextActionV2"]["type"], "rerun_checks");
29
+ assert_eq!(payload["agentLoop"]["state"], "blocked_by_missing_proof");
30
+ }
31
+
32
+ #[test]
33
+ fn task_proof_plan_human_output_lists_recommended_commands() {
34
+ let root = fixture_root(task_state());
35
+ init_git(&root);
36
+ write_fixture_file(&root, "README.md", "changed\n");
37
+
38
+ let output = Command::new(env!("CARGO_BIN_EXE_naome"))
39
+ .args(["task", "proof-plan"])
40
+ .current_dir(root)
41
+ .output()
42
+ .unwrap();
43
+
44
+ assert_success(&output);
45
+ let stdout = String::from_utf8_lossy(&output.stdout);
46
+ assert!(stdout.contains("agent loop: blocked_by_missing_proof"));
47
+ assert!(stdout.contains("recommended commands:"));
48
+ assert!(stdout.contains("diff-check: git diff --check"));
49
+ }
50
+
51
+ fn assert_success(output: &std::process::Output) {
52
+ assert!(
53
+ output.status.success(),
54
+ "{}{}",
55
+ String::from_utf8_lossy(&output.stdout),
56
+ String::from_utf8_lossy(&output.stderr)
57
+ );
58
+ }