@lamentis/naome 1.4.2 → 1.4.4

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.
@@ -0,0 +1,109 @@
1
+ use std::path::Path;
2
+ use std::process::Command;
3
+
4
+ use serde_json::json;
5
+
6
+ use super::common::{agent_session, print_json_with_session};
7
+ use super::path_policy::is_ignored;
8
+ use super::planner;
9
+
10
+ pub(super) fn scope_suggestions(
11
+ root: &Path,
12
+ args: &[String],
13
+ ) -> Result<(), Box<dyn std::error::Error>> {
14
+ let session = agent_session(args)?;
15
+ if !args.iter().any(|arg| arg == "--from-changed") {
16
+ return Err("naome task scope-suggestions requires --from-changed".into());
17
+ }
18
+ let mut suggestions = rename_suggestions(root)
19
+ .into_iter()
20
+ .chain(loader_suggestions(root))
21
+ .filter(|suggestion| {
22
+ !suggestion
23
+ .get("path")
24
+ .and_then(serde_json::Value::as_str)
25
+ .is_some_and(|path| is_ignored(root, path))
26
+ })
27
+ .collect::<Vec<_>>();
28
+ suggestions.sort_by(|left, right| {
29
+ left.get("path")
30
+ .and_then(serde_json::Value::as_str)
31
+ .unwrap_or("")
32
+ .cmp(
33
+ right
34
+ .get("path")
35
+ .and_then(serde_json::Value::as_str)
36
+ .unwrap_or(""),
37
+ )
38
+ });
39
+ print_json_with_session(
40
+ json!({
41
+ "schema": "naome.task.scope-suggestions.v1",
42
+ "suggestions": suggestions,
43
+ "agentInstruction": "Suggestions are read-only hints; task scope mutation still requires explicit approval."
44
+ }),
45
+ session.as_deref(),
46
+ )
47
+ }
48
+
49
+ fn rename_suggestions(root: &Path) -> Vec<serde_json::Value> {
50
+ let Ok(output) = Command::new("git")
51
+ .args(["status", "--porcelain=v1", "-z"])
52
+ .current_dir(root)
53
+ .output()
54
+ else {
55
+ return Vec::new();
56
+ };
57
+ if !output.status.success() {
58
+ return Vec::new();
59
+ }
60
+ let mut suggestions = Vec::new();
61
+ let entries = output.stdout.split(|byte| *byte == 0).collect::<Vec<_>>();
62
+ let mut index = 0;
63
+ while index < entries.len() {
64
+ let entry = entries[index];
65
+ index += 1;
66
+ if entry.len() < 4 {
67
+ continue;
68
+ }
69
+ let status = String::from_utf8_lossy(&entry[..2]);
70
+ if status.contains('R') && index < entries.len() {
71
+ let old = String::from_utf8_lossy(entries[index]).replace('\\', "/");
72
+ index += 1;
73
+ let new = String::from_utf8_lossy(&entry[3..]).replace('\\', "/");
74
+ for path in [old, new] {
75
+ suggestions.push(json!({
76
+ "path": path,
77
+ "reason": "paired path for renamed file",
78
+ "confidence": 0.95,
79
+ "requiresUserApproval": true
80
+ }));
81
+ }
82
+ }
83
+ }
84
+ suggestions
85
+ }
86
+
87
+ fn loader_suggestions(root: &Path) -> Vec<serde_json::Value> {
88
+ planner::changed_paths(root)
89
+ .into_iter()
90
+ .filter(|path| path.ends_with(".test.js") || path.ends_with("_test.rs"))
91
+ .filter_map(|path| {
92
+ let loader = if path.starts_with("scripts/") {
93
+ Some("scripts/naome-installer-support.js".to_string())
94
+ } else if path.contains("/tests/") {
95
+ path.split("/tests/")
96
+ .next()
97
+ .map(|prefix| format!("{prefix}/tests/support/mod.rs"))
98
+ } else {
99
+ None
100
+ }?;
101
+ Some(json!({
102
+ "path": loader,
103
+ "reason": "fixture support helper for changed test",
104
+ "confidence": 0.7,
105
+ "requiresUserApproval": true
106
+ }))
107
+ })
108
+ .collect()
109
+ }
@@ -1,14 +1,21 @@
1
1
  use std::path::Path;
