@lamentis/naome 1.0.2 → 1.1.0

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 (34) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +8 -1
  3. package/bin/naome-node.js +3 -1
  4. package/bin/naome.js +198 -3
  5. package/crates/naome-cli/Cargo.toml +1 -1
  6. package/crates/naome-cli/src/main.rs +110 -13
  7. package/crates/naome-core/Cargo.toml +1 -1
  8. package/crates/naome-core/src/decision.rs +82 -11
  9. package/crates/naome-core/src/git.rs +12 -1
  10. package/crates/naome-core/src/harness_health.rs +3 -1
  11. package/crates/naome-core/src/install_plan.rs +4 -2
  12. package/crates/naome-core/src/intent.rs +914 -0
  13. package/crates/naome-core/src/journal.rs +169 -0
  14. package/crates/naome-core/src/lib.rs +10 -1
  15. package/crates/naome-core/src/models.rs +63 -4
  16. package/crates/naome-core/src/route.rs +1000 -0
  17. package/crates/naome-core/src/task_state.rs +326 -21
  18. package/crates/naome-core/tests/decision.rs +8 -6
  19. package/crates/naome-core/tests/install_plan.rs +9 -1
  20. package/crates/naome-core/tests/intent.rs +826 -0
  21. package/crates/naome-core/tests/route.rs +1108 -0
  22. package/crates/naome-core/tests/task_state.rs +63 -4
  23. package/native/darwin-arm64/naome +0 -0
  24. package/native/linux-x64/naome +0 -0
  25. package/package.json +1 -1
  26. package/templates/naome-root/.naome/bin/check-harness-health.js +7 -6
  27. package/templates/naome-root/.naome/bin/check-task-state.js +7 -6
  28. package/templates/naome-root/.naome/bin/naome.js +143 -13
  29. package/templates/naome-root/.naome/manifest.json +8 -7
  30. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  31. package/templates/naome-root/AGENTS.md +30 -5
  32. package/templates/naome-root/docs/naome/agent-workflow.md +45 -24
  33. package/templates/naome-root/docs/naome/execution.md +55 -51
  34. package/templates/naome-root/docs/naome/index.md +10 -3
