@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
@@ -0,0 +1,217 @@
1
+ use std::fs;
2
+ use std::process::Command;
3
+
4
+ use serde_json::{json, Value};
5
+
6
+ mod task_cli_support;
7
+
8
+ use task_cli_support::{fixture_root, init_git, task_state, write_fixture_file};
9
+
10
+ #[test]
11
+ fn status_json_exposes_policy_hints_and_recovery_guidance() {
12
+ let root = fixture_root(task_state());
13
+ init_git(&root);
14
+ write_fixture_file(&root, "README.md", "changed\n");
15
+
16
+ let status = run_json(&root, ["task", "status", "--json"]);
17
+
18
+ assert_eq!(status["policyHints"]["mayEdit"], json!(["README.md"]));
19
+ assert_eq!(status["taskMode"]["kind"], "standard");
20
+ assert_eq!(
21
+ status["policyHints"]["mustRunAfterEdit"],
22
+ json!(["diff-check"])
23
+ );
24
+ assert!(status["policyHints"]["forbiddenActions"]
25
+ .as_array()
26
+ .unwrap()
27
+ .iter()
28
+ .any(|item| item == "Do not bypass the NAOME commit gate."));
29
+ assert!(status["recoveryGuidance"].as_array().unwrap().is_empty());
30
+ }
31
+
32
+ #[test]
33
+ fn repair_dry_run_previews_selected_safe_plan() {
34
+ let root = fixture_root(task_state());
35
+ init_git(&root);
36
+ write_fixture_file(&root, "src/lib.rs", "outside\n");
37
+ write_fixture_file(&root, "docs/outside.md", "outside\n");
38
+
39
+ let status = run_json(&root, ["task", "status", "--json"]);
40
+ let repair_ids = status["repairPlan"]
41
+ .as_array()
42
+ .unwrap()
43
+ .iter()
44
+ .filter_map(|item| item["id"].as_str())
45
+ .collect::<Vec<_>>();
46
+ assert!(repair_ids.contains(&"remove_out_of_scope_change_src_lib_rs"));
47
+ assert!(repair_ids.contains(&"remove_out_of_scope_change_docs_outside_md"));
48
+
49
+ let preview = run_json(
50
+ &root,
51
+ [
52
+ "task",
53
+ "repair",
54
+ "--plan",
55
+ "remove_out_of_scope_change_src_lib_rs",
56
+ "--dry-run",
57
+ "--json",
58
+ ],
59
+ );
60
+
61
+ assert_eq!(preview["schema"], "naome.task.repair-preview.v1");
62
+ assert_eq!(preview["found"], true);
63
+ assert_eq!(preview["wouldExecute"], false);
64
+ assert_eq!(preview["plan"]["kind"], "git_revert_path");
65
+ assert_eq!(preview["plan"]["paths"], json!(["src/lib.rs"]));
66
+ }
67
+
68
+ #[test]
69
+ fn record_proof_from_plan_writes_compact_batch() {
70
+ let root = fixture_root(task_cli_support::task_state_with_active_task(
71
+ task_cli_support::active_task(json!({
72
+ "proofPathSets": {
73
+ "current-task-diff": ["OLD.md"]
74
+ },
75
+ "proofBatches": [{
76
+ "id": "cli-task-proof",
77
+ "checkedAt": "2026-05-04T12:00:00.000Z",
78
+ "evidencePathSet": "current-task-diff",
79
+ "proofs": [{ "checkId": "diff-check", "exitCode": 0 }]
80
+ }]
81
+ })),
82
+ ));
83
+ init_git(&root);
84
+ write_fixture_file(&root, "README.md", "changed\n");
85
+
86
+ let recorded = run_json(
87
+ &root,
88
+ ["task", "record-proof", "--from-proof-plan", "--json"],
89
+ );
90
+
91
+ assert_eq!(recorded["schema"], "naome.task.record-proof.v1");
92
+ assert_eq!(recorded["recorded"], true);
93
+ assert_eq!(recorded["checksRecorded"], json!(["diff-check"]));
94
+ assert_eq!(recorded["pathSetId"], "current-task-diff-2");
95
+ assert_eq!(recorded["proofBatchId"], "cli-task-proof-2");
96
+
97
+ let task_state: Value =
98
+ serde_json::from_str(&fs::read_to_string(root.join(".naome/task-state.json")).unwrap())
99
+ .unwrap();
100
+ assert_eq!(
101
+ task_state["activeTask"]["proofPathSets"]["current-task-diff"],
102
+ json!(["OLD.md"])
103
+ );
104
+ assert_eq!(
105
+ task_state["activeTask"]["proofPathSets"]["current-task-diff-2"],
106
+ json!(["README.md"])
107
+ );
108
+ assert_eq!(
109
+ task_state["activeTask"]["proofBatches"][1]["id"],
110
+ "cli-task-proof-2"
111
+ );
112
+ assert_eq!(
113
+ task_state["activeTask"]["proofBatches"][1]["proofs"][0]["checkId"],
114
+ "diff-check"
115
+ );
116
+ }
117
+
118
+ #[test]
119
+ fn rerun_check_repair_preserves_configured_cwd() {
120
+ let root = fixture_root(task_state());
121
+ init_git(&root);
122
+ let mut verification: Value =
123
+ serde_json::from_str(&fs::read_to_string(root.join(".naome/verification.json")).unwrap())
124
+ .unwrap();
125
+ verification["checks"][0]["cwd"] = json!("checks");
126
+ task_cli_support::write_json(&root, ".naome/verification.json", &verification);
127
+ write_fixture_file(&root, "README.md", "changed\n");
128
+
129
+ let plan = run_json(&root, ["task", "proof-plan", "--json"]);
130
+ let rerun = plan["repairPlan"]
131
+ .as_array()
132
+ .unwrap()
133
+ .iter()
134
+ .find(|item| item["id"] == "rerun_diff-check")
135
+ .unwrap();
136
+
137
+ assert_eq!(rerun["cwd"], "checks");
138
+ assert_eq!(rerun["commands"], json!(["git diff --check"]));
139
+ }
140
+
141
+ #[test]
142
+ fn agent_control_commands_emit_stable_json_contracts() {
143
+ let root = fixture_root(task_state());
144
+ init_git(&root);
145
+ write_fixture_file(&root, "README.md", "changed\n");
146
+
147
+ let scope = run_json(
148
+ &root,
149
+ [
150
+ "task",
151
+ "request-scope",
152
+ "--path",
153
+ "src/lib.rs",
154
+ "--reason",
155
+ "Need to update implementation.",
156
+ "--json",
157
+ ],
158
+ );
159
+ assert_eq!(scope["schema"], "naome.task.scope-request.v1");
160
+ assert_eq!(scope["wouldMutate"], false);
161
+ assert_eq!(scope["requestedPaths"], json!(["src/lib.rs"]));
162
+
163
+ let can_commit = run_json(&root, ["task", "can-commit", "--json"]);
164
+ assert_eq!(can_commit["schema"], "naome.task.commit-readiness.v1");
165
+ assert_eq!(can_commit["allowed"], false);
166
+ assert!(can_commit["requiredBeforeCommit"]
167
+ .as_array()
168
+ .unwrap()
169
+ .iter()
170
+ .any(|item| item.as_str().unwrap().contains("required check")));
171
+
172
+ let timeline = run_json(&root, ["task", "timeline", "--json"]);
173
+ assert_eq!(timeline["schema"], "naome.task.timeline.v1");
174
+ assert!(timeline["events"].as_array().unwrap().len() >= 2);
175
+
176
+ let snapshot = run_json(&root, ["task", "loop-snapshot", "--json"]);
177
+ assert_eq!(snapshot["schema"], "naome.task.loop-snapshot.v1");
178
+ assert_eq!(
179
+ snapshot["status"]["agentLoop"]["state"],
180
+ "blocked_by_missing_proof"
181
+ );
182
+ }
183
+
184
+ #[test]
185
+ fn review_fix_mode_is_structured_not_inferred_from_prompt_text() {
186
+ let root = fixture_root(task_cli_support::task_state_with_active_task(
187
+ task_cli_support::active_task(json!({
188
+ "kind": "review_fix",
189
+ "declaredChangeTypes": ["review-fix"],
190
+ "proofResults": []
191
+ })),
192
+ ));
193
+ init_git(&root);
194
+
195
+ let status = run_json(&root, ["task", "status", "--json"]);
196
+
197
+ assert_eq!(status["taskMode"]["kind"], "review_fix");
198
+ assert_eq!(status["taskMode"]["reviewFix"], true);
199
+ assert!(status["taskMode"]["scopePolicy"]
200
+ .as_str()
201
+ .unwrap()
202
+ .contains("explicit allowedPaths"));
203
+ }
204
+
205
+ fn run_json<const N: usize>(root: &std::path::Path, args: [&str; N]) -> Value {
206
+ let output = Command::new(env!("CARGO_BIN_EXE_naome"))
207
+ .args(args)
208
+ .current_dir(root)
209
+ .output()
210
+ .unwrap();
211
+ assert!(
212
+ output.status.success(),
213
+ "{}",
214
+ String::from_utf8_lossy(&output.stderr)
215
+ );
216
+ serde_json::from_slice(&output.stdout).unwrap()
217
+ }
@@ -0,0 +1,126 @@
1
+ use std::fs;
2
+ use std::process::Command;
3
+
4
+ use serde_json::{json, Value};
5
+
6
+ mod task_cli_support;
7
+
8
+ use task_cli_support::{
9
+ active_task, fixture_root, init_git, successful_proof, task_state, task_state_with_active_task,
10
+ write_fixture_file, write_json,
11
+ };
12
+
13
+ #[test]
14
+ fn exit_code_reports_missing_stale_scope_git_and_conflict_states() {
15
+ assert_exit_code(
16
+ task_state(),
17
+ |root| write_fixture_file(root, "README.md", "changed\n"),
18
+ 10,
19
+ );
20
+
21
+ assert_exit_code(
22
+ stale_task_state(),
23
+ |root| {
24
+ write_fixture_file(root, "README.md", "covered\n");
25
+ write_fixture_file(root, "docs/task.md", "stale\n");
26
+ },
27
+ 11,
28
+ );
29
+
30
+ assert_exit_code(
31
+ task_state(),
32
+ |root| write_fixture_file(root, "src/lib.rs", "x\n"),
33
+ 20,
34
+ );
35
+
36
+ assert_exit_code(
37
+ task_state(),
38
+ |root| {
39
+ set_admission_head(root, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
40
+ },
41
+ 30,
42
+ );
43
+
44
+ assert_exit_code(
45
+ task_state(),
46
+ |root| {
47
+ fs::write(
48
+ root.join(".naome/task-state.json"),
49
+ "<<<<<<< HEAD\n{}\n=======\n{}\n>>>>>>> branch\n",
50
+ )
51
+ .unwrap();
52
+ },
53
+ 40,
54
+ );
55
+ }
56
+
57
+ #[test]
58
+ fn can_transition_blocks_missing_proof_and_allows_fresh_proof() {
59
+ let blocked = run_transition(task_state(), |root| {
60
+ write_fixture_file(root, "README.md", "changed\n");
61
+ });
62
+ assert_eq!(blocked["schema"], "naome.task.transition-readiness.v1");
63
+ assert_eq!(blocked["allowed"], false);
64
+ assert!(blocked["blockingFindings"]
65
+ .as_array()
66
+ .unwrap()
67
+ .iter()
68
+ .any(|finding| { finding["id"] == "task.proof.missing_check" }));
69
+
70
+ let allowed = run_transition(fresh_task_state(), |root| {
71
+ write_fixture_file(root, "README.md", "changed\n");
72
+ });
73
+ assert_eq!(allowed["allowed"], true);
74
+ assert_eq!(allowed["agentLoop"]["state"], "ready_to_commit");
75
+ }
76
+
77
+ fn assert_exit_code(state: Value, mutate: impl FnOnce(&std::path::Path), expected: i32) {
78
+ let root = fixture_root(state);
79
+ init_git(&root);
80
+ mutate(&root);
81
+ let output = Command::new(env!("CARGO_BIN_EXE_naome"))
82
+ .args(["task", "status", "--json", "--exit-code"])
83
+ .current_dir(root)
84
+ .output()
85
+ .unwrap();
86
+ assert_eq!(output.status.code(), Some(expected));
87
+ }
88
+
89
+ fn run_transition(state: Value, mutate: impl FnOnce(&std::path::Path)) -> Value {
90
+ let root = fixture_root(state);
91
+ init_git(&root);
92
+ mutate(&root);
93
+ let output = Command::new(env!("CARGO_BIN_EXE_naome"))
94
+ .args(["task", "can-transition", "--to", "complete", "--json"])
95
+ .current_dir(root)
96
+ .output()
97
+ .unwrap();
98
+ assert!(output.status.success());
99
+ serde_json::from_slice(&output.stdout).unwrap()
100
+ }
101
+
102
+ fn stale_task_state() -> Value {
103
+ task_state_with_active_task(active_task(json!({
104
+ "allowedPaths": ["README.md", "docs/**"],
105
+ "proofPathSets": { "old": ["README.md"] },
106
+ "proofBatches": [{
107
+ "id": "old-proof",
108
+ "checkedAt": "2026-05-04T12:00:00.000Z",
109
+ "evidencePathSet": "old",
110
+ "proofs": [{ "checkId": "diff-check", "exitCode": 0 }]
111
+ }]
112
+ })))
113
+ }
114
+
115
+ fn fresh_task_state() -> Value {
116
+ task_state_with_active_task(active_task(json!({
117
+ "proofResults": [successful_proof(json!(["README.md"]))]
118
+ })))
119
+ }
120
+
121
+ fn set_admission_head(root: &std::path::Path, head: &str) {
122
+ let path = root.join(".naome/task-state.json");
123
+ let mut state: Value = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
124
+ state["activeTask"]["admission"]["gitHead"] = json!(head);
125
+ write_json(root, ".naome/task-state.json", &state);
126
+ }
@@ -0,0 +1,150 @@
1
+ #![allow(dead_code)]
2
+
3
+ use std::fs;
4
+ use std::process::Command;
5
+ use std::sync::atomic::{AtomicU64, Ordering};
6
+ use std::time::{SystemTime, UNIX_EPOCH};
7
+
8
+ use serde_json::{json, Value};
9
+
10
+ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
11
+
12
+ pub fn task_state() -> Value {
13
+ task_state_with_active_task(active_task(json!({ "proofResults": [] })))
14
+ }
15
+
16
+ pub fn task_state_with_active_task(active_task: Value) -> Value {
17
+ let mut state: Value = serde_json::from_str(include_str!(
18
+ "../../../../templates/naome-root/.naome/task-state.json"
19
+ ))
20
+ .unwrap();
21
+ state["status"] = json!("implementing");
22
+ state["activeTask"] = active_task;
23
+ state["updatedAt"] = json!("2026-05-04T12:00:00.000Z");
24
+ state
25
+ }
26
+
27
+ pub fn active_task(overrides: Value) -> Value {
28
+ let mut task = json!({
29
+ "id": "cli-task",
30
+ "request": "Exercise task CLI.",
31
+ "userPrompt": {
32
+ "receivedAt": "2026-05-04T12:00:00.000Z",
33
+ "text": "Exercise task CLI."
34
+ },
35
+ "admission": {
36
+ "command": "node .naome/bin/check-task-state.js --admission",
37
+ "cwd": ".",
38
+ "exitCode": 0,
39
+ "checkedAt": "2026-05-04T12:00:00.000Z",
40
+ "gitHead": "pending-test-head",
41
+ "changedPaths": []
42
+ },
43
+ "allowedPaths": ["README.md"],
44
+ "declaredChangeTypes": ["docs"],
45
+ "requiredCheckIds": ["diff-check"],
46
+ "humanReview": {
47
+ "required": false,
48
+ "approved": false,
49
+ "reason": null
50
+ }
51
+ });
52
+ for (key, value) in overrides.as_object().unwrap() {
53
+ task.as_object_mut()
54
+ .unwrap()
55
+ .insert(key.clone(), value.clone());
56
+ }
57
+ task
58
+ }
59
+
60
+ pub fn successful_proof(evidence: Value) -> Value {
61
+ json!({
62
+ "checkId": "diff-check",
63
+ "command": "git diff --check",
64
+ "cwd": ".",
65
+ "exitCode": 0,
66
+ "checkedAt": "2026-05-04T12:00:00.000Z",
67
+ "evidence": evidence
68
+ })
69
+ }
70
+
71
+ pub fn fixture_root(task_state: Value) -> std::path::PathBuf {
72
+ let nonce = SystemTime::now()
73
+ .duration_since(UNIX_EPOCH)
74
+ .unwrap()
75
+ .as_nanos();
76
+ let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
77
+ let root = std::env::temp_dir().join(format!(
78
+ "naome-task-cli-fixture-{}-{nonce}-{counter}",
79
+ std::process::id()
80
+ ));
81
+ fs::create_dir_all(root.join(".naome")).unwrap();
82
+ fs::write(
83
+ root.join(".naomeignore"),
84
+ ".naome/archive/\n.naome/tasks/\n",
85
+ )
86
+ .unwrap();
87
+ write_json(&root, ".naome/task-state.json", &task_state);
88
+ write_json(&root, ".naome/verification.json", &verification());
89
+ write_fixture_file(&root, "README.md", "initial\n");
90
+ root
91
+ }
92
+
93
+ pub fn init_git(root: &std::path::Path) {
94
+ git(root, ["init"]);
95
+ git(root, ["config", "user.email", "naome@example.com"]);
96
+ git(root, ["config", "user.name", "NAOME Test"]);
97
+ git(root, ["add", "."]);
98
+ git(root, ["commit", "-m", "baseline"]);
99
+ let head = git(root, ["rev-parse", "HEAD"]).trim().to_string();
100
+ let path = root.join(".naome/task-state.json");
101
+ let mut state: Value = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
102
+ state["activeTask"]["admission"]["gitHead"] = json!(head);
103
+ write_json(root, ".naome/task-state.json", &state);
104
+ }
105
+
106
+ pub fn git<const N: usize>(root: &std::path::Path, args: [&str; N]) -> String {
107
+ let output = Command::new("git")
108
+ .args(args)
109
+ .current_dir(root)
110
+ .output()
111
+ .unwrap();
112
+ assert!(
113
+ output.status.success(),
114
+ "{}",
115
+ String::from_utf8_lossy(&output.stderr)
116
+ );
117
+ String::from_utf8_lossy(&output.stdout).to_string()
118
+ }
119
+
120
+ pub fn write_fixture_file(root: &std::path::Path, path: &str, content: &str) {
121
+ let target = root.join(path);
122
+ fs::create_dir_all(target.parent().unwrap()).unwrap();
123
+ fs::write(target, content).unwrap();
124
+ }
125
+
126
+ pub fn write_json(root: &std::path::Path, path: &str, value: &Value) {
127
+ write_fixture_file(
128
+ root,
129
+ path,
130
+ &format!("{}\n", serde_json::to_string_pretty(value).unwrap()),
131
+ );
132
+ }
133
+
134
+ fn verification() -> Value {
135
+ json!({
136
+ "schema": "naome.verification.v1",
137
+ "version": 1,
138
+ "status": "ready",
139
+ "checks": [{
140
+ "id": "diff-check",
141
+ "command": "git diff --check",
142
+ "cwd": ".",
143
+ "purpose": "Detect whitespace and patch formatting issues.",
144
+ "cost": "fast",
145
+ "source": "git",
146
+ "evidence": ["README.md"],
147
+ "lastVerified": null
148
+ }]
149
+ })
150
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
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