@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
@@ -0,0 +1,101 @@
1
+ use std::collections::BTreeSet;
2
+
3
+ use naome_core::{TaskProofStatus, TaskStatusReportV1};
4
+ use serde_json::{json, Value};
5
+
6
+ pub(super) fn action(
7
+ action_type: &str,
8
+ action_id: &str,
9
+ reason_codes: &[impl AsRef<str>],
10
+ commands: &[Value],
11
+ paths: Vec<String>,
12
+ check_ids: &[String],
13
+ safe_to_execute: bool,
14
+ requires_user_approval: bool,
15
+ primary_finding_id: Option<&str>,
16
+ ) -> Value {
17
+ json!({
18
+ "type": action_type,
19
+ "actionId": action_id,
20
+ "reasonCodes": reason_codes.iter().map(|code| code.as_ref()).collect::<Vec<_>>(),
21
+ "primaryFindingId": primary_finding_id,
22
+ "commands": commands,
23
+ "paths": paths,
24
+ "checkIds": check_ids,
25
+ "safeToExecute": safe_to_execute,
26
+ "requiresUserApproval": requires_user_approval
27
+ })
28
+ }
29
+
30
+ pub(super) fn primary_finding_id(status: &TaskStatusReportV1, prefix: &str) -> Option<String> {
31
+ status
32
+ .findings
33
+ .iter()
34
+ .find(|finding| finding.id.starts_with(prefix))
35
+ .map(|finding| finding.id.clone())
36
+ }
37
+
38
+ pub(super) fn reason_codes(commands: &[Value], proof: &TaskProofStatus) -> Vec<String> {
39
+ let mut codes = BTreeSet::new();
40
+ for command in commands {
41
+ collect_csv(command.get("reason"), &mut codes);
42
+ collect_csv(command.get("selectionReason"), &mut codes);
43
+ }
44
+ if !proof.missing_checks.is_empty() {
45
+ codes.insert("missing-proof".to_string());
46
+ }
47
+ if !proof.stale_checks.is_empty() {
48
+ codes.insert("stale-proof".to_string());
49
+ }
50
+ codes.into_iter().collect()
51
+ }
52
+
53
+ pub(super) fn impacted_paths(commands: &[Value], fallback: &[String]) -> Vec<String> {
54
+ let mut paths = BTreeSet::new();
55
+ for command in commands {
56
+ collect_string_array(command.get("impactedPaths"), &mut paths);
57
+ }
58
+ if paths.is_empty() {
59
+ paths.extend(fallback.iter().cloned());
60
+ }
61
+ paths.into_iter().collect()
62
+ }
63
+
64
+ pub(super) fn check_ids(commands: &[Value], proof: &TaskProofStatus) -> Vec<String> {
65
+ let mut ids = BTreeSet::new();
66
+ ids.extend(commands.iter().filter_map(check_id));
67
+ if ids.is_empty() {
68
+ ids.extend(proof.missing_checks.iter().cloned());
69
+ ids.extend(proof.stale_checks.iter().cloned());
70
+ }
71
+ ids.into_iter().collect()
72
+ }
73
+
74
+ fn check_id(command: &Value) -> Option<String> {
75
+ command
76
+ .get("checkId")
77
+ .and_then(Value::as_str)
78
+ .map(ToString::to_string)
79
+ }
80
+
81
+ pub(super) fn collect_csv(value: Option<&Value>, values: &mut BTreeSet<String>) {
82
+ if let Some(raw) = value.and_then(Value::as_str) {
83
+ values.extend(
84
+ raw.split(',')
85
+ .map(str::trim)
86
+ .filter(|part| !part.is_empty())
87
+ .map(ToString::to_string),
88
+ );
89
+ }
90
+ }
91
+
92
+ pub(super) fn collect_string_array(value: Option<&Value>, values: &mut BTreeSet<String>) {
93
+ values.extend(
94
+ value
95
+ .and_then(Value::as_array)
96
+ .into_iter()
97
+ .flatten()
98
+ .filter_map(Value::as_str)
99
+ .map(ToString::to_string),
100
+ );
101
+ }
@@ -16,6 +16,8 @@ mod record;
16
16
  mod repair;