@@ -0,0 +1,826 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+ use std::time::{SystemTime, UNIX_EPOCH};
5
+
6
+ use naome_core::{evaluate_intent, EvaluationOptions};
7
+ use serde_json::json;
8
+
9
+ #[test]
10
+ fn clean_repo_with_new_goal_creates_task() {
11
+ let repo = TestRepo::new("intent-clean-new-task");
12
+ repo.init_git();
13
+ repo.write_file("README.md", "# Baseline\n");
14
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
15
+ repo.git(&["add", "."]);
16
+ repo.git(&["commit", "-m", "baseline"]);
17
+
18
+ let intent = evaluate_intent(
19
+ repo.path(),
20
+ "Bitte implementiere eine neue kleine Funktion.",
21
+ EvaluationOptions::offline(),
22
+ )
23
+ .unwrap();
24
+
25
+ assert_eq!(intent.repo_state, "ready_for_task");
26
+ assert_eq!(intent.prompt_intent, "new_task");
27
+ assert_eq!(intent.policy_action, "create_new_task");
28
+ assert!(intent.allowed);
29
+ }
30
+
31
+ #[test]
32
+ fn completed_setup_diff_and_new_goal_auto_baselines_before_new_task() {
33
+ let repo = TestRepo::new("intent-setup-new-task");
34
+ repo.init_git();
35
+ repo.write_file("README.md", "# Baseline\n");
36
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
37
+ repo.write_naome_json(
38
+ "manifest.json",
39
+ json!({
40
+ "machineOwned": ["AGENTS.md"],
41
+ "projectOwned": [
42
+ ".naome/init-state.json",
43
+ ".naome/task-state.json",
44
+ ".naome/upgrade-state.json",
45
+ ".naome/verification.json",
46
+ "docs/naome/**"
47
+ ],
48
+ "integrity": {}
49
+ }),
50
+ );
51
+ repo.git(&["add", "."]);
52
+ repo.git(&["commit", "-m", "baseline"]);
53
+ repo.write_naome_json(
54
+ "init-state.json",
55
+ json!({
56
+ "initialized": true,
57
+ "intakeStatus": "complete",
58
+ "initializedBy": "codex"
59
+ }),
60
+ );
61
+
62
+ let intent = evaluate_intent(
63
+ repo.path(),
64
+ "Please add a README summary as a new task.",
65
+ EvaluationOptions::offline(),
66
+ )
67
+ .unwrap();
68
+
69
+ assert_eq!(intent.repo_state, "install_or_upgrade_unbaselined");
70
+ assert_eq!(intent.prompt_intent, "new_task");
71
+ assert_eq!(
72
+ intent.policy_action,
73
+ "auto_commit_upgrade_baseline_then_create_new_task"
74
+ );
75
+ assert!(intent.allowed);
76
+ }
77
+
78
+ #[test]
79
+ fn active_task_with_distinct_new_goal_blocks_instead_of_continuing() {
80
+ let repo = TestRepo::new("intent-active-new-task");
81
+ repo.init_git();
82
+ repo.write_file("README.md", "# Baseline\n");
83
+ repo.write_base_naome_state(active_task_state("implementing"));
84
+ repo.git(&["add", "."]);
85
+ repo.git(&["commit", "-m", "baseline"]);
86
+
87
+ let intent = evaluate_intent(
88
+ repo.path(),
89
+ "Jetzt implementiere bitte noch ein neues Release-System.",
90
+ EvaluationOptions::offline(),
91
+ )
92
+ .unwrap();
93
+
94
+ assert_eq!(intent.repo_state, "active_task_in_progress");
95
+ assert_eq!(intent.prompt_intent, "new_task");
96
+ assert_eq!(intent.policy_action, "block_ambiguous_intent");
97
+ assert!(!intent.allowed);
98
+ }
99
+
100
+ #[test]
101
+ fn active_task_with_correction_continues_current_task() {
102
+ let repo = TestRepo::new("intent-active-correction");
103
+ repo.init_git();
104
+ repo.write_file("README.md", "# Baseline\n");
105
+ repo.write_base_naome_state(active_task_state("implementing"));
106
+ repo.git(&["add", "."]);
107
+ repo.git(&["commit", "-m", "baseline"]);
108
+
109
+ let intent = evaluate_intent(
110
+ repo.path(),
111
+ "Mach bitte diese Kleinigkeit im aktuellen Task auch noch.",
112
+ EvaluationOptions::offline(),
113
+ )
114
+ .unwrap();
115
+
116
+ assert_eq!(intent.repo_state, "active_task_in_progress");
117
+ assert_eq!(intent.prompt_intent, "task_revision");
118
+ assert_eq!(intent.policy_action, "continue_current_task");
119
+ assert!(intent.allowed);
120
+ }
121
+
122
+ #[test]
123
+ fn completed_valid_task_and_new_goal_can_auto_baseline_then_create() {
124
+ let repo = TestRepo::new("intent-complete-new-task");
125
+ repo.init_git();
126
+ repo.write_file("README.md", "# Baseline\n");
127
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
128
+ repo.git(&["add", "."]);
129
+ repo.git(&["commit", "-m", "baseline"]);
130
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
131
+ repo.write_base_naome_state(completed_task_state(&admission_head, true));
132
+ repo.git(&["add", ".naome/task-state.json"]);
133
+ repo.git(&["commit", "-m", "task state"]);
134
+ repo.write_file("README.md", "# Changed\n");
135
+
136
+ let intent = evaluate_intent(
137
+ repo.path(),
138
+ "Danach implementiere bitte einen neuen Task.",
139
+ EvaluationOptions::offline(),
140
+ )
141
+ .unwrap();
142
+
143
+ assert_eq!(intent.repo_state, "completed_task_unbaselined");
144
+ assert_eq!(intent.prompt_intent, "new_task");
145
+ assert_eq!(
146
+ intent.policy_action,
147
+ "auto_commit_completed_task_then_create_new_task"
148
+ );
149
+ assert!(intent.allowed);
150
+ }
151
+
152
+ #[test]
153
+ fn completed_valid_task_with_unrelated_user_edit_creates_isolated_worktree() {
154
+ let repo = TestRepo::new("intent-complete-new-task-with-user-edit");
155
+ repo.init_git();
156
+ repo.write_file("README.md", "# Baseline\n");
157
+ repo.write_file("USER.md", "user baseline\n");
158
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
159
+ repo.git(&["add", "."]);
160
+ repo.git(&["commit", "-m", "baseline"]);
161
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
162
+ repo.write_base_naome_state(completed_task_state(&admission_head, true));
163
+ repo.git(&["add", ".naome/task-state.json"]);
164
+ repo.git(&["commit", "-m", "task state"]);
165
+ repo.write_file("README.md", "# Changed\n");
166
+ repo.write_file("USER.md", "user local edit\n");
167
+
168
+ let intent = evaluate_intent(
169
+ repo.path(),
170
+ "Add another line to README as a new task.",
171
+ EvaluationOptions::offline(),
172
+ )
173
+ .unwrap();
174
+
175
+ assert_eq!(intent.repo_state, "completed_task_unbaselined");
176
+ assert_eq!(intent.prompt_intent, "new_task");
177
+ assert_eq!(
178
+ intent.policy_action,
179
+ "auto_commit_completed_task_then_create_isolated_task_worktree"
180
+ );
181
+ assert!(intent.allowed);
182
+ assert!(intent.human_options.is_empty());
183
+ assert!(intent.user_message.contains("isolated worktree"));
184
+ }
185
+
186
+ #[test]
187
+ fn dirty_repo_with_harness_refresh_and_user_edit_baselines_refresh_before_worktree() {
188
+ let repo = TestRepo::new("intent-dirty-harness-refresh-with-user-edit");
189
+ repo.init_git();
190
+ repo.write_file("README.md", "# Baseline\n");
191
+ repo.write_file("USER.md", "user baseline\n");
192
+ repo.write_file("AGENTS.md", "# Agent Instructions\n\nOld harness.\n");
193
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
194
+ repo.write_naome_json(
195
+ "manifest.json",
196
+ json!({
197
+ "name": "naome",
198
+ "harnessVersion": "1.1.0",
199
+ "profile": "standard",
200
+ "machineOwned": ["AGENTS.md"],
201
+ "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
202
+ "integrity": {}
203
+ }),
204
+ );
205
+ repo.git(&["add", "."]);
206
+ repo.git(&["commit", "-m", "baseline"]);
207
+ repo.write_file("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
208
+ repo.write_naome_json(
209
+ "manifest.json",
210
+ json!({
211
+ "name": "naome",
212
+ "harnessVersion": "1.1.0",
213
+ "profile": "standard",
214
+ "machineOwned": ["AGENTS.md"],
215
+ "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
216
+ "integrity": {}
217
+ }),
218
+ );
219
+ repo.write_file("USER.md", "user local edit\n");
220
+
221
+ let intent = evaluate_intent(
222
+ repo.path(),
223
+ "Add another line to README as a new task.",
224
+ EvaluationOptions::offline(),
225
+ )
226
+ .unwrap();
227
+
228
+ assert_eq!(intent.repo_state, "dirty_unowned_diff");
229
+ assert_eq!(
230
+ intent.policy_action,
231
+ "auto_commit_harness_refresh_then_create_isolated_task_worktree"
232
+ );
233
+ assert!(intent.allowed);
234
+ assert!(intent.human_options.is_empty());
235
+ }
236
+
237
+ #[test]
238
+ fn completed_task_state_with_only_harness_refresh_baselines_before_new_task() {
239
+ let repo = TestRepo::new("intent-complete-pure-harness-refresh");
240
+ repo.init_git();
241
+ repo.write_file("README.md", "# Baseline\n");
242
+ repo.write_file("AGENTS.md", "# Agent Instructions\n\nOld harness.\n");
243
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
244
+ repo.write_naome_json(
245
+ "manifest.json",
246
+ json!({
247
+ "name": "naome",
248
+ "harnessVersion": "1.1.0",
249
+ "profile": "old",
250
+ "machineOwned": ["AGENTS.md"],
251
+ "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
252
+ "integrity": {}
253
+ }),
254
+ );
255
+ repo.git(&["add", "."]);
256
+ repo.git(&["commit", "-m", "baseline"]);
257
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
258
+ repo.write_base_naome_state(completed_task_state(&admission_head, true));
259
+ repo.git(&["add", ".naome/task-state.json"]);
260
+ repo.git(&["commit", "-m", "task state"]);
261
+ repo.write_file("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
262
+ repo.write_naome_json(
263
+ "manifest.json",
264
+ json!({
265
+ "name": "naome",
266
+ "harnessVersion": "1.1.0",
267
+ "profile": "standard",
268
+ "machineOwned": ["AGENTS.md"],
269
+ "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
270
+ "integrity": {}
271
+ }),
272
+ );
273
+
274
+ let intent = evaluate_intent(
275
+ repo.path(),
276
+ "Add another line to README as a new task.",
277
+ EvaluationOptions::offline(),
278
+ )
279
+ .unwrap();
280
+
281
+ assert_eq!(intent.repo_state, "harness_repair_unbaselined");
282
+ assert_eq!(intent.prompt_intent, "new_task");
283
+ assert_eq!(
284
+ intent.policy_action,
285
+ "auto_commit_harness_refresh_then_create_new_task"
286
+ );
287
+ assert!(intent.allowed);
288
+ assert!(intent.human_options.is_empty());
289
+ assert!(!intent.user_message.contains("isolated worktree"));
290
+ }
291
+
292
+ #[test]
293
+ fn completed_task_state_with_manifest_only_diff_is_not_harness_refresh() {
294
+ let repo = TestRepo::new("intent-complete-manifest-only-diff");
295
+ repo.init_git();
296
+ repo.write_file("README.md", "# Baseline\n");
297
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
298
+ repo.write_naome_json(
299
+ "manifest.json",
300
+ json!({
301
+ "name": "naome",
302
+ "harnessVersion": "1.1.0",
303
+ "profile": "standard",
304
+ "machineOwned": ["AGENTS.md"],
305
+ "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
306
+ "integrity": {}
307
+ }),
308
+ );
309
+ repo.git(&["add", "."]);
310
+ repo.git(&["commit", "-m", "baseline"]);
311
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
312
+ repo.write_base_naome_state(completed_task_state(&admission_head, true));
313
+ repo.git(&["add", ".naome/task-state.json"]);
314
+ repo.git(&["commit", "-m", "task state"]);
315
+ repo.write_naome_json(
316
+ "manifest.json",
317
+ json!({
318
+ "name": "naome",
319
+ "harnessVersion": "9.9.9",
320
+ "profile": "standard",
321
+ "machineOwned": ["AGENTS.md"],
322
+ "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
323
+ "integrity": {}
324
+ }),
325
+ );
326
+
327
+ let intent = evaluate_intent(
328
+ repo.path(),
329
+ "Add another line to README as a new task.",
330
+ EvaluationOptions::offline(),
331
+ )
332
+ .unwrap();
333
+
334
+ assert_eq!(intent.repo_state, "dirty_unowned_diff");
335
+ assert_eq!(intent.policy_action, "create_isolated_task_worktree");
336
+ }
337
+
338
+ #[test]
339
+ fn dirty_repo_repair_request_baselines_only_harness_refresh() {
340
+ let repo = TestRepo::new("intent-dirty-harness-refresh-repair-request");
341
+ repo.init_git();
342
+ repo.write_file("README.md", "# Baseline\n");
343
+ repo.write_file("USER.md", "user baseline\n");
344
+ repo.write_file("AGENTS.md", "# Agent Instructions\n\nOld harness.\n");
345
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
346
+ repo.write_naome_json(
347
+ "manifest.json",
348
+ json!({
349
+ "name": "naome",
350
+ "harnessVersion": "1.1.0",
351
+ "profile": "standard",
352
+ "machineOwned": ["AGENTS.md"],
353
+ "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
354
+ "integrity": {}
355
+ }),
356
+ );
357
+ repo.git(&["add", "."]);
358
+ repo.git(&["commit", "-m", "baseline"]);
359
+ repo.write_file("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
360
+ repo.write_naome_json(
361
+ "manifest.json",
362
+ json!({
363
+ "name": "naome",
364
+ "harnessVersion": "1.1.0",
365
+ "profile": "standard",
366
+ "machineOwned": ["AGENTS.md"],
367
+ "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
368
+ "integrity": {}
369
+ }),
370
+ );
371
+ repo.write_file("USER.md", "user local edit\n");
372
+
373
+ let intent = evaluate_intent(
374
+ repo.path(),
375
+ "please repair all",
376
+ EvaluationOptions::offline(),
377
+ )
378
+ .unwrap();
379
+
380
+ assert_eq!(intent.repo_state, "dirty_unowned_diff");
381
+ assert_eq!(intent.prompt_intent, "repair_request");
382
+ assert_eq!(intent.policy_action, "auto_commit_harness_refresh_baseline");
383
+ assert!(intent.allowed);
384
+ assert!(intent.human_options.is_empty());
385
+ }
386
+
387
+ #[test]
388
+ fn dirty_unowned_commit_request_does_not_offer_clear_or_commit_fallback() {
389
+ let repo = TestRepo::new("intent-dirty-commit-request");
390
+ repo.init_git();
391
+ repo.write_file("README.md", "# Baseline\n");
392
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
393
+ repo.git(&["add", "."]);
394
+ repo.git(&["commit", "-m", "baseline"]);
395
+ repo.write_file("README.md", "# Manual edit\n");
396
+
397
+ let intent =
398
+ evaluate_intent(repo.path(), "okay commit it", EvaluationOptions::offline()).unwrap();
399
+
400
+ assert_eq!(intent.repo_state, "dirty_unowned_diff");
401
+ assert_eq!(intent.prompt_intent, "commit_request");
402
+ assert_eq!(intent.policy_action, "commit_user_diff_with_quality_gate");
403
+ assert!(intent.allowed);
404
+ assert!(!intent
405
+ .human_options
406
+ .contains(&"clear_or_commit_unowned_diff".to_string()));
407
+ assert!(intent.user_message.contains("quality gate"));
408
+ }
409
+
410
+ #[test]
411
+ fn unsafe_terms_win_over_commit_and_new_task_requests() {
412
+ let repo = TestRepo::new("intent-unsafe-priority");
413
+ repo.init_git();
414
+ repo.write_file("README.md", "# Baseline\n");
415
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
416
+ repo.git(&["add", "."]);
417
+ repo.git(&["commit", "-m", "baseline"]);
418
+
419
+ let intent = evaluate_intent(
420
+ repo.path(),
421
+ "Create a new task, commit it, then push with --no-verify and include this API key.",
422
+ EvaluationOptions::offline(),
423
+ )
424
+ .unwrap();
425
+
426
+ assert_eq!(intent.prompt_intent, "unsafe");
427
+ assert_eq!(intent.policy_action, "block_unsafe_intent");
428
+ assert!(!intent.allowed);
429
+ assert!(!intent.human_options.is_empty());
430
+ assert!(intent
431
+ .internal_notes
432
+ .contains(&"winning_rule:unsafe_intent_precedence".to_string()));
433
+ }
434
+
435
+ #[test]
436
+ fn do_not_commit_blocks_completed_task_auto_baseline() {
437
+ let repo = TestRepo::new("intent-no-commit-priority");
438
+ repo.init_git();
439
+ repo.write_file("README.md", "# Baseline\n");
440
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
441
+ repo.git(&["add", "."]);
442
+ repo.git(&["commit", "-m", "baseline"]);
443
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
444
+ repo.write_base_naome_state(completed_task_state(&admission_head, true));
445
+ repo.git(&["add", ".naome/task-state.json"]);
446
+ repo.git(&["commit", "-m", "task state"]);
447
+ repo.write_file("README.md", "# Changed\n");
448
+
449
+ let intent = evaluate_intent(
450
+ repo.path(),
451
+ "Do not commit. Please start a new task after this.",
452
+ EvaluationOptions::offline(),
453
+ )
454
+ .unwrap();
455
+
456
+ assert_eq!(intent.prompt_intent, "no_commit_request");
457
+ assert_eq!(intent.policy_action, "block_auto_baseline_due_to_no_commit");
458
+ assert!(!intent.allowed);
459
+ assert!(intent
460
+ .human_options
461
+ .contains(&"review_task_diff".to_string()));
462
+ }
463
+
464
+ #[test]
465
+ fn explicit_review_request_overrides_auto_baseline() {
466
+ let repo = TestRepo::new("intent-review-priority");
467
+ repo.init_git();
468
+ repo.write_file("README.md", "# Baseline\n");
469
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
470
+ repo.git(&["add", "."]);
471
+ repo.git(&["commit", "-m", "baseline"]);
472
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
473
+ repo.write_base_naome_state(completed_task_state(&admission_head, true));
474
+ repo.git(&["add", ".naome/task-state.json"]);
475
+ repo.git(&["commit", "-m", "task state"]);
476
+ repo.write_file("README.md", "# Changed\n");
477
+
478
+ let intent = evaluate_intent(
479
+ repo.path(),
480
+ "Review the current task diff, then also create a new task.",
481
+ EvaluationOptions::offline(),
482
+ )
483
+ .unwrap();
484
+
485
+ assert_eq!(intent.prompt_intent, "review_request");
486
+ assert_eq!(intent.policy_action, "review_task_diff");
487
+ assert!(!intent.allowed);
488
+ assert!(intent
489
+ .human_options
490
+ .contains(&"commit_task_baseline".to_string()));
491
+ }
492
+
493
+ #[test]
494
+ fn explicit_cancel_request_overrides_auto_baseline() {
495
+ let repo = TestRepo::new("intent-cancel-priority");
496
+ repo.init_git();
497
+ repo.write_file("README.md", "# Baseline\n");
498
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
499
+ repo.git(&["add", "."]);
500
+ repo.git(&["commit", "-m", "baseline"]);
501
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
502
+ repo.write_base_naome_state(completed_task_state(&admission_head, true));
503
+ repo.git(&["add", ".naome/task-state.json"]);
504
+ repo.git(&["commit", "-m", "task state"]);
505
+ repo.write_file("README.md", "# Changed\n");
506
+
507
+ let intent = evaluate_intent(
508
+ repo.path(),
509
+ "Cancel the current task changes, but also start a new task.",
510
+ EvaluationOptions::offline(),
511
+ )
512
+ .unwrap();
513
+
514
+ assert_eq!(intent.prompt_intent, "cancel_request");
515
+ assert_eq!(intent.policy_action, "cancel_task_changes");
516
+ assert!(!intent.allowed);
517
+ assert!(intent
518
+ .human_options
519
+ .contains(&"review_task_diff".to_string()));
520
+ }
521
+
522
+ #[test]
523
+ fn explicit_new_task_wins_over_vague_continuation_wording() {
524
+ let repo = TestRepo::new("intent-new-over-vague-continuation");
525
+ repo.init_git();
526
+ repo.write_file("README.md", "# Baseline\n");
527
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
528
+ repo.git(&["add", "."]);
529
+ repo.git(&["commit", "-m", "baseline"]);
530
+
531
+ let intent = evaluate_intent(
532
+ repo.path(),
533
+ "Als neuer Task: mach bitte auch noch eine kurze README Ergänzung.",
534
+ EvaluationOptions::offline(),
535
+ )
536
+ .unwrap();
537
+
538
+ assert_eq!(intent.prompt_intent, "new_task");
539
+ assert_eq!(intent.policy_action, "create_new_task");
540
+ assert!(intent.allowed);
541
+ }
542
+
543
+ #[test]
544
+ fn status_requests_do_not_offer_mutating_human_options() {
545
+ let repo = TestRepo::new("intent-status-readonly");
546
+ repo.init_git();
547
+ repo.write_file("README.md", "# Baseline\n");
548
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
549
+ repo.git(&["add", "."]);
550
+ repo.git(&["commit", "-m", "baseline"]);
551
+
552
+ let intent = evaluate_intent(
553
+ repo.path(),
554
+ "What is the current NAOME status and why?",
555
+ EvaluationOptions::offline(),
556
+ )
557
+ .unwrap();
558
+
559
+ assert_eq!(intent.prompt_intent, "status_question");
560
+ assert_eq!(intent.policy_action, "answer_status_only");
561
+ assert!(intent.allowed);
562
+ assert!(intent.human_options.is_empty());
563
+ assert!(!intent.user_message.contains("commit_task_baseline"));
564
+ }
565
+
566
+ #[test]
567
+ fn completed_invalid_task_and_new_goal_blocks_auto_baseline() {
568
+ let repo = TestRepo::new("intent-invalid-complete-new-task");
569
+ repo.init_git();
570
+ repo.write_file("README.md", "# Baseline\n");
571
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
572
+ repo.git(&["add", "."]);
573
+ repo.git(&["commit", "-m", "baseline"]);
574
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
575
+ repo.write_base_naome_state(completed_task_state(&admission_head, false));
576
+ repo.git(&["add", ".naome/task-state.json"]);
577
+ repo.git(&["commit", "-m", "task state"]);
578
+ repo.write_file("README.md", "# Changed\n");
579
+
580
+ let intent = evaluate_intent(
581
+ repo.path(),
582
+ "Danach implementiere bitte einen neuen Task.",
583
+ EvaluationOptions::offline(),
584
+ )
585
+ .unwrap();
586
+
587
+ assert_eq!(intent.repo_state, "completed_task_unbaselined");
588
+ assert_eq!(intent.policy_action, "block_unsafe_intent");
589
+ assert!(!intent.allowed);
590
+ assert!(intent
591
+ .risk_codes
592
+ .contains(&"completed_task_proof_invalid".to_string()));
593
+ }
594
+
595
+ #[test]
596
+ fn completed_task_correction_reopens_same_task_revision() {
597
+ let repo = TestRepo::new("intent-complete-correction");
598
+ repo.init_git();
599
+ repo.write_file("README.md", "# Baseline\n");
600
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
601
+ repo.git(&["add", "."]);
602
+ repo.git(&["commit", "-m", "baseline"]);
603
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
604
+ repo.write_base_naome_state(completed_task_state(&admission_head, true));
605
+ repo.git(&["add", ".naome/task-state.json"]);
606
+ repo.git(&["commit", "-m", "task state"]);
607
+ repo.write_file("README.md", "# Changed\n");
608
+
609
+ let intent = evaluate_intent(
610
+ repo.path(),
611
+ "Bitte im gleichen Task noch eine Kleinigkeit korrigieren.",
612
+ EvaluationOptions::offline(),
613
+ )
614
+ .unwrap();
615
+
616
+ assert_eq!(intent.prompt_intent, "task_revision");
617
+ assert_eq!(intent.policy_action, "reopen_completed_task_revision");
618
+ assert!(intent.allowed);
619
+ }
620
+
621
+ #[test]
622
+ fn risky_prompt_without_clear_task_transition_blocks_unsafe() {
623
+ let repo = TestRepo::new("intent-risky-unclear");
624
+ repo.init_git();
625
+ repo.write_file("README.md", "# Baseline\n");
626
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
627
+ repo.git(&["add", "."]);
628
+ repo.git(&["commit", "-m", "baseline"]);
629
+
630
+ let intent = evaluate_intent(
631
+ repo.path(),
632
+ "Hier ist ein API key und bitte --no-verify.",
633
+ EvaluationOptions::offline(),
634
+ )
635
+ .unwrap();
636
+
637
+ assert_eq!(intent.prompt_intent, "unsafe");
638
+ assert_eq!(intent.policy_action, "block_unsafe_intent");
639
+ assert!(!intent.allowed);
640
+ }
641
+
642
+ fn active_task_state(status: &str) -> serde_json::Value {
643
+ json!({
644
+ "schema": "naome.task-state.v1",
645
+ "version": 1,
646
+ "status": status,
647
+ "activeTask": {
648
+ "id": "readme-task",
649
+ "request": "Change README.",
650
+ "userPrompt": {
651
+ "receivedAt": "2026-05-06T00:00:00.000Z",
652
+ "text": "Change README."
653
+ },
654
+ "admission": {
655
+ "command": "node .naome/bin/check-task-state.js --admission",
656
+ "cwd": ".",
657
+ "exitCode": 0,
658
+ "checkedAt": "2026-05-06T00:00:00.000Z",
659
+ "gitHead": "",
660
+ "changedPaths": []
661
+ },
662
+ "allowedPaths": ["README.md"],
663
+ "declaredChangeTypes": ["product-docs"],
664
+ "requiredCheckIds": ["diff-check"],
665
+ "proofResults": [],
666
+ "revisions": [],
667
+ "humanReview": {
668
+ "required": false,
669
+ "approved": false,
670
+ "reason": null
671
+ }
672
+ },
673
+ "blocker": null,
674
+ "updatedAt": "2026-05-06T00:00:00.000Z"
675
+ })
676
+ }
677
+
678
+ fn completed_task_state(admission_head: &str, include_proof: bool) -> serde_json::Value {
679
+ let proof_results = if include_proof {
680
+ json!([
681
+ {
682
+ "checkId": "diff-check",
683
+ "command": "git diff --check",
684
+ "cwd": ".",
685
+ "exitCode": 0,
686
+ "checkedAt": "2026-05-06T00:00:00.000Z",
687
+ "evidence": ["README.md"]
688
+ }
689
+ ])
690
+ } else {
691
+ json!([])
692
+ };
693
+
694
+ json!({
695
+ "schema": "naome.task-state.v1",
696
+ "version": 1,
697
+ "status": "complete",
698
+ "activeTask": {
699
+ "id": "readme-task",
700
+ "request": "Change README.",
701
+ "userPrompt": {
702
+ "receivedAt": "2026-05-06T00:00:00.000Z",
703
+ "text": "Change README."
704
+ },
705
+ "admission": {
706
+ "command": "node .naome/bin/check-task-state.js --admission",
707
+ "cwd": ".",
708
+ "exitCode": 0,
709
+ "checkedAt": "2026-05-06T00:00:00.000Z",
710
+ "gitHead": admission_head,
711
+ "changedPaths": []
712
+ },
713
+ "allowedPaths": ["README.md"],
714
+ "declaredChangeTypes": ["product-docs"],
715
+ "requiredCheckIds": ["diff-check"],
716
+ "proofResults": proof_results,
717
+ "revisions": [],
718
+ "humanReview": {
719
+ "required": false,
720
+ "approved": false,
721
+ "reason": null
722
+ }
723
+ },
724
+ "blocker": null,
725
+ "updatedAt": "2026-05-06T00:00:00.000Z"
726
+ })
727
+ }
728
+
729
+ struct TestRepo {
730
+ root: PathBuf,
731
+ }
732
+
733
+ impl TestRepo {
734
+ fn new(name: &str) -> Self {
735
+ let nonce = SystemTime::now()
736
+ .duration_since(UNIX_EPOCH)
737
+ .unwrap()
738
+ .as_nanos();
739
+ let root = std::env::temp_dir().join(format!("naome-intent-{name}-{nonce}"));
740
+ fs::create_dir_all(root.join(".naome")).unwrap();
741
+ Self { root }
742
+ }
743
+
744
+ fn path(&self) -> &Path {
745
+ &self.root
746
+ }
747
+
748
+ fn write_file(&self, relative_path: &str, content: &str) {
749
+ let path = self.root.join(relative_path);
750
+ if let Some(parent) = path.parent() {
751
+ fs::create_dir_all(parent).unwrap();
752
+ }
753
+ fs::write(path, content).unwrap();
754
+ }
755
+
756
+ fn write_base_naome_state(&self, task_state: serde_json::Value) {
757
+ self.write_naome_json(
758
+ "init-state.json",
759
+ json!({ "initialized": true, "intakeStatus": "complete" }),
760
+ );
761
+ self.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
762
+ self.write_naome_json(
763
+ "verification.json",
764
+ json!({
765
+ "schema": "naome.verification.v1",
766
+ "version": 1,
767
+ "status": "ready",
768
+ "checks": [
769
+ {
770
+ "id": "diff-check",
771
+ "command": "git diff --check",
772
+ "cwd": ".",
773
+ "purpose": "Reject whitespace errors.",
774
+ "cost": "fast",
775
+ "source": "test",
776
+ "evidence": ["README.md"],
777
+ "lastVerified": null
778
+ }
779
+ ],
780
+ "changeTypes": [],
781
+ "releaseGates": []
782
+ }),
783
+ );
784
+ self.write_naome_json("task-state.json", task_state);
785
+ }
786
+
787
+ fn write_naome_json(&self, file_name: &str, value: serde_json::Value) {
788
+ let path = self.root.join(".naome").join(file_name);
789
+ fs::write(
790
+ path,
791
+ format!("{}\n", serde_json::to_string_pretty(&value).unwrap()),
792
+ )
793
+ .unwrap();
794
+ }
795
+
796
+ fn init_git(&self) {
797
+ self.git(&["init"]);
798
+ self.git(&["config", "user.email", "naome@example.com"]);
799
+ self.git(&["config", "user.name", "NAOME Test"]);
800
+ }
801
+
802
+ fn git(&self, args: &[&str]) {
803
+ let output = Command::new("git")
804
+ .args(args)
805
+ .current_dir(&self.root)
806
+ .output()
807
+ .unwrap();
808
+ assert!(
809
+ output.status.success(),
810
+ "git {:?} failed\nstdout:\n{}\nstderr:\n{}",
811
+ args,
812
+ String::from_utf8_lossy(&output.stdout),
813
+ String::from_utf8_lossy(&output.stderr)
814
+ );
815
+ }
816
+
817
+ fn git_stdout(&self, args: &[&str]) -> String {
818
+ let output = Command::new("git")
819
+ .args(args)
820
+ .current_dir(&self.root)
821
+ .output()
822
+ .unwrap();
823
+ assert!(output.status.success());
824
+ String::from_utf8_lossy(&output.stdout).trim().to_string()
825
+ }
826
+ }