@lamentis/naome 1.4.5 → 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 (38) 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/common.rs +3 -3
  5. package/crates/naome-cli/tests/task_cli_local_state.rs +59 -0
  6. package/crates/naome-core/Cargo.toml +1 -1
  7. package/crates/naome-core/src/information_architecture.rs +1 -1
  8. package/crates/naome-core/src/install_plan.rs +2 -2
  9. package/crates/naome-core/src/route/execution_baselines.rs +3 -1
  10. package/crates/naome-core/src/route/git_ops.rs +48 -1
  11. package/crates/naome-core/src/route/worktree_files.rs +36 -1
  12. package/crates/naome-core/src/task_ledger.rs +13 -2
  13. package/crates/naome-core/src/task_state/commit_gate.rs +29 -0
  14. package/crates/naome-core/src/task_state/completed_refresh.rs +10 -2
  15. package/crates/naome-core/src/task_state/task_diff_api.rs +17 -3
  16. package/crates/naome-core/src/task_state/types.rs +1 -0
  17. package/crates/naome-core/tests/information_architecture.rs +1 -4
  18. package/crates/naome-core/tests/install_plan.rs +7 -0
  19. package/crates/naome-core/tests/repo_support/mod.rs +4 -3
  20. package/crates/naome-core/tests/route_baseline.rs +104 -0
  21. package/crates/naome-core/tests/route_worktree.rs +47 -1
  22. package/crates/naome-core/tests/task_ledger.rs +141 -203
  23. package/crates/naome-core/tests/task_ledger_support/mod.rs +206 -0
  24. package/crates/naome-core/tests/task_state.rs +38 -1
  25. package/crates/naome-core/tests/task_state_compact.rs +1 -1
  26. package/installer/harness-file-ops.js +6 -1
  27. package/installer/harness-files.js +10 -1
  28. package/native/darwin-arm64/naome +0 -0
  29. package/native/linux-x64/naome +0 -0
  30. package/package.json +1 -1
  31. package/templates/naome-root/.naome/bin/check-harness-health.js +4 -4
  32. package/templates/naome-root/.naome/bin/check-task-state.js +4 -4
  33. package/templates/naome-root/.naome/manifest.json +5 -6
  34. package/templates/naome-root/AGENTS.md +3 -1
  35. package/templates/naome-root/docs/naome/agent-workflow.md +4 -3
  36. package/templates/naome-root/docs/naome/architecture.md +2 -2
  37. package/templates/naome-root/docs/naome/execution.md +6 -6
  38. package/templates/naome-root/docs/naome/task-ledger.md +15 -11
@@ -1,8 +1,13 @@
1
1
  use serde_json::json;
2
+ use std::fs;
2
3
 
3
4
  mod repo_support;
4
5
 
5
- use repo_support::{route_readme_task, try_route_readme_task, TestRepo};
6
+ use naome_core::read_task_state_projection;
7
+ use repo_support::{
8
+ completed_task_state, diff_check, route_readme_task, try_route_readme_task, verification_value,
9
+ TestRepo,
10
+ };
6
11
 
7
12
  #[test]
8
13
  fn execute_route_refuses_to_create_more_than_max_isolated_worktrees() {
@@ -52,3 +57,44 @@ fn execute_route_uses_preflighted_worktree_name_after_completed_task_baseline()
52
57
  assert!(worktree.branch.contains(before_short));
53
58
  assert!(worktree.path.contains(before_short));
54
59
  }