17
17
  mod scope_request;
18
18
  mod scope_suggestions;
19
+ mod single_pass_action;
20
+ mod single_pass_action_fields;
19
21
  mod timeline;
20
22
 
21
23
  use naome_core::{
@@ -21,6 +21,18 @@ fn agent_snapshot_reports_missing_proof_and_safe_commands() {
21
21
  assert_eq!(snapshot["schema"], "naome.task.agent-snapshot.v1");
22
22
  assert_eq!(snapshot["proof"]["missingChecks"], json!(["diff-check"]));
23
23
  assert_eq!(snapshot["nextAction"]["type"], "run_checks");
24
+ assert_eq!(
25
+ snapshot["nextAction"]["actionId"],
26
+ "proof.run_required_checks"
27
+ );
28
+ assert_eq!(
29
+ snapshot["nextAction"]["reasonCodes"],
30
+ json!(["changed_docs", "missing-proof"])
31
+ );
32
+ assert_eq!(
33
+ snapshot["nextAction"]["commands"][0]["command"],
34
+ "git diff --check"
35
+ );
24
36
  assert_eq!(snapshot["checks"]["safeToRun"][0]["checkId"], "diff-check");
25
37
  }
26
38
 
@@ -85,6 +97,12 @@ fn preflight_reports_path_policy_and_check_plan() {
85
97
  );
86
98
  assert_eq!(allowed["paths"][0]["editable"], true);
87
99
  assert_eq!(allowed["paths"][0]["risk"], "medium");
100
+ assert_eq!(allowed["nextAction"]["actionId"], "preflight.edit_allowed");
101
+ assert_eq!(allowed["nextAction"]["paths"], json!(["scripts/check.js"]));
102
+ assert_eq!(
103
+ allowed["nextAction"]["reasonCodes"],
104
+ json!(["preflight-clean"])
105
+ );
88
106
 
89
107
  let ignored = run_json(
90
108
  &root,
@@ -92,6 +110,12 @@ fn preflight_reports_path_policy_and_check_plan() {
92
110
  );
93
111
  assert_eq!(ignored["paths"][0]["editable"], false);
94
112
  assert_eq!(ignored["findings"][0]["id"], "task.preflight.ignored_path");
113
+ assert_eq!(ignored["nextAction"]["actionId"], "preflight.blocked");
114
+ assert_eq!(ignored["nextAction"]["paths"], json!(["dist/bundle.js"]));
115
+ assert_eq!(
116
+ ignored["nextAction"]["reasonCodes"],
117
+ json!(["preflight-blocked"])
118
+ );
95
119
 
96
120
  let traversal = run_json(
97
121
  &root,
@@ -121,6 +145,7 @@ fn preflight_blocks_when_no_target_paths_are_selected() {
121
145
  "task.preflight.missing_target_paths"
122
146
  );
123
147
  assert_eq!(preflight["nextAction"]["type"], "blocked");
148
+ assert_eq!(preflight["nextAction"]["actionId"], "preflight.blocked");
124
149
  assert_eq!(preflight["nextAction"]["safeToExecute"], false);
125
150
  }
126
151
 
@@ -153,7 +178,11 @@ fn commit_preflight_blocks_missing_proof() {
153
178
  preflight["blockingFindings"][0]["id"],
154
179
  "task.proof.missing_check"
155
180
  );
156
- assert_eq!(preflight["nextAction"]["type"], "rerun_checks");
181
+ assert_eq!(preflight["nextAction"]["type"], "run_checks");
182
+ assert_eq!(
183
+ preflight["nextAction"]["actionId"],
184
+ "proof.run_required_checks"
185
+ );
157
186
  assert_eq!(preflight["nextAction"]["checkIds"], json!(["diff-check"]));
158
187
  }
159
188
 
@@ -180,9 +209,120 @@ fn agent_snapshot_prefers_commit_ready_over_more_editing() {
180
209
 
181
210
  assert_eq!(snapshot["commit"]["canCommit"], true);
182
211
  assert_eq!(snapshot["nextAction"]["type"], "commit_ready");
212
+ assert_eq!(snapshot["nextAction"]["actionId"], "commit.ready");
213
+ assert_eq!(
214
+ snapshot["nextAction"]["reasonCodes"],
215
+ json!(["commit-ready"])
216
+ );
183
217
  assert_eq!(snapshot["nextAction"]["safeToExecute"], false);
184
218
  }
185
219
 
220
+ #[test]
221
+ fn agent_snapshot_reports_stale_proof_as_concrete_check_action() {
222
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
223
+ "allowedPaths": ["README.md", "docs/**"],
224
+ "requiredCheckIds": ["diff-check"],
225
+ "proofPathSets": {
226
+ "old": ["README.md"]
227
+ },
228
+ "proofBatches": [{
229
+ "id": "old-proof",
230
+ "checkedAt": "2026-05-04T12:00:00.000Z",
231
+ "evidencePathSet": "old",
232
+ "proofs": [{ "checkId": "diff-check", "exitCode": 0 }]
233
+ }]
234
+ }))));
235
+ init_git(&root);
236
+ write_fixture_file(&root, "README.md", "covered\n");
237
+ write_fixture_file(&root, "docs/new.md", "not-covered\n");
238
+
239
+ let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
240
+
241
+ assert_eq!(snapshot["nextAction"]["type"], "run_checks");
242
+ assert_eq!(
243
+ snapshot["nextAction"]["actionId"],
244
+ "proof.run_required_checks"
245
+ );
246
+ assert_eq!(
247
+ snapshot["nextAction"]["reasonCodes"],
248
+ json!(["changed_docs", "stale-proof"])
249
+ );
250
+ assert_eq!(snapshot["nextAction"]["checkIds"], json!(["diff-check"]));
251
+ assert_eq!(
252
+ snapshot["nextAction"]["paths"],
253
+ json!(["README.md", "docs/new.md"])
254
+ );
255
+ }
256
+
257
+ #[test]
258
+ fn agent_snapshot_blocks_scope_drift_before_edit_or_commit_paths() {
259
+ let root = fixture_root(task_state());
260
+ init_git(&root);
261
+ write_fixture_file(&root, "README.md", "changed\n");
262
+ write_fixture_file(&root, "src/outside.rs", "pub fn outside() {}\n");
263
+
264
+ let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
265
+
266
+ assert_eq!(snapshot["nextAction"]["type"], "repair_scope");
267
+ assert_eq!(snapshot["nextAction"]["actionId"], "scope.repair_drift");
268
+ assert_eq!(
269
+ snapshot["nextAction"]["reasonCodes"],
270
+ json!(["scope-drift"])
271
+ );
272
+ assert_eq!(snapshot["nextAction"]["paths"], json!(["src/outside.rs"]));
273
+ assert_eq!(snapshot["nextAction"]["safeToExecute"], false);
274
+ assert_eq!(snapshot["nextAction"]["requiresUserApproval"], true);
275
+ }
276
+
277
+ #[test]
278
+ fn task_loop_execute_safe_records_proof_compacts_and_returns_commit_ready() {
279
+ let root = fixture_root(task_state());
280
+ let mut state: Value =
281
+ serde_json::from_str(&fs::read_to_string(root.join(".naome/task-state.json")).unwrap())
282
+ .unwrap();
283
+ state["activeTask"]["proofResults"] = json!([{
284
+ "checkId": "old-check",
285
+ "command": "old",
286
+ "cwd": ".",
287
+ "exitCode": 0,
288
+ "checkedAt": "2026-05-14T00:00:00.000Z",
289
+ "evidence": ["README.md"],
290
+ "stdoutSummary": "verbose",
291
+ "stderrSummary": "verbose",
292
+ "durationMs": 99
293
+ }]);
294
+ write_json(&root, ".naome/task-state.json", &state);
295
+ init_git(&root);
296
+ write_fixture_file(&root, "README.md", "changed\n");
297
+
298
+ let looped = run_json(&root, ["task", "loop", "--execute-safe", "--json"]);
299
+
300
+ assert_eq!(looped["schema"], "naome.task.loop.v1");
301
+ assert_eq!(looped["nextAction"]["type"], "commit_ready");
302
+ assert_eq!(looped["nextAction"]["actionId"], "commit.ready");
303
+ assert_eq!(looped["nextAction"]["reasonCodes"], json!(["commit-ready"]));
304
+ assert!(looped["executedSteps"]
305
+ .as_array()
306
+ .unwrap()
307
+ .iter()
308
+ .any(|step| {
309
+ step["schema"] == "naome.task.run-check.v1"
310
+ && step["checkId"] == "diff-check"
311
+ && step["recordedProof"] == true
312
+ }));
313
+ assert!(looped["executedSteps"]
314
+ .as_array()
315
+ .unwrap()
316
+ .iter()
317
+ .any(|step| {
318
+ step["schema"] == "naome.task.compact-proof.v1" && step["compacted"] == true
319
+ }));
320
+
321
+ let changed = fs::read_to_string(root.join(".naome/task-state.json")).unwrap();
322
+ assert!(!changed.contains("stdoutSummary"));
323
+ assert!(changed.contains("evidenceFingerprint"));
324
+ }
325
+
186
326
  #[test]
