@lamentis/naome 1.4.0 → 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 (45) 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 +9 -0
  4. package/crates/naome-cli/src/task_commands/common.rs +32 -0
  5. package/crates/naome-cli/src/task_commands/readiness.rs +40 -0
  6. package/crates/naome-cli/src/task_commands/record.rs +134 -0
  7. package/crates/naome-cli/src/task_commands/repair.rs +30 -0
  8. package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
  9. package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
  10. package/crates/naome-cli/src/task_commands.rs +69 -1
  11. package/crates/naome-cli/tests/task_cli.rs +58 -0
  12. package/crates/naome-cli/tests/task_cli_agent_controls.rs +217 -0
  13. package/crates/naome-cli/tests/task_cli_control.rs +126 -0
  14. package/crates/naome-cli/tests/task_cli_support/mod.rs +150 -0
  15. package/crates/naome-core/Cargo.toml +1 -1
  16. package/crates/naome-core/src/lib.rs +7 -2
  17. package/crates/naome-core/src/task_state/mod.rs +10 -0
  18. package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
  19. package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
  20. package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
  21. package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
  22. package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
  23. package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
  24. package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
  25. package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
  26. package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
  27. package/crates/naome-core/src/task_state/status/control.rs +16 -0
  28. package/crates/naome-core/src/task_state/status/git.rs +133 -0
  29. package/crates/naome-core/src/task_state/status/model.rs +150 -0
  30. package/crates/naome-core/src/task_state/status/proof.rs +167 -0
  31. package/crates/naome-core/src/task_state/status/proof_read.rs +150 -0
  32. package/crates/naome-core/src/task_state/status/report.rs +148 -0
  33. package/crates/naome-core/src/task_state/status/report_context.rs +126 -0
  34. package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
  35. package/crates/naome-core/src/task_state/status/scope.rs +111 -0
  36. package/crates/naome-core/src/task_state/status/transition.rs +73 -0
  37. package/crates/naome-core/src/task_state/status.rs +23 -0
  38. package/crates/naome-core/src/task_state/status_output.rs +103 -0
  39. package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
  40. package/crates/naome-core/tests/task_state_support/states.rs +4 -0
  41. package/crates/naome-core/tests/task_status.rs +301 -0
  42. package/crates/naome-core/tests/task_status_git.rs +141 -0
  43. package/native/darwin-arm64/naome +0 -0
  44. package/native/linux-x64/naome +0 -0
  45. package/package.json +1 -1