60
+
61
+ #[test]
62
+ fn execute_route_resets_local_task_state_projection_in_isolated_worktree() {
63
+ let repo = TestRepo::new("route-worktree-no-stale-projection");
64
+ repo.init_git();
65
+ repo.write_file(".naomeignore", ".naome/archive/\n.naome/tasks/\n");
66
+ repo.write_file("README.md", "# Baseline\n");
67
+ repo.write_file("USER.md", "user baseline\n");
68
+ repo.write_naome_json(
69
+ "init-state.json",
70
+ json!({ "initialized": true, "intakeStatus": "complete" }),
71
+ );
72
+ repo.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
73
+ repo.write_naome_json(
74
+ "verification.json",
75
+ verification_value("ready", vec![diff_check(vec!["README.md"])], vec![]),
76
+ );
77
+ repo.git(&["add", "."]);
78
+ repo.git(&["commit", "-m", "baseline"]);
79
+ let git_dir = repo.git_stdout(&["rev-parse", "--git-dir"]);
80
+ std::fs::write(
81
+ repo.path().join(git_dir).join("info").join("exclude"),
82
+ ".naome/task-state.json\n",
83
+ )
84
+ .unwrap();
85
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
86
+ repo.write_naome_json("task-state.json", completed_task_state(&admission_head));
87
+ repo.write_file("README.md", "# Changed\n");
88
+ repo.write_file("USER.md", "user local edit\n");
89
+
90
+ let route = route_readme_task(&repo, true);
91
+ let task_root = route.task_root.as_str();
92
+
93
+ assert_ne!(task_root, repo.path().to_string_lossy().as_ref());
94
+ assert!(fs::exists(std::path::Path::new(task_root).join(".naome/task-state.json")).unwrap());
95
+ let projection = read_task_state_projection(std::path::Path::new(task_root))
96
+ .unwrap()
97
+ .unwrap();
98
+ assert_eq!(projection["status"], "idle");
99
+ assert_eq!(projection["activeTask"], serde_json::Value::Null);
100
+ }
@@ -1,12 +1,30 @@
1
1
  mod repo_support;
2
+ mod task_ledger_support;
2
3
 
3
4
  use naome_core::{
4
- migrate_task_state_to_ledger, read_task_state_projection, render_task_state_from_ledger,
5
- validate_task_state, TaskStateMode, TaskStateOptions,
5
+ completed_task_commit_paths, migrate_task_state_to_ledger, read_task_state_projection,
6
+ render_task_state_from_ledger, validate_task_state, TaskStateMode, TaskStateOptions,
6
7
  };
7
- use serde_json::{json, Map, Value};
8
+ use serde_json::{json, Value};
8
9
 
9
- use repo_support::{diff_check, verification_value, TestRepo};
10
+ use repo_support::TestRepo;
11
+ use task_ledger_support::{
12
+ compact_complete_state, compact_task_spec_record, format_json, idle_state, object,
13
+ task_spec_record_with_id, LedgerHarness,
14
+ };
15
+
16
+ #[test]
17
+ fn missing_task_state_projection_derives_idle_state() {
18
+ let repo = TestRepo::new("missing-projection");
19
+
20
+ let state = read_task_state_projection(repo.path()).unwrap().unwrap();
21
+
22
+ assert_eq!(state["schema"], "naome.task-state.v2");
23
+ assert_eq!(state["status"], "idle");
24
+ assert_eq!(state["activeTask"], Value::Null);
25
+ assert_eq!(state["blocker"], Value::Null);
26
+ assert_eq!(state["updatedAt"], "1970-01-01T00:00:00.000Z");
27
+ }
10
28
 
11
29
  #[test]