187
327
  fn commit_preflight_surfaces_transition_blocker_action_and_exit_code() {
188
328
  let root = fixture_root(task_state_with_active_task(active_task(json!({
@@ -204,7 +344,12 @@ fn commit_preflight_surfaces_transition_blocker_action_and_exit_code() {
204
344
  preflight["blockingFindings"][0]["id"],
205
345
  "task.transition.human_review_required"
206
346
  );
207
- assert_eq!(preflight["nextAction"]["type"], "blocked");
347
+ assert_eq!(preflight["nextAction"]["type"], "human_review");
348
+ assert_eq!(preflight["nextAction"]["actionId"], "human_review.required");
349
+ assert_eq!(
350
+ preflight["nextAction"]["reasonCodes"],
351
+ json!(["human-review-required"])
352
+ );
208
353
  assert_eq!(preflight["nextAction"]["safeToExecute"], false);
209
354
 
210
355
  let output = Command::new(env!("CARGO_BIN_EXE_naome"))
@@ -0,0 +1,59 @@
1
+ mod task_cli_support;
2
+
3
+ use std::fs;
4
+
5
+ use task_cli_support::{fixture_root, git, run_json, task_state, write_json};
6
+
7
+ #[test]
8
+ fn task_control_commands_work_without_task_state_projection_file() {
9
+ let root = fixture_root(task_state());
10
+ fs::remove_file(root.join(".naome/task-state.json")).unwrap();
11
+ write_json(
12
+ &root,
13
+ ".naome/manifest.json",
14
+ &serde_json::json!({
15
+ "schema": "naome.manifest.v1",
16
+ "name": "naome",
17
+ "harnessVersion": "1.4.6"
18
+ }),
19
+ );
20
+ git(&root, ["init"]);
21
+ git(&root, ["config", "user.email", "naome@example.com"]);
22
+ git(&root, ["config", "user.name", "NAOME Test"]);
23
+ git(
24
+ &root,
25
+ [
26
+ "add",
27
+ ".naomeignore",
28
+ ".naome/manifest.json",
29
+ ".naome/verification.json",
30
+ "README.md",
31
+ ],
32
+ );
33
+ git(&root, ["commit", "-m", "baseline"]);
34
+
35
+ let status = run_json(&root, ["task", "status", "--json"]);
36
+ assert_eq!(status["state"], "idle");
37
+ assert_eq!(status["taskId"], serde_json::Value::Null);
38
+ assert_eq!(status["proof"]["missingChecks"], serde_json::json!([]));
39
+
40
+ let proof_plan = run_json(&root, ["task", "proof-plan", "--json"]);
41
+ assert_eq!(proof_plan["state"], "idle");
42
+ assert_eq!(proof_plan["proof"]["requiredChecks"], serde_json::json!([]));
43
+
44
+ let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
45
+ assert_eq!(snapshot["task"]["state"], "idle");
46
+ assert_eq!(snapshot["nextAction"]["type"], "blocked");
47
+ assert_eq!(
48
+ snapshot["nextAction"]["primaryFindingId"],
49
+ "task.transition.no_active_task"
50
+ );
51
+
52
+ let commit_preflight = run_json(&root, ["task", "commit-preflight", "--json"]);
53
+ assert_eq!(commit_preflight["wouldPass"], false);
54
+ assert_eq!(commit_preflight["nextAction"]["type"], "blocked");
55
+ assert_eq!(
56
+ commit_preflight["nextAction"]["primaryFindingId"],
57
+ "task.transition.no_active_task"
58
+ );
59
+ }
@@ -0,0 +1,107 @@
1
+ use serde_json::json;
2
+
3
+ mod task_cli_support;
4
+
5
+ use task_cli_support::{
6
+ active_task, fixture_root, init_git, run_json, task_state, task_state_with_active_task,
7
+ write_fixture_file, write_verification_checks,
8
+ };
9
+
10
+ #[test]
11
+ fn agent_snapshot_records_existing_receipts_before_rerunning_checks() {
12
+ let root = fixture_root(task_state());
13
+ init_git(&root);
14
+ write_fixture_file(&root, "README.md", "changed\n");
15
+
16
+ let checked = run_json(
17
+ &root,
18
+ ["task", "run-check", "--check", "diff-check", "--json"],
19
+ );
20
+ assert_eq!(checked["exitCode"], 0);
21
+ assert_eq!(checked["recordedProof"], false);
22
+
23
+ let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
24
+
25
+ assert_eq!(snapshot["nextAction"]["type"], "record_proof");
26
+ assert_eq!(
27
+ snapshot["nextAction"]["actionId"],
28
+ "proof.record_from_receipts"
29
+ );
30
+ assert_eq!(snapshot["nextAction"]["checkIds"], json!(["diff-check"]));
31
+ assert_eq!(snapshot["nextAction"]["safeToExecute"], true);
32
+ }
33
+
34
+ #[test]
35
+ fn commit_preflight_surfaces_generic_transition_blocker_action() {
36
+ let mut state = task_state_with_active_task(active_task(json!({
37
+ "requiredCheckIds": [],
38
+ "proofResults": []
39
+ })));
40
+ state["status"] = json!("blocked");
41
+ let root = fixture_root(state);
42
+ init_git(&root);
43
+
44
+ let preflight = run_json(&root, ["task", "commit-preflight", "--json"]);
45
+
46
+ assert_eq!(preflight["wouldPass"], false);
47
+ assert_eq!(
48
+ preflight["blockingFindings"][0]["id"],
49
+ "task.transition.blocked_state"
50
+ );
51
+ assert_eq!(preflight["nextAction"]["type"], "blocked");
52
+ assert_eq!(preflight["nextAction"]["actionId"], "transition.blocked");
53
+ assert_eq!(
54
+ preflight["nextAction"]["reasonCodes"],
55
+ json!(["transition-blocked"])
56
+ );
57
+ assert_eq!(
58
+ preflight["nextAction"]["primaryFindingId"],
59
+ "task.transition.blocked_state"
60
+ );
61
+ }
62
+
63
+ #[test]
64
+ fn task_loop_execute_safe_runs_merged_safe_impact_plan() {
65
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
66
+ "allowedPaths": ["src/lib.rs"],
67
+ "requiredCheckIds": ["diff-check"],
68
+ "proofResults": []
69
+ }))));
70
+ write_verification_checks(
71
+ &root,
72
+ json!([
73
+ {
74
+ "id": "diff-check",
75
+ "command": "git diff --check",
76
+ "cwd": ".",
77
+ "purpose": "Detect whitespace and patch formatting issues.",
78
+ "cost": "fast",
79
+ "source": "git",
80
+ "evidence": ["src/lib.rs"],
81
+ "lastVerified": null
82
+ },
83
+ {
84
+ "id": "repository-quality-check",
85
+ "command": "git diff --check",
86
+ "cwd": ".",
87
+ "purpose": "Exercise an additional safe impact-planned check.",
88
+ "cost": "fast",
89
+ "source": "git",
90
+ "evidence": ["src/lib.rs"],
91
+ "lastVerified": null
92
+ }
93
+ ]),
94
+ );
95
+ init_git(&root);
96
+ write_fixture_file(&root, "src/lib.rs", "pub fn changed() {}\n");
97
+
98
+ let looped = run_json(&root, ["task", "loop", "--execute-safe", "--json"]);
99
+ let executed = looped["executedSteps"].as_array().unwrap();
100
+
101
+ assert!(executed
102
+ .iter()
103
+ .any(|step| step["checkId"] == "diff-check" && step["executed"] == true));
104
+ assert!(executed
105
+ .iter()
106
+ .any(|step| { step["checkId"] == "repository-quality-check" && step["executed"] == true }));
107
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
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
@@ -39,7 +39,7 @@ const CLASSES: &[(&str, &str, &str, &str)] = &[
39
39
  (
40
40
  "generated_projection",
41
41
  "Compatibility views generated from durable state.",
42
- "commit_when_required_by_compatibility",
42
+ "never_commit",
43
43
  "regenerate_from_durable_state",
44
44
  ),
45
45
  (
@@ -21,7 +21,6 @@ pub const PROJECT_OWNED_PATHS: &[&str] = &[
21
21
  ".naomeignore",
22
22
  ".naome/init-state.json",
23
23
  ".naome/manifest.json",
24
- ".naome/task-state.json",
25
24
  ".naome/upgrade-state.json",
26
25
  ".naome/verification.json",
27
26
  ".naome/repository-model.json",
@@ -43,6 +42,7 @@ pub const LOCAL_ONLY_MACHINE_OWNED_PATHS: &[&str] = &[
43
42
  ".naome/bin/naome.js",
44
43
  ".naome/bin/check-task-state.js",
45
44
  ".naome/bin/check-harness-health.js",
45
+ ".naome/task-state.json",
46
46
  ".naome/task-contract.schema.json",
47
47
  "docs/naome/index.md",
48
48
  "docs/naome/first-run.md",
@@ -76,7 +76,7 @@ pub struct InstallPlan {
76
76
  }
77
77
 
78
78
  pub fn install_plan(harness_version: impl Into<String>) -> InstallPlan {
79
- let gitignore_entries = vec![".naome/task-journal.jsonl"];
79
+ let gitignore_entries = vec![".naome/task-journal.jsonl", ".naome/tasks/"];
80
80
  let mut git_exclude_entries = vec![
81
81
  "# NAOME local machine-owned harness files.",
82
82
  ".naome/archive/",
@@ -56,7 +56,9 @@ pub(super) fn baseline_harness_refresh_then_completed_task(
56
56
  "Unable to split harness refresh paths from completed task diff.",
57
57
  ));
58
58
  };
59
- commit_harness_paths(root, &split.harness_paths, execution)?;
59
+ let mut harness_paths = split.harness_paths;
60
+ harness_paths.extend(split.local_only_cleanup_paths);
61
+ commit_harness_paths(root, &harness_paths, execution)?;
60
62
  baseline_completed_task(
61
63
  root,
62
64
  "route_auto_baseline",
@@ -29,8 +29,10 @@ pub(super) fn git_add_completed_task_paths(root: &Path) -> Result<(), NaomeError
29
29
  }
30
30
 
31
31
  pub(super) fn git_stage_only_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
32
+ let cached_only_deletions = staged_deletions_with_local_files(root, paths)?;
32
33
  ensure_git_success(git_output(root, &["reset", "-q", "--", "."])?)?;
33
- git_add_paths(root, paths)
34
+ git_add_paths(root, paths)?;
35
+ git_rm_cached_paths(root, &cached_only_deletions)
34
36
  }
35
37
 
36
38
  fn git_add_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
@@ -81,6 +83,51 @@ fn git_ls_files(
81
83
  }
82
84
  }
83
85
 
86
+ fn staged_deletions_with_local_files(
87
+ root: &Path,
88
+ paths: &[String],
89
+ ) -> Result<Vec<String>, NaomeError> {
90
+ if paths.is_empty() {
91
+ return Ok(Vec::new());
92
+ }
93
+ let mut args = vec!["diff", "--name-status", "--cached", "-z", "--"];
94
+ args.extend(paths.iter().map(String::as_str));
95
+ let output = Command::new("git").args(args).current_dir(root).output()?;
96
+ if !output.status.success() {
97
+ return Err(NaomeError::new(command_output(&output)));
98
+ }
99
+
100
+ let mut deletions = Vec::new();
101
+ let tokens = split_nul_paths(&output.stdout);
102
+ let mut index = 0;
103
+ while index + 1 < tokens.len() {
104
+ let status = &tokens[index];
105
+ let path = tokens[index + 1].clone();
106
+ index += if status.starts_with('R') || status.starts_with('C') {
107
+ 3
108
+ } else {
109
+ 2
110
+ };
111
+ if status == "D" && root.join(&path).exists() && is_local_only_cleanup_path(&path) {
112
+ deletions.push(path);
113
+ }
114
+ }
115
+ Ok(deletions)
116
+ }
117
+
118
+ fn git_rm_cached_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
119
+ if paths.is_empty() {
120
+ return Ok(());
121
+ }
122
+ let mut args = vec!["rm", "--cached", "-q", "--ignore-unmatch", "--"];
123
+ args.extend(paths.iter().map(String::as_str));
124
+ ensure_git_success(Command::new("git").args(args).current_dir(root).output()?)
125
+ }
126
+
127
+ fn is_local_only_cleanup_path(path: &str) -> bool {
128
+ path == ".naome/task-state.json" || path.starts_with(".naome/tasks/")
129
+ }
130
+
84
131
  fn split_nul_paths(bytes: &[u8]) -> Vec<String> {
85
132
  String::from_utf8_lossy(bytes)
86
133
  .split('\0')
@@ -1,5 +1,6 @@
1
1
  use std::fs;
2
2
  use std::path::Path;
3
+ use std::process::Command;
3
4
 
4
5
  use crate::install_plan::{LOCAL_NATIVE_BINARY_PATHS, LOCAL_ONLY_MACHINE_OWNED_PATHS};
5
6
  use crate::models::NaomeError;
@@ -13,7 +14,10 @@ pub(super) fn copy_local_harness_files(
13
14
  paths.extend_from_slice(LOCAL_NATIVE_BINARY_PATHS);
14
15
 
15
16
  for relative_path in paths {
16
- if relative_path == ".naome/archive" || relative_path == ".naome/task-journal.jsonl" {
17
+ if relative_path == ".naome/archive"
18
+ || relative_path == ".naome/task-journal.jsonl"
19
+ || relative_path == ".naome/task-state.json"
20
+ {
17
21
  continue;
18
22
  }
19
23
 
@@ -27,6 +31,37 @@ pub(super) fn copy_local_harness_files(
27
31
  }
28
32
  fs::copy(&source, &destination)?;
29
33
  }
34
+ write_idle_projection_if_local_only(worktree_root)?;
30
35
 
31
36
  Ok(())
32
37
  }
38
+
39
+ fn write_idle_projection_if_local_only(worktree_root: &Path) -> Result<(), NaomeError> {
40
+ let output = Command::new("git")
41
+ .args([
42
+ "ls-files",
43
+ "--error-unmatch",
44
+ "--",
45
+ ".naome/task-state.json",
46
+ ])
47
+ .current_dir(worktree_root)
48
+ .output()?;
49
+ if output.status.success() {
50
+ return Ok(());
51
+ }
52
+
53
+ fs::write(
54
+ worktree_root.join(".naome/task-state.json"),
55
+ concat!(
56
+ "{\n",
57
+ " \"schema\": \"naome.task-state.v2\",\n",
58
+ " \"version\": 2,\n",
59
+ " \"status\": \"idle\",\n",
60
+ " \"activeTask\": null,\n",
61
+ " \"blocker\": null,\n",
62
+ " \"updatedAt\": \"1970-01-01T00:00:00.000Z\"\n",
63
+ "}\n"
64
+ ),
65
+ )?;
66
+ Ok(())
67
+ }