2
2
 
3
+ mod agent_snapshot;
3
4
  mod can_edit;
4
5
  mod check_run;
6
+ mod commit_preflight;
5
7
  mod common;
8
+ mod compact_proof;
6
9
  mod complete;
7
10
  mod loop_control;
11
+ mod path_policy;
12
+ mod planner;
13
+ mod preflight;
8
14
  mod readiness;
9
15
  mod record;
10
16
  mod repair;
11
17
  mod scope_request;
18
+ mod scope_suggestions;
12
19
  mod timeline;
13
20
 
14
21
  use naome_core::{
@@ -23,13 +30,18 @@ pub fn run_task_command(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
23
30
  Some("migrate-ledger") => migrate_ledger(root, args),
24
31
  Some("status") => task_status(root, args),
25
32
  Some("proof-plan") => proof_plan(root, args),
33
+ Some("agent-snapshot") => agent_snapshot::agent_snapshot(root, args),
34
+ Some("preflight") => preflight::preflight(root, args),
26
35
  Some("can-edit") => can_edit::can_edit(root, args),
27
36
  Some("run-check") => check_run::run_check_command(root, args),
28
37
  Some("can-transition") => can_transition(root, args),
29
38
  Some("repair") => repair::repair_preview(root, args),
30
39
  Some("record-proof") => record::record_proof(root, args),
40
+ Some("compact-proof") => compact_proof::compact_proof(root, args),
31
41
  Some("complete") => complete::complete_task(root, args),
32
42
  Some("loop") => loop_control::task_loop(root, args),
43
+ Some("commit-preflight") => commit_preflight::commit_preflight(root, args),
44
+ Some("scope-suggestions") => scope_suggestions::scope_suggestions(root, args),
33
45
  Some("request-scope") => scope_request::request_scope(root, args),
34
46
  Some("can-commit") => readiness::can_commit(root, args),
35
47
  Some("timeline") => timeline::timeline(root, args),
@@ -0,0 +1,332 @@
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, git, init_git, run_json, task_state, task_state_with_active_task,
10
+ write_fixture_file, write_json, write_verification_checks,
11
+ };
12
+
13
+ #[test]
14
+ fn agent_snapshot_reports_missing_proof_and_safe_commands() {
15
+ let root = fixture_root(task_state());
16
+ init_git(&root);
17
+ write_fixture_file(&root, "README.md", "changed\n");
18
+
19
+ let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
20
+
21
+ assert_eq!(snapshot["schema"], "naome.task.agent-snapshot.v1");
22
+ assert_eq!(snapshot["proof"]["missingChecks"], json!(["diff-check"]));
23
+ assert_eq!(snapshot["nextAction"]["type"], "run_checks");
24
+ assert_eq!(snapshot["checks"]["safeToRun"][0]["checkId"], "diff-check");
25
+ }
26
+
27
+ #[test]
28
+ fn agent_snapshot_merges_duplicate_check_reasons_from_proof_and_impact_plans() {
29
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
30
+ "allowedPaths": ["src/lib.rs"],
31
+ "requiredCheckIds": ["repository-quality-check"],
32
+ "proofResults": []
33
+ }))));
34
+ write_verification_checks(
35
+ &root,
36
+ json!([{
37
+ "id": "repository-quality-check",
38
+ "command": "node .naome/bin/naome.js quality check --changed",
39
+ "cwd": ".",
40
+ "purpose": "Validate changed-file quality.",
41
+ "cost": "fast",
42
+ "source": "NAOME",
43
+ "evidence": [".naome/repository-quality.json"],
44
+ "lastVerified": null
45
+ }]),
46
+ );
47
+ init_git(&root);
48
+ write_fixture_file(&root, "src/lib.rs", "pub fn changed() {}\n");
49
+
50
+ let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
51
+ let commands = snapshot["checks"]["recommended"].as_array().unwrap();
52
+ let quality_commands = commands
53
+ .iter()
54
+ .filter(|command| command["checkId"] == "repository-quality-check")
55
+ .collect::<Vec<_>>();
56
+
57
+ assert_eq!(quality_commands.len(), 1);
58
+ assert_eq!(
59
+ quality_commands[0]["reason"],
60
+ "changed_source,missing-proof"
61
+ );
62
+ assert_eq!(
63
+ quality_commands[0]["selectionReason"],
64
+ "changed_source,missing-proof"
65
+ );
66
+ assert_eq!(quality_commands[0]["impactedPaths"], json!(["src/lib.rs"]));
67
+ }
68
+
69
+ #[test]
70
+ fn preflight_reports_path_policy_and_check_plan() {
71
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
72
+ "allowedPaths": ["scripts/*.js"],
73
+ "proofResults": []
74
+ }))));
75
+ fs::write(
76
+ root.join(".naomeignore"),
77
+ ".naome/archive/\n.naome/tasks/\ndist/\n",
78
+ )
79
+ .unwrap();
80
+ init_git(&root);
81
+
82
+ let allowed = run_json(
83
+ &root,
84
+ ["task", "preflight", "--path", "scripts/check.js", "--json"],
85
+ );
86
+ assert_eq!(allowed["paths"][0]["editable"], true);
87
+ assert_eq!(allowed["paths"][0]["risk"], "medium");
88
+
89
+ let ignored = run_json(
90
+ &root,
91
+ ["task", "preflight", "--path", "dist/bundle.js", "--json"],
92
+ );
93
+ assert_eq!(ignored["paths"][0]["editable"], false);
94
+ assert_eq!(ignored["findings"][0]["id"], "task.preflight.ignored_path");
95
+
96
+ let traversal = run_json(
97
+ &root,
98
+ ["task", "preflight", "--path", "..\\outside.js", "--json"],
99
+ );
100
+ assert_eq!(traversal["paths"][0]["editable"], false);
101
+ assert_eq!(traversal["findings"][0]["id"], "task.preflight.unsafe_path");
102
+
103
+ let control = run_json(
104
+ &root,
105
+ ["task", "preflight", "--path", ".naomeignore", "--json"],
106
+ );
107
+ assert_eq!(control["paths"][0]["editable"], false);
108
+ assert_eq!(control["findings"][0]["id"], "task.preflight.control_path");
109
+ }
110
+
111
+ #[test]
112
+ fn preflight_blocks_when_no_target_paths_are_selected() {
113
+ let root = fixture_root(task_state());
114
+ init_git(&root);
115
+
116
+ let preflight = run_json(&root, ["task", "preflight", "--json"]);
117
+
118
+ assert_eq!(preflight["schema"], "naome.task.preflight.v1");
119
+ assert_eq!(
120
+ preflight["findings"][0]["id"],
121
+ "task.preflight.missing_target_paths"
122
+ );
123
+ assert_eq!(preflight["nextAction"]["type"], "blocked");
124
+ assert_eq!(preflight["nextAction"]["safeToExecute"], false);
125
+ }
126
+
127
+ #[test]
128
+ fn preflight_from_changed_allows_clean_changed_set() {
129
+ let root = fixture_root(task_state());
130
+ init_git(&root);
131
+ git(&root, ["add", ".naome/task-state.json"]);
132
+ git(&root, ["commit", "-m", "record admission head"]);
133
+
134
+ let preflight = run_json(&root, ["task", "preflight", "--from-changed", "--json"]);
135
+
136
+ assert_eq!(preflight["schema"], "naome.task.preflight.v1");
137
+ assert!(preflight["findings"].as_array().unwrap().is_empty());
138
+ assert_eq!(preflight["paths"], json!([]));
139
+ assert_eq!(preflight["nextAction"]["type"], "edit");
140
+ }
141
+
142
+ #[test]
143
+ fn commit_preflight_blocks_missing_proof() {
144
+ let root = fixture_root(task_state());
145
+ init_git(&root);
146
+ write_fixture_file(&root, "README.md", "changed\n");
147
+
148
+ let preflight = run_json(&root, ["task", "commit-preflight", "--json"]);
149
+
150
+ assert_eq!(preflight["schema"], "naome.task.commit-preflight.v1");
151
+ assert_eq!(preflight["wouldPass"], false);
152
+ assert_eq!(
153
+ preflight["blockingFindings"][0]["id"],
154
+ "task.proof.missing_check"
155
+ );
156
+ assert_eq!(preflight["nextAction"]["type"], "rerun_checks");
157
+ assert_eq!(preflight["nextAction"]["checkIds"], json!(["diff-check"]));
158
+ }
159
+
160
+ #[test]
161
+ fn agent_snapshot_prefers_commit_ready_over_more_editing() {
162
+ let root = fixture_root(task_state());
163
+ init_git(&root);
164
+ write_fixture_file(&root, "README.md", "changed\n");
165
+
166
+ let checked = run_json(
167
+ &root,
168
+ [
169
+ "task",
170
+ "run-check",
171
+ "--check",
172
+ "diff-check",
173
+ "--record-proof",
174
+ "--json",
175
+ ],
176
+ );
177
+ assert_eq!(checked["recordedProof"], true);
178
+
179
+ let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
180
+
181
+ assert_eq!(snapshot["commit"]["canCommit"], true);
182
+ assert_eq!(snapshot["nextAction"]["type"], "commit_ready");
183
+ assert_eq!(snapshot["nextAction"]["safeToExecute"], false);
184
+ }
185
+
186
+ #[test]
187
+ fn commit_preflight_surfaces_transition_blocker_action_and_exit_code() {
188
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
189
+ "requiredCheckIds": [],
190
+ "humanReview": {
191
+ "required": true,
192
+ "approved": false,
193
+ "reason": "Release owner approval required."
194
+ },
195
+ "proofResults": []
196
+ }))));
197
+ init_git(&root);
198
+ write_fixture_file(&root, "README.md", "changed\n");
199
+
200
+ let preflight = run_json(&root, ["task", "commit-preflight", "--json"]);
201
+
202
+ assert_eq!(preflight["wouldPass"], false);
203
+ assert_eq!(
204
+ preflight["blockingFindings"][0]["id"],
205
+ "task.transition.human_review_required"
206
+ );
207
+ assert_eq!(preflight["nextAction"]["type"], "blocked");
208
+ assert_eq!(preflight["nextAction"]["safeToExecute"], false);
209
+
210
+ let output = Command::new(env!("CARGO_BIN_EXE_naome"))
211
+ .args(["task", "commit-preflight", "--json", "--exit-code"])
212
+ .current_dir(root)
213
+ .output()
214
+ .unwrap();
215
+
216
+ assert_eq!(output.status.code(), Some(1));
217
+ }
218
+
219
+ #[test]
220
+ fn compact_proof_dry_run_and_write_remove_verbose_fields() {
221
+ let root = fixture_root(task_state());
222
+ let path = root.join(".naome/task-state.json");
223
+ let mut state: Value = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
224
+ state["activeTask"]["proofPathSets"] = json!({ "current": ["README.md"] });
225
+ state["activeTask"]["proofBatches"] = json!([{
226
+ "id": "batch",
227
+ "checkedAt": "2026-05-14T00:00:00.000Z",
228
+ "evidencePathSet": "current",
229
+ "proofs": [{
230
+ "checkId": "diff-check",
231
+ "command": "git diff --check",
232
+ "cwd": ".",
233
+ "exitCode": 0,
234
+ "evidenceFingerprint": "fnv64:test",
235
+ "stdoutSummary": "large",
236
+ "stderrSummary": "large",
237
+ "durationMs": 42
238
+ }]
239
+ }]);
240
+ write_json(&root, ".naome/task-state.json", &state);
241
+
242
+ let dry = run_json(&root, ["task", "compact-proof", "--dry-run", "--json"]);
243
+ assert_eq!(dry["dryRun"], true);
244
+ assert_eq!(dry["removedVerboseFields"], 3);
245
+ let unchanged = fs::read_to_string(&path).unwrap();
246
+ assert!(unchanged.contains("stdoutSummary"));
247
+
248
+ let compacted = run_json(&root, ["task", "compact-proof", "--json"]);
249
+ assert_eq!(compacted["compacted"], true);
250
+ let changed = fs::read_to_string(&path).unwrap();
251
+ assert!(!changed.contains("stdoutSummary"));
252
+ assert!(changed.contains("evidenceFingerprint"));
253
+ }
254
+
255
+ #[test]
256
+ fn record_proof_rejects_task_and_path_mismatched_receipts() {
257
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
258
+ "allowedPaths": ["README.md", "docs/**"],
259
+ "proofResults": []
260
+ }))));
261
+ init_git(&root);
262
+ write_fixture_file(&root, "README.md", "changed\n");
263
+
264
+ let checked = run_json(
265
+ &root,
266
+ ["task", "run-check", "--check", "diff-check", "--json"],
267
+ );
268
+ assert_eq!(checked["exitCode"], 0);
269
+
270
+ let mut state: Value =
271
+ serde_json::from_str(&fs::read_to_string(root.join(".naome/task-state.json")).unwrap())
272
+ .unwrap();
273
+ state["activeTask"]["id"] = json!("different-task");
274
+ write_json(&root, ".naome/task-state.json", &state);
275
+ let task_mismatch = run_json(
276
+ &root,
277
+ ["task", "record-proof", "--from-proof-plan", "--json"],
278
+ );
279
+ assert_eq!(
280
+ task_mismatch["findings"][0]["id"],
281
+ "task.proof.receipt_task_mismatch"
282
+ );
283
+
284
+ state["activeTask"]["id"] = json!("cli-task");
285
+ write_json(&root, ".naome/task-state.json", &state);
286
+ write_fixture_file(&root, "docs/new.md", "new\n");
287
+ let path_mismatch = run_json(
288
+ &root,
289
+ ["task", "record-proof", "--from-proof-plan", "--json"],
290
+ );
291
+ assert_eq!(
292
+ path_mismatch["findings"][0]["id"],
293
+ "task.proof.receipt_path_mismatch"
294
+ );
295
+ }
296
+
297
+ #[test]
298
+ fn scope_suggestions_reports_test_support_helpers() {
299
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
300
+ "allowedPaths": ["scripts/**"],
301
+ "proofResults": []
302
+ }))));
303
+ init_git(&root);
304
+ write_fixture_file(&root, "scripts/new-flow.test.js", "test('x', () => {});\n");
305
+
306
+ let suggestions = run_json(
307
+ &root,
308
+ ["task", "scope-suggestions", "--from-changed", "--json"],
309
+ );
310
+
311
+ assert_eq!(suggestions["schema"], "naome.task.scope-suggestions.v1");
312
+ assert!(suggestions["suggestions"]
313
+ .as_array()
314
+ .unwrap()
315
+ .iter()
316
+ .any(|suggestion| suggestion["path"] == "scripts/naome-installer-support.js"));
317
+ }
318
+
319
+ #[test]
320
+ fn commit_preflight_exit_code_reports_blocker() {
321
+ let root = fixture_root(task_state());
322
+ init_git(&root);
323
+ write_fixture_file(&root, "README.md", "changed\n");
324
+
325
+ let output = Command::new(env!("CARGO_BIN_EXE_naome"))
326
+ .args(["task", "commit-preflight", "--json", "--exit-code"])
327
+ .current_dir(root)
328
+ .output()
329
+ .unwrap();
330
+
331
+ assert_eq!(output.status.code(), Some(10));
332
+ }
@@ -96,7 +96,7 @@ fn record_proof_rejects_receipts_from_older_same_path_content() {
96
96
  assert_eq!(recorded["recorded"], false);
97
97
  assert_eq!(
98
98
  recorded["findings"][0]["id"],
99
- "task.proof.no_recent_success"
99
+ "task.proof.stale_receipt_content"
100
100
  );
101
101
  }
102
102
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.4.2"
3
+ version = "1.4.4"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",