@@ -0,0 +1,301 @@
1
+ #![allow(dead_code, unused_imports)]
2
+
3
+ use std::fs;
4
+
5
+ use naome_core::{
6
+ task_proof_plan, task_status_exit_code, task_status_report, task_transition_readiness,
7
+ };
8
+ use serde_json::{json, Value};
9
+
10
+ mod task_state_support;
11
+
12
+ use task_state_support::{
13
+ active_task, complete_task_state, idle_task_state, successful_proof, task_state_with_status,
14
+ TaskFixture,
15
+ };
16
+
17
+ #[test]
18
+ fn clean_idle_status_is_actionable_and_stable_json() {
19
+ let repo = TaskFixture::new(idle_task_state());
20
+ repo.init_git();
21
+
22
+ let status = task_status_report(repo.path()).unwrap();
23
+ let value = serde_json::to_value(&status).unwrap();
24
+
25
+ assert_eq!(value["schema"], "naome.task.status.v1");
26
+ assert_eq!(value["state"], "idle");
27
+ assert_eq!(value["blocked"], false);
28
+ assert_eq!(value["scope"]["changedPaths"], json!([]));
29
+ assert!(value["nextAction"]
30
+ .as_str()
31
+ .is_some_and(|action| action.contains("Create a NAOME task")));
32
+ assert_eq!(value["nextActionV2"]["type"], "create_task");
33
+ assert_eq!(value["agentLoop"]["state"], "ready_for_new_task");
34
+ }
35
+
36
+ #[test]
37
+ fn implementing_status_splits_in_scope_and_out_of_scope_changes() {
38
+ let repo = TaskFixture::new(task_state_with_status(
39
+ "implementing",
40
+ active_task(json!({
41
+ "allowedPaths": ["README.md"],
42
+ "requiredCheckIds": ["diff-check"],
43
+ "proofResults": []
44
+ })),
45
+ ));
46
+ repo.init_git();
47
+ repo.write("README.md", "changed\n");
48
+ repo.write("src/lib.rs", "pub fn outside() {}\n");
49
+
50
+ let status = task_status_report(repo.path()).unwrap();
51
+
52
+ assert_eq!(status.scope.in_scope_changed_paths, vec!["README.md"]);
53
+ assert_eq!(status.scope.out_of_scope_changed_paths, vec!["src/lib.rs"]);
54
+ assert!(status.findings.iter().any(|finding| {
55
+ finding.id == "task.scope.out_of_scope_change"
56
+ && finding.path.as_deref() == Some("src/lib.rs")
57
+ && finding.agent_instruction.contains("Do not commit")
58
+ }));
59
+ assert_eq!(status.next_action_v2.action_type, "repair_scope");
60
+ assert_eq!(status.agent_loop.state, "blocked_by_scope_drift");
61
+ assert!(status
62
+ .repair_plan
63
+ .iter()
64
+ .any(|item| item.id == "remove_out_of_scope_change_src_lib_rs"
65
+ && item.paths == vec!["src/lib.rs"]));
66
+ assert!(status.blocked);
67
+ }
68
+
69
+ #[test]
70
+ fn completed_task_with_new_diff_reports_unbaselined_diff() {
71
+ let repo = TaskFixture::new(complete_task_state(json!({
72
+ "allowedPaths": ["README.md"],
73
+ "proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))],
74
+ })));
75
+ repo.init_git();
76
+ repo.write("README.md", "new diff after completion\n");
77
+
78
+ let status = task_status_report(repo.path()).unwrap();
79
+
80
+ assert!(status.findings.iter().any(|finding| {
81
+ finding.id == "task.state.completed_task_has_diff"
82
+ && finding.path.as_deref() == Some("README.md")
83
+ }));
84
+ let transition = task_transition_readiness(repo.path(), "complete").unwrap();
85
+ assert!(!transition.allowed);
86
+ assert!(transition
87
+ .blocking_findings
88
+ .iter()
89
+ .any(|finding| finding.id == "task.state.completed_task_has_diff"));
90
+ }
91
+
92
+ #[test]
93
+ fn proof_plan_reports_missing_and_stale_required_checks() {
94
+ let repo = TaskFixture::new(task_state_with_status(
95
+ "implementing",
96
+ active_task(json!({
97
+ "allowedPaths": ["README.md", "docs/**"],
98
+ "requiredCheckIds": ["diff-check"],
99
+ "proofPathSets": {
100
+ "old": ["README.md"]
101
+ },
102
+ "proofBatches": [{
103
+ "id": "old-proof",
104
+ "checkedAt": "2026-05-04T12:00:00.000Z",
105
+ "evidencePathSet": "old",
106
+ "proofs": [{ "checkId": "diff-check", "exitCode": 0 }]
107
+ }]
108
+ })),
109
+ ));
110
+ repo.init_git();
111
+ repo.write("README.md", "covered\n");
112
+ repo.write("docs/task.md", "not covered\n");
113
+
114
+ let plan = task_proof_plan(repo.path()).unwrap();
115
+
116
+ assert_eq!(plan.proof.missing_checks, Vec::<String>::new());
117
+ assert_eq!(plan.proof.stale_checks, vec!["diff-check"]);
118
+ assert_eq!(plan.next_action_v2.action_type, "rerun_checks");
119
+ assert_eq!(plan.agent_loop.state, "blocked_by_stale_proof");
120
+ assert_eq!(
121
+ plan.proof_recording.paths,
122
+ vec!["README.md", "docs/task.md"]
123
+ );
124
+ assert_eq!(plan.proof_recording.checks_to_record, vec!["diff-check"]);
125
+ assert!(plan
126
+ .repair_plan
127
+ .iter()
128
+ .any(|item| item.kind == "rerun_check" && item.check_ids == vec!["diff-check"]));
129
+ assert!(plan.recommended_commands.iter().any(|command| {
130
+ command.check_id == "diff-check" && command.command == "git diff --check"
131
+ }));
132
+ assert!(plan.task_feedback.iter().any(|feedback| {
133
+ feedback.problem.contains("Proof evidence") && feedback.files == vec!["docs/task.md"]
134
+ }));
135
+ }
136
+
137
+ #[test]
138
+ fn proof_plan_next_action_reports_missing_proof_without_command_metadata() {
139
+ let repo = TaskFixture::new(task_state_with_status(
140
+ "implementing",
141
+ active_task(json!({
142
+ "allowedPaths": ["README.md"],
143
+ "requiredCheckIds": ["unknown-check"],
144
+ "proofResults": []
145
+ })),
146
+ ));
147
+ repo.write_json(
148
+ ".naome/verification.json",
149
+ json!({
150
+ "schema": "naome.verification.v1",
151
+ "version": 1,
152
+ "status": "ready",
153
+ "checks": []
154
+ }),
155
+ );
156
+ repo.init_git();
157
+ repo.write("README.md", "changed\n");
158
+
159
+ let plan = task_proof_plan(repo.path()).unwrap();
160
+
161
+ assert!(plan.recommended_commands.is_empty());
162
+ assert_eq!(plan.proof.missing_checks, vec!["unknown-check"]);
163
+ assert!(plan.next_action.contains("Recover verification metadata"));
164
+ }
165
+
166
+ #[test]
167
+ fn transition_readiness_blocks_missing_proof_and_allows_fresh_proof() {
168
+ let repo = TaskFixture::new(task_state_with_status(
169
+ "implementing",
170
+ active_task(json!({
171
+ "allowedPaths": ["README.md"],
172
+ "requiredCheckIds": ["diff-check"],
173
+ "proofResults": []
174
+ })),
175
+ ));
176
+ repo.init_git();
177
+ repo.write("README.md", "changed\n");
178
+
179
+ let blocked = task_transition_readiness(repo.path(), "complete").unwrap();
180
+ assert!(!blocked.allowed);
181
+ assert!(blocked
182
+ .blocking_findings
183
+ .iter()
184
+ .any(|finding| finding.id == "task.proof.missing_check"));
185
+
186
+ let mut state: Value = serde_json::from_str(
187
+ &fs::read_to_string(repo.path().join(".naome/task-state.json")).unwrap(),
188
+ )
189
+ .unwrap();
190
+ state["activeTask"]["proofResults"] =
191
+ json!([successful_proof(json!({ "evidence": ["README.md"] }))]);
192
+ fs::write(
193
+ repo.path().join(".naome/task-state.json"),
194
+ format!("{}\n", serde_json::to_string_pretty(&state).unwrap()),
195
+ )
196
+ .unwrap();
197
+
198
+ let allowed = task_transition_readiness(repo.path(), "complete").unwrap();
199
+ assert!(allowed.allowed);
200
+ assert_eq!(allowed.agent_loop.state, "ready_to_commit");
201
+ }
202
+
203
+ #[test]
204
+ fn legacy_proof_results_remain_readable_when_fresh() {
205
+ let repo = TaskFixture::new(task_state_with_status(
206
+ "implementing",
207
+ active_task(json!({
208
+ "allowedPaths": ["README.md"],
209
+ "requiredCheckIds": ["diff-check"],
210
+ "proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
211
+ })),
212
+ ));
213
+ repo.init_git();
214
+ repo.write("README.md", "covered\n");
215
+
216
+ let plan = task_proof_plan(repo.path()).unwrap();
217
+
218
+ assert_eq!(plan.proof.passed_checks, vec!["diff-check"]);
219
+ assert!(plan.proof.missing_checks.is_empty());
220
+ assert!(plan.proof.stale_checks.is_empty());
221
+ }
222
+
223
+ #[test]
224
+ fn task_status_reports_no_active_task_and_control_state_only_changes() {
225
+ let repo = TaskFixture::new(task_state_with_status("idle", Value::Null));
226
+ repo.init_git();
227
+ repo.write("README.md", "unowned\n");
228
+
229
+ let status = task_status_report(repo.path()).unwrap();
230
+ assert!(status
231
+ .findings
232
+ .iter()
233
+ .all(|finding| finding.id != "task.scope.out_of_scope_change"));
234
+ assert!(status
235
+ .findings
236
+ .iter()
237
+ .any(|finding| finding.id == "task.state.no_active_task_with_diff"));
238
+ assert_eq!(status.next_action_v2.action_type, "create_task");
239
+ assert_eq!(task_status_exit_code(&status.findings, &status.proof), 50);
240
+
241
+ fs::remove_file(repo.path().join("README.md")).unwrap();
242
+ repo.write(".naome/task-state.json", "{\n \"status\": \"idle\"\n}\n");
243
+ let status = task_status_report(repo.path()).unwrap();
244
+ assert!(status
245
+ .findings
246
+ .iter()
247
+ .any(|finding| finding.id == "task.scope.control_state_only_change"));
248
+ }
249
+
250
+ #[test]
251
+ fn malformed_task_state_reports_deterministic_status_finding() {
252
+ let repo = TaskFixture::new(task_state_with_status(
253
+ "implementing",
254
+ active_task(json!({})),
255
+ ));
256
+ repo.init_git();
257
+ repo.write(".naome/task-state.json", "{ invalid json\n");
258
+
259
+ let status = task_status_report(repo.path()).unwrap();
260
+
261
+ assert!(status
262
+ .findings
263
+ .iter()
264
+ .any(|finding| finding.id == "task.state.invalid_json"));
265
+ assert_eq!(task_status_exit_code(&status.findings, &status.proof), 40);
266
+ }
267
+
268
+ #[test]
269
+ fn active_state_without_active_task_is_corrupt_and_cannot_complete() {
270
+ let repo = TaskFixture::new(task_state_with_status("implementing", Value::Null));
271
+ repo.init_git();
272
+
273
+ let status = task_status_report(repo.path()).unwrap();
274
+ let transition = task_transition_readiness(repo.path(), "complete").unwrap();
275
+
276
+ assert!(status
277
+ .findings
278
+ .iter()
279
+ .any(|finding| finding.id == "task.state.active_task_missing"));
280
+ assert!(!transition.allowed);
281
+ }
282
+
283
+ #[test]
284
+ fn blocked_task_states_cannot_transition_to_complete() {
285
+ let repo = TaskFixture::new(task_state_with_status(
286
+ "blocked",
287
+ active_task(json!({
288
+ "requiredCheckIds": ["diff-check"],
289
+ "proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
290
+ })),
291
+ ));
292
+ repo.init_git();
293
+
294
+ let transition = task_transition_readiness(repo.path(), "complete").unwrap();
295
+
296
+ assert!(!transition.allowed);
297
+ assert!(transition
298
+ .blocking_findings
299
+ .iter()
300
+ .any(|finding| finding.id == "task.transition.blocked_state"));
301
+ }
@@ -0,0 +1,141 @@
1
+ #![allow(dead_code, unused_imports)]
2
+
3
+ use std::fs;
4
+
5
+ use naome_core::{task_status_exit_code, task_status_report};
6
+ use serde_json::json;
7
+
8
+ mod task_state_support;
9
+
10
+ use task_state_support::{active_task, task_state_with_status, TaskFixture};
11
+
12
+ #[test]
13
+ fn task_status_reports_git_recovery_findings() {
14
+ let repo = TaskFixture::new(task_state_with_status(
15
+ "implementing",
16
+ active_task(json!({
17
+ "allowedPaths": ["**"],
18
+ "requiredCheckIds": [],
19
+ "proofResults": []
20
+ })),
21
+ ));
22
+ repo.init_git();
23
+ repo.set_admission_head("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
24
+
25
+ let status = task_status_report(repo.path()).unwrap();
26
+
27
+ assert_eq!(status.git.admission_head_reachable, false);
28
+ assert!(status
29
+ .findings
30
+ .iter()
31
+ .any(|finding| finding.id == "task.git.admission_head_missing"));
32
+ }
33
+
34
+ #[test]
35
+ fn task_status_reports_unreachable_existing_admission_head() {
36
+ let repo = TaskFixture::new(task_state_with_status(
37
+ "implementing",
38
+ active_task(json!({})),
39
+ ));
40
+ repo.init_git();
41
+ let admission_head = repo.git(["rev-parse", "HEAD"]).trim().to_string();
42
+ repo.git(["checkout", "--orphan", "other"]);
43
+ repo.write("OTHER.md", "other\n");
44
+ repo.git(["add", "."]);
45
+ repo.git(["commit", "-m", "other"]);
46
+ repo.set_admission_head(&admission_head);
47
+
48
+ let status = task_status_report(repo.path()).unwrap();
49
+
50
+ assert!(status.findings.iter().any(|finding| {
51
+ finding.id == "task.git.admission_head_not_reachable"
52
+ && finding.suggested_fix.contains("Rebase")
53
+ }));
54
+ }
55
+
56
+ #[test]
57
+ fn task_status_reports_operation_in_progress_and_conflict_markers() {
58
+ let repo = TaskFixture::new(task_state_with_status(
59
+ "implementing",
60
+ active_task(json!({})),
61
+ ));
62
+ repo.init_git();
63
+ let merge_head = repo.git(["rev-parse", "--git-path", "MERGE_HEAD"]);
64
+ fs::write(repo.path().join(merge_head.trim()), "deadbeef\n").unwrap();
65
+
66
+ let status = task_status_report(repo.path()).unwrap();
67
+ assert!(status
68
+ .findings
69
+ .iter()
70
+ .any(|finding| finding.id == "task.git.operation_in_progress"));
71
+
72
+ fs::write(
73
+ repo.path().join(".naome/task-state.json"),
74
+ "<<<<<<< HEAD\n{}\n=======\n{}\n>>>>>>> branch\n",
75
+ )
76
+ .unwrap();
77
+ let status = task_status_report(repo.path()).unwrap();
78
+ assert!(status
79
+ .findings
80
+ .iter()
81
+ .any(|finding| finding.id == "task.state.conflict_markers"));
82
+ }
83
+
84
+ #[test]
85
+ fn task_status_reports_branch_divergence_when_upstream_split() {
86
+ let repo = TaskFixture::new(task_state_with_status(
87
+ "implementing",
88
+ active_task(json!({
89
+ "allowedPaths": ["**"],
90
+ "requiredCheckIds": [],
91
+ "proofResults": []
92
+ })),
93
+ ));
94
+ repo.init_git();
95
+ let remote = repo.path().join("remote.git");
96
+ let branch = repo.git(["rev-parse", "--abbrev-ref", "HEAD"]);
97
+ let branch = branch.trim();
98
+ repo.git(["init", "--bare", remote.to_str().unwrap()]);
99
+ repo.git(["remote", "add", "origin", remote.to_str().unwrap()]);
100
+ repo.git(["push", "-u", "origin", branch]);
101
+
102
+ repo.write("LOCAL.md", "local\n");
103
+ repo.git(["add", "."]);
104
+ repo.git(["commit", "-m", "local"]);
105
+
106
+ let clone = std::env::temp_dir().join(format!(
107
+ "naome-task-status-remote-work-{}",
108
+ std::process::id()
109
+ ));
110
+ let _ = fs::remove_dir_all(&clone);
111
+ repo.git(["clone", remote.to_str().unwrap(), clone.to_str().unwrap()]);
112
+ git_in(&clone, ["config", "user.email", "naome@example.com"]);
113
+ git_in(&clone, ["config", "user.name", "NAOME Test"]);
114
+ fs::write(clone.join("REMOTE.md"), "remote\n").unwrap();
115
+ git_in(&clone, ["add", "."]);
116
+ git_in(&clone, ["commit", "-m", "remote"]);
117
+ git_in(&clone, ["push"]);
118
+ repo.git(["fetch", "origin"]);
119
+
120
+ let status = task_status_report(repo.path()).unwrap();
121
+ assert_eq!(status.git.ahead, 1);
122
+ assert_eq!(status.git.behind, 1);
123
+ assert!(status
124
+ .findings
125
+ .iter()
126
+ .any(|finding| finding.id == "task.git.branch_diverged"));
127
+ assert_eq!(task_status_exit_code(&status.findings, &status.proof), 0);
128
+ }
129
+
130
+ fn git_in<const N: usize>(root: &std::path::Path, args: [&str; N]) {
131
+ let output = std::process::Command::new("git")
132
+ .args(args)
133
+ .current_dir(root)
134
+ .output()
135
+ .unwrap();
136
+ assert!(
137
+ output.status.success(),
138
+ "{}",
139
+ String::from_utf8_lossy(&output.stderr)
140
+ );
141
+ }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",