12
30
  fn ledger_projection_renders_compatible_task_state_without_task_state_json() {
@@ -37,6 +55,76 @@ fn ledger_projection_renders_compatible_task_state_without_task_state_json() {
37
55
  assert!(report.errors.is_empty(), "{:?}", report.errors);
38
56
  }
39
57
 
58
+ #[test]
59
+ fn completed_task_commit_paths_exclude_local_task_state_projection() {
60
+ let repo = TestRepo::new("commit-with-local-projection");
61
+ repo.init_git();
62
+ repo.write_file("README.md", "# Baseline\n");
63
+ repo.write_base_harness(None);
64
+ repo.git(&["add", "."]);
65
+ repo.git(&["commit", "-m", "baseline"]);
66
+ let head = repo.git_stdout(&["rev-parse", "HEAD"]);
67
+ repo.write_complete_ledger(&head);
68
+ render_task_state_from_ledger(repo.path(), true).unwrap();
69
+ repo.write_file("README.md", "# Changed\n");
70
+
71
+ let paths = completed_task_commit_paths(repo.path()).unwrap();
72
+
73
+ assert_eq!(paths, vec!["README.md"]);
74
+ }
75
+
76
+ #[test]
77
+ fn completed_task_commit_paths_include_tracked_projection_untrack_deletion() {
78
+ let repo = TestRepo::new("commit-with-projection-deletion");
79
+ repo.init_git();
80
+ repo.write_file("README.md", "# Baseline\n");
81
+ repo.write_base_harness(Some(idle_state()));
82
+ repo.git(&["add", "."]);
83
+ repo.git(&["commit", "-m", "baseline"]);
84
+ let head = repo.git_stdout(&["rev-parse", "HEAD"]);
85
+ repo.write_complete_ledger(&head);
86
+ repo.write_file("README.md", "# Changed\n");
87
+ repo.git(&["rm", "--cached", "-q", "--", ".naome/task-state.json"]);
88
+
89
+ let paths = completed_task_commit_paths(repo.path()).unwrap();
90
+
91
+ assert_eq!(paths, vec![".naome/task-state.json", "README.md"]);
92
+ assert!(repo.path().join(".naome/task-state.json").is_file());
93
+ }
94
+
95
+ #[test]
96
+ fn local_runtime_ledgers_can_diverge_between_parallel_worktrees() {
97
+ let repo = TestRepo::new("parallel-ledger-main");
98
+ repo.init_git();
99
+ repo.write_file("README.md", "# Baseline\n");
100
+ repo.write_base_harness(None);
101
+ repo.git(&["add", "."]);
102
+ repo.git(&["commit", "-m", "baseline"]);
103
+ let branch_root = repo.path().with_file_name(format!(
104
+ "{}-branch",
105
+ repo.path().file_name().unwrap().to_string_lossy()
106
+ ));
107
+ repo.git(&[
108
+ "worktree",
109
+ "add",
110
+ "-b",
111
+ "parallel-ledger-branch",
112
+ branch_root.to_str().unwrap(),
113
+ "HEAD",
114
+ ]);
115
+ let head = repo.git_stdout(&["rev-parse", "HEAD"]);
116
+ repo.write_complete_ledger(&head);
117
+ write_branch_ledger(&branch_root, &head, "branch-task");
118
+
119
+ let main_state = read_task_state_projection(repo.path()).unwrap().unwrap();
120
+ let branch_state = read_task_state_projection(&branch_root).unwrap().unwrap();
121
+
122
+ assert_eq!(main_state["activeTask"]["id"], "readme-task");
123
+ assert_eq!(branch_state["activeTask"]["id"], "branch-task");
124
+ assert!(!repo.path().join(".naome/task-state.json").exists());
125
+ assert!(!branch_root.join(".naome/task-state.json").exists());
126
+ }
127
+
40
128
  #[test]
41
129
  fn task_state_json_remains_backward_compatible_when_no_ledger_exists() {
42
130
  let repo = TestRepo::new("legacy");
@@ -47,6 +135,29 @@ fn task_state_json_remains_backward_compatible_when_no_ledger_exists() {
47
135
  assert_eq!(state["status"], "idle");
48
136
  }
49
137
 
138
+ #[test]
139
+ fn ignored_ledger_keeps_cli_written_projection_authoritative() {
140
+ let repo = TestRepo::new("ignored-ledger-with-cli-projection");
141
+ repo.init_git();
142
+ repo.write_file(".naomeignore", ".naome/archive/\n.naome/tasks/\n");
143
+ repo.write_file("README.md", "# Baseline\n");
144
+ repo.write_base_harness(Some(idle_state()));
145
+ repo.git(&["add", "."]);
146
+ repo.git(&["commit", "-m", "baseline"]);
147
+ let head = repo.git_stdout(&["rev-parse", "HEAD"]);
148
+ repo.write_complete_ledger(&head);
149
+ repo.write_naome_json("task-state.json", compact_complete_state(&head));
150
+ repo.write_file("README.md", "# Changed\n");
151
+
152
+ let state = read_task_state_projection(repo.path()).unwrap().unwrap();
153
+
154
+ assert_eq!(state["status"], "complete");
155
+ assert_eq!(
156
+ state["activeTask"]["proofBatches"][0]["proofs"][0]["checkId"],
157
+ "diff-check"
158
+ );
159
+ }
160
+
50
161
  #[test]
51
162
  fn renderer_can_write_projection_for_external_tools() {
52
163
  let repo = TestRepo::new("write");
@@ -254,205 +365,32 @@ fn compact_completed_task_state_survives_incomplete_local_runtime_ledger() {
254
365
  assert!(report.errors.is_empty(), "{:?}", report.errors);
255
366
  }
256
367
 
257
- trait LedgerHarness {
258
- fn write_base_harness(&self, task_state: Option<Value>);
259
- fn write_complete_ledger(&self, admission_head: &str);
260
- }
261
-
262
- impl LedgerHarness for TestRepo {
263
- fn write_base_harness(&self, task_state: Option<Value>) {
264
- self.write_naome_json(
265
- "init-state.json",
266
- json!({ "initialized": true, "intakeStatus": "complete" }),
267
- );
268
- self.write_naome_json(
269
- "upgrade-state.json",
270
- json!({ "status": "complete", "pending": [] }),
271
- );
272
- self.write_naome_json(
273
- "verification.json",
274
- verification_value("ready", vec![diff_check(vec!["README.md"])], vec![]),
275
- );
276
- if let Some(state) = task_state {
277
- self.write_naome_json("task-state.json", state);
278
- }
279
- }
280
-
281
- fn write_complete_ledger(&self, admission_head: &str) {
282
- self.write_file(
283
- ".naome/tasks/active.json",
284
- &format_json(object([
285
- ("schema", json!("naome.task-ledger-active.v1")),
286
- ("version", json!(1)),
287
- ("primaryTaskId", json!("readme-task")),
288
- (
289
- "worklanes",
290
- json!([{ "id": "default", "taskId": "readme-task", "status": "active" }]),
291
- ),
292
- ])),
293
- );
294
- self.write_file(
295
- ".naome/tasks/readme-task/task.json",
296
- &format_json(task_spec_record(admission_head)),
297
- );
298
- self.write_file(
299
- ".naome/tasks/readme-task/events.jsonl",
300
- concat!(
301
- "{\"schema\":\"naome.task-ledger-event.v1\",\"type\":\"status\",\"status\":\"implementing\",\"recordedAt\":\"2026-05-08T00:00:01.000Z\"}\n",
302
- "{\"schema\":\"naome.task-ledger-event.v1\",\"type\":\"status\",\"status\":\"complete\",\"recordedAt\":\"2026-05-08T00:00:02.000Z\"}\n"
368
+ fn write_branch_ledger(root: &std::path::Path, admission_head: &str, task_id: &str) {
369
+ std::fs::create_dir_all(root.join(".naome/tasks").join(task_id)).unwrap();
370
+ std::fs::write(
371
+ root.join(".naome/tasks/active.json"),
372
+ format_json(object([
373
+ ("schema", json!("naome.task-ledger-active.v1")),
374
+ ("version", json!(1)),
375
+ ("primaryTaskId", json!(task_id)),
376
+ (
377
+ "worklanes",
378
+ json!([{ "id": "default", "taskId": task_id, "status": "active" }]),
303
379
  ),
304
- );
305
- self.write_file(
306
- ".naome/tasks/readme-task/mutations.json",
307
- &format_json(object([
308
- ("schema", json!("naome.task-ledger-mutations.v1")),
309
- ("version", json!(1)),
310
- ("taskId", json!("readme-task")),
311
- ("touchedPaths", json!(["README.md"])),
312
- (
313
- "entries",
314
- json!([{ "path": "README.md", "class": "source change", "owner": "agent" }]),
315
- ),
316
- ])),
317
- );
318
- self.write_file(
319
- ".naome/tasks/readme-task/proofs/diff-check.json",
320
- &format_json(diff_proof_record()),
321
- );
322
- }
323
- }
324
-
325
- fn idle_state() -> Value {
326
- object([
327
- ("schema", json!("naome.task-state.v1")),
328
- ("version", json!(1)),
329
- ("status", json!("idle")),
330
- ("activeTask", Value::Null),
331
- ("blocker", Value::Null),
332
- ("updatedAt", json!("2026-05-08T00:00:00.000Z")),
333
- ])
334
- }
335
-
336
- fn compact_complete_state(admission_head: &str) -> Value {
337
- object([
338
- ("schema", json!("naome.task-state.v2")),
339
- ("version", json!(2)),
340
- ("status", json!("complete")),
341
- ("activeTask", compact_active_task(admission_head)),
342
- ("blocker", Value::Null),
343
- ("updatedAt", json!("2026-05-08T00:00:04.000Z")),
344
- ])
345
- }
346
-
347
- fn compact_active_task(admission_head: &str) -> Value {
348
- object([
349
- ("id", json!("readme-task")),
350
- ("request", json!("Update README.")),
351
- ("userPrompt", user_prompt()),
352
- ("admission", admission_record(admission_head)),
353
- ("allowedPaths", json!(["README.md"])),
354
- ("declaredChangeTypes", json!(["product-docs"])),
355
- ("requiredCheckIds", json!(["diff-check"])),
356
- ("proofResults", json!([])),
357
- ("proofPathSets", object([("changed", json!(["README.md"]))])),
358
- ("proofBatches", json!([compact_proof_batch()])),
359
- ("revisions", json!([])),
360
- ("humanReview", human_review()),
361
- ])
362
- }
363
-
364
- fn task_spec_record(admission_head: &str) -> Value {
365
- task_spec_record_with_id(admission_head, "readme-task")
366
- }
367
-
368
- fn task_spec_record_with_id(admission_head: &str, task_id: &str) -> Value {
369
- object([
370
- ("schema", json!("naome.task-ledger-task.v1")),
371
- ("version", json!(1)),
372
- ("id", json!(task_id)),
373
- ("request", json!("Update README.")),
374
- ("userPrompt", user_prompt()),
375
- ("admission", admission_record(admission_head)),
376
- ("allowedPaths", json!(["README.md"])),
377
- ("declaredChangeTypes", json!(["product-docs"])),
378
- ("requiredCheckIds", json!(["diff-check"])),
379
- ("humanReview", human_review()),
380
- ])
381
- }
382
-
383
- fn compact_task_spec_record(admission_head: &str) -> Value {
384
- let mut task = task_spec_record(admission_head);
385
- let task_object = task.as_object_mut().unwrap();
386
- task_object.insert(
387
- "proofPathSets".to_string(),
388
- object([("changed", json!(["README.md"]))]),
389
- );
390
- task_object.insert("proofBatches".to_string(), json!([compact_proof_batch()]));
391
- task
392
- }
393
-
394
- fn diff_proof_record() -> Value {
395
- object([
396
- ("schema", json!("naome.task-ledger-proof.v1")),
397
- ("version", json!(1)),
398
- ("taskId", json!("readme-task")),
399
- ("checkId", json!("diff-check")),
400
- ("command", json!("git diff --check")),
401
- ("cwd", json!(".")),
402
- ("exitCode", json!(0)),
403
- ("checkedAt", json!("2026-05-08T00:00:03.000Z")),
404
- ("evidence", json!(["README.md"])),
405
- ])
406
- }
407
-
408
- fn compact_proof_batch() -> Value {
409
- object([
410
- ("checkedAt", json!("2026-05-08T00:00:03.000Z")),
411
- ("evidencePathSet", json!("changed")),
412
- (
413
- "proofs",
414
- json!([{ "checkId": "diff-check", "exitCode": 0 }]),
415
- ),
416
- ])
417
- }
418
-
419
- fn admission_record(git_head: &str) -> Value {
420
- object([
421
- (
422
- "command",
423
- json!("node .naome/bin/check-task-state.js --admission"),
380
+ ])),
381
+ )
382
+ .unwrap();
383
+ std::fs::write(
384
+ root.join(".naome/tasks").join(task_id).join("task.json"),
385
+ format_json(task_spec_record_with_id(admission_head, task_id)),
386
+ )
387
+ .unwrap();
388
+ std::fs::write(
389
+ root.join(".naome/tasks").join(task_id).join("events.jsonl"),
390
+ concat!(
391
+ "{\"schema\":\"naome.task-ledger-event.v1\",\"type\":\"status\",\"status\":\"implementing\",\"recordedAt\":\"2026-05-08T00:00:01.000Z\"}\n",
392
+ "{\"schema\":\"naome.task-ledger-event.v1\",\"type\":\"status\",\"status\":\"complete\",\"recordedAt\":\"2026-05-08T00:00:02.000Z\"}\n"
424
393
  ),
425
- ("cwd", json!(".")),
426
- ("exitCode", json!(0)),
427
- ("checkedAt", json!("2026-05-08T00:00:00.000Z")),
428
- ("gitHead", json!(git_head)),
429
- ("changedPaths", json!([])),
430
- ])
431
- }
432
-
433
- fn user_prompt() -> Value {
434
- object([
435
- ("receivedAt", json!("2026-05-08T00:00:00.000Z")),
436
- ("text", json!("Update README.")),
437
- ])
438
- }
439
-
440
- fn human_review() -> Value {
441
- object([
442
- ("required", json!(false)),
443
- ("approved", json!(false)),
444
- ("reason", Value::Null),
445
- ])
446
- }
447
-
448
- fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
449
- let mut map = Map::new();
450
- for (key, value) in entries {
451
- map.insert(key.to_string(), value);
452
- }
453
- Value::Object(map)
454
- }
455
-
456
- fn format_json(value: Value) -> String {
457
- format!("{}\n", serde_json::to_string_pretty(&value).unwrap())
394
+ )
395
+ .unwrap();
458
396
  }
@@ -0,0 +1,206 @@
1
+ use serde_json::{json, Map, Value};
2
+
3
+ use super::repo_support::{diff_check, verification_value, TestRepo};
4
+
5
+ pub trait LedgerHarness {
6
+ fn write_base_harness(&self, task_state: Option<Value>);
7
+ fn write_complete_ledger(&self, admission_head: &str);
8
+ }
9
+
10
+ impl LedgerHarness for TestRepo {
11
+ fn write_base_harness(&self, task_state: Option<Value>) {
12
+ self.write_naome_json(
13
+ "init-state.json",
14
+ json!({ "initialized": true, "intakeStatus": "complete" }),
15
+ );
16
+ self.write_naome_json(
17
+ "upgrade-state.json",
18
+ json!({ "status": "complete", "pending": [] }),
19
+ );
20
+ self.write_naome_json(
21
+ "verification.json",
22
+ verification_value("ready", vec![diff_check(vec!["README.md"])], vec![]),
23
+ );
24
+ if let Some(state) = task_state {
25
+ self.write_naome_json("task-state.json", state);
26
+ }
27
+ }
28
+
29
+ fn write_complete_ledger(&self, admission_head: &str) {
30
+ self.write_file(
31
+ ".naome/tasks/active.json",
32
+ &format_json(object([
33
+ ("schema", json!("naome.task-ledger-active.v1")),
34
+ ("version", json!(1)),
35
+ ("primaryTaskId", json!("readme-task")),
36
+ (
37
+ "worklanes",
38
+ json!([{ "id": "default", "taskId": "readme-task", "status": "active" }]),
39
+ ),
40
+ ])),
41
+ );
42
+ self.write_file(
43
+ ".naome/tasks/readme-task/task.json",
44
+ &format_json(task_spec_record(admission_head)),
45
+ );
46
+ self.write_file(
47
+ ".naome/tasks/readme-task/events.jsonl",
48
+ concat!(
49
+ "{\"schema\":\"naome.task-ledger-event.v1\",\"type\":\"status\",\"status\":\"implementing\",\"recordedAt\":\"2026-05-08T00:00:01.000Z\"}\n",
50
+ "{\"schema\":\"naome.task-ledger-event.v1\",\"type\":\"status\",\"status\":\"complete\",\"recordedAt\":\"2026-05-08T00:00:02.000Z\"}\n"
51
+ ),
52
+ );
53
+ self.write_file(
54
+ ".naome/tasks/readme-task/mutations.json",
55
+ &format_json(object([
56
+ ("schema", json!("naome.task-ledger-mutations.v1")),
57
+ ("version", json!(1)),
58
+ ("taskId", json!("readme-task")),
59
+ ("touchedPaths", json!(["README.md"])),
60
+ (
61
+ "entries",
62
+ json!([{ "path": "README.md", "class": "source change", "owner": "agent" }]),
63
+ ),
64
+ ])),
65
+ );
66
+ self.write_file(
67
+ ".naome/tasks/readme-task/proofs/diff-check.json",
68
+ &format_json(diff_proof_record()),
69
+ );
70
+ }
71
+ }
72
+
73
+ pub fn idle_state() -> Value {
74
+ object([
75
+ ("schema", json!("naome.task-state.v1")),
76
+ ("version", json!(1)),
77
+ ("status", json!("idle")),
78
+ ("activeTask", Value::Null),
79
+ ("blocker", Value::Null),
80
+ ("updatedAt", json!("2026-05-08T00:00:00.000Z")),
81
+ ])
82
+ }
83
+
84
+ pub fn compact_complete_state(admission_head: &str) -> Value {
85
+ object([
86
+ ("schema", json!("naome.task-state.v2")),
87
+ ("version", json!(2)),
88
+ ("status", json!("complete")),
89
+ ("activeTask", compact_active_task(admission_head)),
90
+ ("blocker", Value::Null),
91
+ ("updatedAt", json!("2026-05-08T00:00:04.000Z")),
92
+ ])
93
+ }
94
+
95
+ pub fn task_spec_record_with_id(admission_head: &str, task_id: &str) -> Value {
96
+ object([
97
+ ("schema", json!("naome.task-ledger-task.v1")),
98
+ ("version", json!(1)),
99
+ ("id", json!(task_id)),
100
+ ("request", json!("Update README.")),
101
+ ("userPrompt", user_prompt()),
102
+ ("admission", admission_record(admission_head)),
103
+ ("allowedPaths", json!(["README.md"])),
104
+ ("declaredChangeTypes", json!(["product-docs"])),
105
+ ("requiredCheckIds", json!(["diff-check"])),
106
+ ("humanReview", human_review()),
107
+ ])
108
+ }
109
+
110
+ pub fn compact_task_spec_record(admission_head: &str) -> Value {
111
+ let mut task = task_spec_record(admission_head);
112
+ let task_object = task.as_object_mut().unwrap();
113
+ task_object.insert(
114
+ "proofPathSets".to_string(),
115
+ object([("changed", json!(["README.md"]))]),
116
+ );
117
+ task_object.insert("proofBatches".to_string(), json!([compact_proof_batch()]));
118
+ task
119
+ }
120
+
121
+ pub fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
122
+ let mut map = Map::new();
123
+ for (key, value) in entries {
124
+ map.insert(key.to_string(), value);
125
+ }
126
+ Value::Object(map)
127
+ }
128
+
129
+ pub fn format_json(value: Value) -> String {
130
+ format!("{}\n", serde_json::to_string_pretty(&value).unwrap())
131
+ }
132
+
133
+ fn compact_active_task(admission_head: &str) -> Value {
134
+ object([
135
+ ("id", json!("readme-task")),
136
+ ("request", json!("Update README.")),
137
+ ("userPrompt", user_prompt()),
138
+ ("admission", admission_record(admission_head)),
139
+ ("allowedPaths", json!(["README.md"])),
140
+ ("declaredChangeTypes", json!(["product-docs"])),
141
+ ("requiredCheckIds", json!(["diff-check"])),
142
+ ("proofResults", json!([])),
143
+ ("proofPathSets", object([("changed", json!(["README.md"]))])),
144
+ ("proofBatches", json!([compact_proof_batch()])),
145
+ ("revisions", json!([])),
146
+ ("humanReview", human_review()),
147
+ ])
148
+ }
149
+
150
+ fn task_spec_record(admission_head: &str) -> Value {
151
+ task_spec_record_with_id(admission_head, "readme-task")
152
+ }
153
+
154
+ fn diff_proof_record() -> Value {
155
+ object([
156
+ ("schema", json!("naome.task-ledger-proof.v1")),
157
+ ("version", json!(1)),
158
+ ("taskId", json!("readme-task")),
159
+ ("checkId", json!("diff-check")),
160
+ ("command", json!("git diff --check")),
161
+ ("cwd", json!(".")),
162
+ ("exitCode", json!(0)),
163
+ ("checkedAt", json!("2026-05-08T00:00:03.000Z")),
164
+ ("evidence", json!(["README.md"])),
165
+ ])
166
+ }
167
+
168
+ fn compact_proof_batch() -> Value {
169
+ object([
170
+ ("checkedAt", json!("2026-05-08T00:00:03.000Z")),
171
+ ("evidencePathSet", json!("changed")),
172
+ (
173
+ "proofs",
174
+ json!([{ "checkId": "diff-check", "exitCode": 0 }]),
175
+ ),
176
+ ])
177
+ }
178
+
179
+ fn admission_record(git_head: &str) -> Value {
180
+ object([
181
+ (
182
+ "command",
183
+ json!("node .naome/bin/check-task-state.js --admission"),
184
+ ),
185
+ ("cwd", json!(".")),
186
+ ("exitCode", json!(0)),
187
+ ("checkedAt", json!("2026-05-08T00:00:00.000Z")),
188
+ ("gitHead", json!(git_head)),
189
+ ("changedPaths", json!([])),
190
+ ])
191
+ }
192
+
193
+ fn user_prompt() -> Value {
194
+ object([
195
+ ("receivedAt", json!("2026-05-08T00:00:00.000Z")),
196
+ ("text", json!("Update README.")),
197
+ ])
198
+ }
199
+
200
+ fn human_review() -> Value {
201
+ object([
202
+ ("required", json!(false)),
203
+ ("approved", json!(false)),
204
+ ("reason", Value::Null),
205
+ ])
206
+ }
@@ -275,7 +275,44 @@ fn commit_gate_ignores_unstaged_user_edits_outside_completed_task_scope() {
275
275
  repo.init_git();
276
276
  repo.write("README.md", "# Task result\n");
277
277
  repo.write("USER.md", "user local edit\n");
278
- repo.git(["add", "README.md", ".naome/task-state.json"]);
278
+ repo.git(["add", "README.md"]);
279
+
280
+ let report = commit_gate_report(&repo);
281
+
282
+ assert!(report.errors.is_empty(), "{:#?}", report.errors);
283
+ }
284
+
285
+ #[test]
286
+ fn commit_gate_rejects_forced_local_task_state_projection_commit() {
287
+ let repo = TaskFixture::new(complete_task_state(json!({
288
+ "allowedPaths": ["README.md"],
289
+ "proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
290
+ })));
291
+ repo.write(".gitignore", ".naome/task-state.json\n");
292
+ repo.write("README.md", "# Baseline\n");
293
+ repo.init_git();
294
+ repo.write("README.md", "# Task result\n");
295
+ repo.git(["add", "README.md"]);
296
+ repo.git(["add", "-f", ".naome/task-state.json"]);
297
+
298
+ let report = commit_gate_report(&repo);
299
+ let joined = report.errors.join("\n");
300
+
301
+ assert!(joined.contains("local-only runtime path"));
302
+ assert!(joined.contains(".naome/task-state.json"));
303
+ }
304
+
305
+ #[test]
306
+ fn commit_gate_allows_local_task_state_projection_untrack_deletion() {
307
+ let repo = TaskFixture::new(complete_task_state(json!({
308
+ "allowedPaths": ["README.md"],
309
+ "proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
310
+ })));
311
+ repo.write("README.md", "# Baseline\n");
312
+ repo.init_git();
313
+ repo.write("README.md", "# Task result\n");
314
+ repo.git(["add", "README.md"]);
315
+ repo.git(["rm", "--cached", "-q", "--", ".naome/task-state.json"]);
279
316
 
280
317
  let report = commit_gate_report(&repo);
281
318