@lamentis/naome 1.2.0 → 1.3.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 (139) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +108 -47
  3. package/bin/naome-node.js +2 -1579
  4. package/bin/naome.js +34 -5
  5. package/crates/naome-cli/Cargo.toml +1 -1
  6. package/crates/naome-cli/src/dispatcher.rs +7 -2
  7. package/crates/naome-cli/src/main.rs +37 -22
  8. package/crates/naome-cli/src/quality_commands.rs +317 -10
  9. package/crates/naome-cli/src/workflow_commands.rs +21 -1
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/decision/checks.rs +64 -0
  12. package/crates/naome-core/src/decision/idle.rs +67 -0
  13. package/crates/naome-core/src/decision/json.rs +36 -0
  14. package/crates/naome-core/src/decision/states.rs +165 -0
  15. package/crates/naome-core/src/decision.rs +131 -353
  16. package/crates/naome-core/src/git.rs +4 -2
  17. package/crates/naome-core/src/install_plan.rs +4 -0
  18. package/crates/naome-core/src/lib.rs +12 -6
  19. package/crates/naome-core/src/paths.rs +3 -1
  20. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  21. package/crates/naome-core/src/quality/adapters.rs +20 -67
  22. package/crates/naome-core/src/quality/baseline.rs +8 -0
  23. package/crates/naome-core/src/quality/cache.rs +153 -0
  24. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +25 -11
  25. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  26. package/crates/naome-core/src/quality/checks.rs +7 -8
  27. package/crates/naome-core/src/quality/cleanup.rs +48 -3
  28. package/crates/naome-core/src/quality/config.rs +8 -15
  29. package/crates/naome-core/src/quality/config_support.rs +24 -0
  30. package/crates/naome-core/src/quality/mod.rs +72 -6
  31. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  32. package/crates/naome-core/src/quality/scanner/analysis.rs +160 -0
  33. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  34. package/crates/naome-core/src/quality/scanner.rs +200 -215
  35. package/crates/naome-core/src/quality/semantic/checks.rs +134 -0
  36. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  37. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  38. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  39. package/crates/naome-core/src/quality/semantic.rs +68 -0
  40. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  41. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  42. package/crates/naome-core/src/quality/structure/checks/directory.rs +134 -0
  43. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  44. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  45. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  46. package/crates/naome-core/src/quality/structure/classify.rs +146 -0
  47. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  48. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  49. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  50. package/crates/naome-core/src/quality/structure/model.rs +131 -0
  51. package/crates/naome-core/src/quality/types.rs +43 -2
  52. package/crates/naome-core/src/route/builtin_checks.rs +141 -0
  53. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  54. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  55. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  56. package/crates/naome-core/src/route/context.rs +180 -0
  57. package/crates/naome-core/src/route/execution.rs +96 -0
  58. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  59. package/crates/naome-core/src/route/execution_support.rs +57 -0
  60. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  61. package/crates/naome-core/src/route/git_ops.rs +72 -0
  62. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  63. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  64. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  65. package/crates/naome-core/src/route/worktree.rs +75 -0
  66. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  67. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  68. package/crates/naome-core/src/route.rs +44 -1217
  69. package/crates/naome-core/src/verification.rs +1 -0
  70. package/crates/naome-core/src/workflow/doctor.rs +144 -0
  71. package/crates/naome-core/src/workflow/mod.rs +2 -0
  72. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  73. package/crates/naome-core/tests/decision.rs +24 -118
  74. package/crates/naome-core/tests/harness_health.rs +2 -0
  75. package/crates/naome-core/tests/install_plan.rs +2 -0
  76. package/crates/naome-core/tests/quality.rs +26 -123
  77. package/crates/naome-core/tests/quality_performance.rs +231 -0
  78. package/crates/naome-core/tests/quality_structure.rs +116 -0
  79. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  80. package/crates/naome-core/tests/quality_structure_policy.rs +144 -0
  81. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  82. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  83. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  84. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  85. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  86. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  87. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  88. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  89. package/crates/naome-core/tests/route.rs +1 -1376
  90. package/crates/naome-core/tests/route_baseline.rs +86 -0
  91. package/crates/naome-core/tests/route_completion.rs +141 -0
  92. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  93. package/crates/naome-core/tests/route_user_diff.rs +202 -0
  94. package/crates/naome-core/tests/route_worktree.rs +54 -0
  95. package/crates/naome-core/tests/semantic_legacy.rs +140 -0
  96. package/crates/naome-core/tests/task_state.rs +60 -432
  97. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  98. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  99. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  100. package/crates/naome-core/tests/verification.rs +4 -45
  101. package/crates/naome-core/tests/verification_contract.rs +22 -78
  102. package/crates/naome-core/tests/workflow_doctor.rs +24 -0
  103. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  104. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  105. package/installer/agents.js +90 -0
  106. package/installer/context.js +67 -0
  107. package/installer/filesystem.js +166 -0
  108. package/installer/flows.js +84 -0
  109. package/installer/git-boundary.js +171 -0
  110. package/installer/git-hook-content.js +36 -0
  111. package/installer/git-hooks.js +134 -0
  112. package/installer/git-local.js +2 -0
  113. package/installer/git-shared.js +35 -0
  114. package/installer/harness-file-ops.js +140 -0
  115. package/installer/harness-files.js +56 -0
  116. package/installer/harness-verification.js +123 -0
  117. package/installer/install-plan.js +66 -0
  118. package/installer/main.js +25 -0
  119. package/installer/manifest-state.js +167 -0
  120. package/installer/native-build.js +24 -0
  121. package/installer/native-format.js +6 -0
  122. package/installer/native.js +162 -0
  123. package/installer/output.js +131 -0
  124. package/installer/version.js +32 -0
  125. package/native/darwin-arm64/naome +0 -0
  126. package/native/linux-x64/naome +0 -0
  127. package/package.json +2 -1
  128. package/templates/naome-root/.naome/bin/check-harness-health.js +3 -3
  129. package/templates/naome-root/.naome/bin/check-task-state.js +3 -3
  130. package/templates/naome-root/.naome/bin/naome.js +32 -21
  131. package/templates/naome-root/.naome/manifest.json +5 -3
  132. package/templates/naome-root/.naome/repository-structure.json +90 -0
  133. package/templates/naome-root/.naome/verification.json +1 -0
  134. package/templates/naome-root/.naomeignore +1 -0
  135. package/templates/naome-root/docs/naome/agent-workflow.md +16 -14
  136. package/templates/naome-root/docs/naome/index.md +4 -3
  137. package/templates/naome-root/docs/naome/repository-quality.md +66 -4
  138. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  139. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -1,1376 +1 @@
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_route, explain_route, EvaluationOptions, RouteOptions};
7
- use serde_json::{json, Value};
8
-
9
- #[test]
10
- fn dry_route_reports_auto_baseline_without_mutating() {
11
- let repo = TestRepo::completed_task_with_diff("route-dry-auto");
12
- let before = repo.git_stdout(&["rev-parse", "HEAD"]);
13
-
14
- let route = evaluate_route(
15
- repo.path(),
16
- "Start a new task for README polish.",
17
- RouteOptions {
18
- execute: false,
19
- evaluation: EvaluationOptions::offline(),
20
- },
21
- )
22
- .unwrap();
23
-
24
- assert_eq!(
25
- route.policy_action,
26
- "auto_commit_completed_task_then_create_new_task"
27
- );
28
- assert!(!route.mutation_performed);
29
- assert!(!route.can_create_task);
30
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
31
- assert!(!route.user_message.contains("commit_task_baseline"));
32
- assert!(route.human_options.is_empty());
33
- }
34
-
35
- #[test]
36
- fn execute_route_auto_baselines_then_admits_next_task_and_writes_local_journal() {
37
- let repo = TestRepo::completed_task_with_diff("route-execute-auto");
38
-
39
- let route = evaluate_route(
40
- repo.path(),
41
- "Start a new task for README polish.",
42
- RouteOptions {
43
- execute: true,
44
- evaluation: EvaluationOptions::offline(),
45
- },
46
- )
47
- .unwrap();
48
-
49
- assert_eq!(
50
- route.policy_action,
51
- "auto_commit_completed_task_then_create_new_task"
52
- );
53
- assert!(route.mutation_performed);
54
- assert!(route.can_create_task);
55
- assert_eq!(route.next_decision.state, "ready_for_task");
56
- assert!(route
57
- .executed_actions
58
- .contains(&"commit_task_baseline".to_string()));
59
- assert!(repo.git_status_short().is_empty());
60
-
61
- let journal = fs::read_to_string(repo.path().join(".naome/task-journal.jsonl")).unwrap();
62
- assert!(journal.contains("\"taskId\":\"readme-task\""));
63
- assert!(journal.contains("\"outcome\":\"route_auto_baseline\""));
64
- }
65
-
66
- #[test]
67
- fn execute_route_baselines_completed_task_and_creates_worktree_for_unrelated_user_edit() {
68
- let repo = TestRepo::completed_task_with_unrelated_user_edit("route-preserve-user-edit");
69
-
70
- let route = evaluate_route(
71
- repo.path(),
72
- "Add another line to README as a new task.",
73
- RouteOptions {
74
- execute: true,
75
- evaluation: EvaluationOptions::offline(),
76
- },
77
- )
78
- .unwrap();
79
-
80
- assert_eq!(
81
- route.policy_action,
82
- "auto_commit_completed_task_then_create_isolated_task_worktree"
83
- );
84
- assert!(route.mutation_performed);
85
- assert!(route.can_create_task);
86
- assert_eq!(route.next_decision.state, "ready_for_task");
87
- assert!(route.user_message.contains("isolated task worktree"));
88
- assert!(route.human_options.is_empty());
89
- assert_ne!(route.task_root, repo.path().to_string_lossy());
90
- assert!(route.worktree.is_some());
91
- assert_eq!(repo.git_status_short(), "M USER.md");
92
- assert_eq!(
93
- TestRepo::git_status_short_at(Path::new(&route.task_root)),
94
- ""
95
- );
96
-
97
- let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
98
- assert!(committed_paths.contains("README.md"));
99
- assert!(!committed_paths.contains("USER.md"));
100
- }
101
-
102
- #[test]
103
- fn execute_route_creates_worktree_for_dirty_repo_new_task() {
104
- let repo = TestRepo::new("route-dirty-worktree");
105
- repo.init_git();
106
- repo.write_file("README.md", "# Baseline\n");
107
- repo.write_file("USER.md", "user baseline\n");
108
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
109
- repo.git(&["add", "."]);
110
- repo.git(&["commit", "-m", "baseline"]);
111
- repo.write_file("USER.md", "user local edit\n");
112
-
113
- let route = evaluate_route(
114
- repo.path(),
115
- "Add another line to README as a new task.",
116
- RouteOptions {
117
- execute: true,
118
- evaluation: EvaluationOptions::offline(),
119
- },
120
- )
121
- .unwrap();
122
-
123
- assert_eq!(route.policy_action, "create_isolated_task_worktree");
124
- assert!(route.mutation_performed);
125
- assert!(route.can_create_task);
126
- assert_eq!(route.next_decision.state, "ready_for_task");
127
- assert!(route.worktree.is_some());
128
- assert_eq!(repo.git_status_short(), "M USER.md");
129
- assert_eq!(
130
- TestRepo::git_status_short_at(Path::new(&route.task_root)),
131
- ""
132
- );
133
- }
134
-
135
- #[test]
136
- fn execute_route_does_not_mutate_or_offer_clear_commit_for_dirty_unowned_diff() {
137
- let repo = TestRepo::new("route-dirty-commit-request");
138
- repo.init_git();
139
- repo.write_file("README.md", "# Baseline\n");
140
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
141
- repo.git(&["add", "."]);
142
- repo.git(&["commit", "-m", "baseline"]);
143
- repo.write_file("README.md", "# Manual edit\n");
144
- let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
145
- let before_status = repo.git_status_short();
146
-
147
- let route = evaluate_route(
148
- repo.path(),
149
- "clear_or_commit_unowned_diff",
150
- RouteOptions {
151
- execute: true,
152
- evaluation: EvaluationOptions::offline(),
153
- },
154
- )
155
- .unwrap();
156
-
157
- assert_eq!(route.policy_action, "block_unowned_diff");
158
- assert!(!route.mutation_performed);
159
- assert!(!route.can_create_task);
160
- assert_eq!(route.executed_actions, Vec::<String>::new());
161
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
162
- assert_eq!(repo.git_status_short(), before_status);
163
- assert!(!route
164
- .human_options
165
- .contains(&"clear_or_commit_unowned_diff".to_string()));
166
- }
167
-
168
- #[test]
169
- fn execute_route_commits_user_diff_after_quality_gate_passes() {
170
- let repo = TestRepo::new("route-user-diff-quality-pass");
171
- repo.init_git();
172
- repo.write_file("README.md", "# Baseline\n");
173
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
174
- repo.write_readme_quality_verification(Vec::new(), vec!["diff-check"]);
175
- repo.git(&["add", "."]);
176
- repo.git(&["commit", "-m", "baseline"]);
177
- repo.write_file("README.md", "# Manual edit\n");
178
-
179
- let route = evaluate_route(
180
- repo.path(),
181
- "commit my changes",
182
- RouteOptions {
183
- execute: true,
184
- evaluation: EvaluationOptions::offline(),
185
- },
186
- )
187
- .unwrap();
188
-
189
- assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
190
- assert!(route.allowed);
191
- assert!(route.mutation_performed);
192
- assert_eq!(
193
- route.executed_actions,
194
- vec![
195
- "run_user_diff_quality_gate".to_string(),
196
- "commit_user_diff".to_string()
197
- ]
198
- );
199
- assert_eq!(repo.git_status_short(), "");
200
-
201
- let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
202
- assert!(committed_paths.contains("README.md"));
203
- assert!(route.user_message.contains("quality gates passed"));
204
- }
205
-
206
- #[test]
207
- fn execute_route_runs_repository_quality_check_without_shelling_to_repo_command() {
208
- let repo = TestRepo::new("route-repository-quality-check");
209
- repo.init_git();
210
- repo.write_file(
211
- ".naome/repository-quality.json",
212
- r#"{
213
- "schema": "naome.repository-quality.v1",
214
- "version": 1,
215
- "status": "ready",
216
- "limits": {
217
- "maxFileLines": 5,
218
- "maxNewFileLines": 5,
219
- "maxDiffAddedLines": 200,
220
- "maxFunctionLines": 40,
221
- "maxTopLevelSymbols": 30,
222
- "duplicateBlockLines": 4,
223
- "nearDuplicateSimilarity": 0.9
224
- },
225
- "disabledChecks": [],
226
- "ignoredPaths": [],
227
- "generatedPaths": []
228
- }
229
- "#,
230
- );
231
- repo.write_file(
232
- "src/large.js",
233
- "export function legacy() {\n one();\n two();\n three();\n four();\n}\n",
234
- );
235
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
236
- repo.write_naome_json(
237
- "verification.json",
238
- json!({
239
- "schema": "naome.verification.v1",
240
- "version": 1,
241
- "status": "ready",
242
- "checks": [
243
- {
244
- "id": "repository-quality-check",
245
- "command": "naome quality check --changed",
246
- "cwd": ".",
247
- "purpose": "Validate changed files against repository quality rules.",
248
- "cost": "fast",
249
- "source": "NAOME built-in",
250
- "evidence": ["src/**"],
251
- "lastVerified": null
252
- }
253
- ],
254
- "changeTypes": [
255
- {
256
- "id": "source",
257
- "description": "Source changes.",
258
- "paths": ["src/**"],
259
- "requiredChecks": ["repository-quality-check"],
260
- "recommendedChecks": [],
261
- "humanReview": false
262
- }
263
- ],
264
- "releaseGates": []
265
- }),
266
- );
267
- repo.git(&["add", "."]);
268
- repo.git(&["commit", "-m", "baseline"]);
269
- repo.write_file(
270
- "src/large.js",
271
- "export function legacy() {\n one();\n two();\n three();\n four();\n five();\n}\n",
272
- );
273
- let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
274
-
275
- let route = evaluate_route(
276
- repo.path(),
277
- "commit my changes",
278
- RouteOptions {
279
- execute: true,
280
- evaluation: EvaluationOptions::offline(),
281
- },
282
- )
283
- .unwrap();
284
-
285
- assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
286
- assert!(!route.allowed);
287
- assert!(!route.mutation_performed);
288
- assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
289
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
290
- assert!(route.user_message.contains("repository-quality-check"));
291
- assert!(route.user_message.contains("file-length"));
292
- }
293
-
294
- #[test]
295
- fn execute_route_commits_product_diff_with_declared_safe_quality_checks() {
296
- let repo = TestRepo::new("route-product-diff-quality-pass");
297
- repo.init_git();
298
- repo.write_file(
299
- "packages/naome/crates/naome-core/src/route.rs",
300
- "pub fn baseline() {}\n",
301
- );
302
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
303
- repo.write_product_quality_verification();
304
- repo.git(&["add", "."]);
305
- repo.git(&["commit", "-m", "baseline"]);
306
- repo.write_file(
307
- "packages/naome/crates/naome-core/src/route.rs",
308
- "pub fn changed() {}\n",
309
- );
310
-
311
- let route = evaluate_route(
312
- repo.path(),
313
- "commit my changes",
314
- RouteOptions {
315
- execute: true,
316
- evaluation: EvaluationOptions::offline(),
317
- },
318
- )
319
- .unwrap();
320
-
321
- assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
322
- assert!(route.allowed);
323
- assert!(route.mutation_performed);
324
- assert_eq!(
325
- route.executed_actions,
326
- vec![
327
- "run_user_diff_quality_gate".to_string(),
328
- "commit_user_diff".to_string()
329
- ]
330
- );
331
- assert_eq!(repo.git_status_short(), "");
332
-
333
- let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
334
- assert!(committed_paths.contains("packages/naome/crates/naome-core/src/route.rs"));
335
- assert!(route.user_message.contains("quality gates passed"));
336
- }
337
-
338
- #[test]
339
- fn execute_route_preserves_staged_rename_when_committing_user_diff() {
340
- let repo = TestRepo::new("route-user-diff-staged-rename");
341
- repo.init_git();
342
- repo.write_file("README.md", "# Baseline\n");
343
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
344
- repo.write_naome_json(
345
- "verification.json",
346
- json!({
347
- "schema": "naome.verification.v1",
348
- "version": 1,
349
- "status": "ready",
350
- "checks": [
351
- {
352
- "id": "diff-check",
353
- "command": "git diff --check",
354
- "cwd": ".",
355
- "purpose": "Reject whitespace errors.",
356
- "cost": "fast",
357
- "source": "test",
358
- "evidence": ["README.md", "INTRO.md"],
359
- "lastVerified": null
360
- }
361
- ],
362
- "changeTypes": [
363
- {
364
- "id": "docs",
365
- "description": "Repository docs.",
366
- "paths": ["README.md", "INTRO.md"],
367
- "requiredChecks": ["diff-check"],
368
- "recommendedChecks": [],
369
- "humanReview": false
370
- }
371
- ],
372
- "releaseGates": []
373
- }),
374
- );
375
- repo.git(&["add", "."]);
376
- repo.git(&["commit", "-m", "baseline"]);
377
- repo.git(&["mv", "README.md", "INTRO.md"]);
378
-
379
- let route = evaluate_route(
380
- repo.path(),
381
- "commit my changes",
382
- RouteOptions {
383
- execute: true,
384
- evaluation: EvaluationOptions::offline(),
385
- },
386
- )
387
- .unwrap();
388
-
389
- assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
390
- assert!(route.allowed);
391
- assert!(route.mutation_performed);
392
- assert_eq!(repo.git_status_short(), "");
393
-
394
- let committed_paths = repo.git_stdout(&["show", "--name-status", "--format=", "HEAD"]);
395
- assert!(committed_paths.contains("README.md"));
396
- assert!(committed_paths.contains("INTRO.md"));
397
- }
398
-
399
- #[test]
400
- fn execute_route_refuses_user_diff_commit_without_quality_coverage() {
401
- let repo = TestRepo::new("route-user-diff-no-quality-coverage");
402
- repo.init_git();
403
- repo.write_file("README.md", "# Baseline\n");
404
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
405
- repo.git(&["add", "."]);
406
- repo.git(&["commit", "-m", "baseline"]);
407
- repo.write_file("README.md", "# Manual edit\n");
408
- let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
409
-
410
- let route = evaluate_route(
411
- repo.path(),
412
- "commit my changes",
413
- RouteOptions {
414
- execute: true,
415
- evaluation: EvaluationOptions::offline(),
416
- },
417
- )
418
- .unwrap();
419
-
420
- assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
421
- assert!(!route.allowed);
422
- assert!(!route.mutation_performed);
423
- assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
424
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
425
- assert!(repo.git_status_short().contains("README.md"));
426
- assert!(route.user_message.contains("No quality coverage"));
427
- }
428
-
429
- #[test]
430
- fn execute_route_refuses_user_diff_commit_when_check_mutates_after_diff_check() {
431
- let repo = TestRepo::new("route-user-diff-mutating-check");
432
- repo.init_git();
433
- repo.write_file("README.md", "# Baseline\n");
434
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
435
- repo.write_readme_quality_verification(
436
- vec![json!({
437
- "id": "mutate-readme-after-check",
438
- "command": "node -e \"require('fs').writeFileSync('README.md', '# Mutated \\\\n')\"",
439
- "cwd": ".",
440
- "purpose": "Simulate a mutating check that dirties checked content.",
441
- "cost": "fast",
442
- "source": "test",
443
- "evidence": ["README.md"],
444
- "lastVerified": null
445
- })],
446
- vec!["mutate-readme-after-check"],
447
- );
448
- repo.git(&["add", "."]);
449
- repo.git(&["commit", "-m", "baseline"]);
450
- repo.write_file("README.md", "# Manual edit\n");
451
- let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
452
-
453
- let route = evaluate_route(
454
- repo.path(),
455
- "commit my changes",
456
- RouteOptions {
457
- execute: true,
458
- evaluation: EvaluationOptions::offline(),
459
- },
460
- )
461
- .unwrap();
462
-
463
- assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
464
- assert!(!route.allowed);
465
- assert!(!route.mutation_performed);
466
- assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
467
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
468
- assert!(repo.git_status_short().contains("README.md"));
469
- assert_eq!(
470
- fs::read_to_string(repo.path().join("README.md")).unwrap(),
471
- "# Manual edit\n"
472
- );
473
- assert!(route
474
- .user_message
475
- .contains("will not execute repository-controlled verification commands"));
476
- }
477
-
478
- #[test]
479
- fn execute_route_refuses_user_diff_commit_when_diff_check_command_is_unsafe() {
480
- let repo = TestRepo::new("route-user-diff-mutating-diff-check");
481
- repo.init_git();
482
- repo.write_file("README.md", "# Baseline\n");
483
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
484
- repo.write_naome_json(
485
- "verification.json",
486
- json!({
487
- "schema": "naome.verification.v1",
488
- "version": 1,
489
- "status": "ready",
490
- "checks": [
491
- {
492
- "id": "diff-check",
493
- "command": "node -e \"const fs = require('fs'); const marker = '.git/naome-diff-check-marker'; if (fs.existsSync(marker)) { fs.writeFileSync('NEW.md', '# unexpected\\\\n'); } else { fs.writeFileSync(marker, 'x'); }\"",
494
- "cwd": ".",
495
- "purpose": "Simulate a mutating diff check.",
496
- "cost": "fast",
497
- "source": "test",
498
- "evidence": ["README.md"],
499
- "lastVerified": null
500
- }
501
- ],
502
- "changeTypes": [
503
- {
504
- "id": "readme",
505
- "description": "README changes.",
506
- "paths": ["README.md"],
507
- "requiredChecks": ["diff-check"],
508
- "recommendedChecks": [],
509
- "humanReview": false
510
- }
511
- ],
512
- "releaseGates": []
513
- }),
514
- );
515
- repo.git(&["add", "."]);
516
- repo.git(&["commit", "-m", "baseline"]);
517
- repo.write_file("README.md", "# Manual edit\n");
518
- let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
519
-
520
- let route = evaluate_route(
521
- repo.path(),
522
- "commit my changes",
523
- RouteOptions {
524
- execute: true,
525
- evaluation: EvaluationOptions::offline(),
526
- },
527
- )
528
- .unwrap();
529
-
530
- assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
531
- assert!(!route.allowed);
532
- assert!(!route.mutation_performed);
533
- assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
534
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
535
- assert!(!repo.path().join("NEW.md").exists());
536
- assert!(route
537
- .user_message
538
- .contains("Quality check diff-check has an unsafe command or cwd"));
539
- }
540
-
541
- #[test]
542
- fn execute_route_refuses_user_diff_commit_when_quality_gate_fails() {
543
- let repo = TestRepo::new("route-user-diff-quality-fail");
544
- repo.init_git();
545
- repo.write_file("README.md", "# Baseline\n");
546
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
547
- repo.write_readme_quality_verification(Vec::new(), vec!["diff-check"]);
548
- repo.git(&["add", "."]);
549
- repo.git(&["commit", "-m", "baseline"]);
550
- repo.write_file("README.md", "# Manual edit \n");
551
- let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
552
-
553
- let route = evaluate_route(
554
- repo.path(),
555
- "commit my changes",
556
- RouteOptions {
557
- execute: true,
558
- evaluation: EvaluationOptions::offline(),
559
- },
560
- )
561
- .unwrap();
562
-
563
- assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
564
- assert!(!route.allowed);
565
- assert!(!route.mutation_performed);
566
- assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
567
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
568
- assert!(repo.git_status_short().contains("README.md"));
569
- assert_eq!(route.human_options, vec!["review_unowned_diff"]);
570
- assert!(route.user_message.contains("quality gate failed"));
571
- }
572
-
573
- #[test]
574
- fn execute_route_baselines_harness_refresh_before_dirty_repo_worktree() {
575
- let repo = TestRepo::new("route-dirty-harness-refresh-worktree");
576
- repo.init_git();
577
- repo.write_file("README.md", "# Baseline\n");
578
- repo.write_file("USER.md", "user baseline\n");
579
- repo.write_file("AGENTS.md", "# Agent Instructions\n\nOld harness.\n");
580
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
581
- repo.write_naome_json(
582
- "manifest.json",
583
- json!({
584
- "name": "naome",
585
- "harnessVersion": "1.1.0",
586
- "profile": "old",
587
- "machineOwned": ["AGENTS.md"],
588
- "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
589
- "integrity": {}
590
- }),
591
- );
592
- repo.git(&["add", "."]);
593
- repo.git(&["commit", "-m", "baseline"]);
594
- repo.write_file("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
595
- repo.write_naome_json(
596
- "manifest.json",
597
- json!({
598
- "name": "naome",
599
- "harnessVersion": "1.1.0",
600
- "profile": "standard",
601
- "machineOwned": ["AGENTS.md"],
602
- "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
603
- "integrity": {}
604
- }),
605
- );
606
- repo.write_file("USER.md", "user local edit\n");
607
-
608
- let route = evaluate_route(
609
- repo.path(),
610
- "Add another line to README as a new task.",
611
- RouteOptions {
612
- execute: true,
613
- evaluation: EvaluationOptions::offline(),
614
- },
615
- )
616
- .unwrap();
617
-
618
- assert_eq!(
619
- route.policy_action,
620
- "auto_commit_harness_refresh_then_create_isolated_task_worktree"
621
- );
622
- assert_eq!(
623
- route.executed_actions,
624
- vec![
625
- "commit_harness_refresh_baseline".to_string(),
626
- "create_task_worktree".to_string()
627
- ]
628
- );
629
- assert!(route.can_create_task);
630
- assert_eq!(route.next_decision.state, "ready_for_task");
631
- assert!(route.worktree.is_some());
632
- assert_eq!(repo.git_status_short(), "M USER.md");
633
- assert_eq!(
634
- TestRepo::git_status_short_at(Path::new(&route.task_root)),
635
- ""
636
- );
637
-
638
- let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
639
- assert!(committed_paths.contains("AGENTS.md"));
640
- assert!(committed_paths.contains(".naome/manifest.json"));
641
- assert!(!committed_paths.contains("USER.md"));
642
- }
643
-
644
- #[test]
645
- fn execute_route_baselines_pure_harness_refresh_before_new_task_without_worktree() {
646
- let repo = TestRepo::new("route-pure-harness-refresh-no-worktree");
647
- repo.init_git();
648
- repo.write_file("README.md", "# Baseline\n");
649
- repo.write_file("AGENTS.md", "# Agent Instructions\n\nOld harness.\n");
650
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
651
- repo.write_naome_json(
652
- "manifest.json",
653
- json!({
654
- "name": "naome",
655
- "harnessVersion": "1.1.0",
656
- "profile": "old",
657
- "machineOwned": ["AGENTS.md"],
658
- "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
659
- "integrity": {}
660
- }),
661
- );
662
- repo.git(&["add", "."]);
663
- repo.git(&["commit", "-m", "baseline"]);
664
- let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
665
- repo.write_base_naome_state(completed_task_state(&admission_head));
666
- repo.git(&["add", ".naome/task-state.json"]);
667
- repo.git(&["commit", "-m", "task state"]);
668
- repo.write_file("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
669
- repo.write_naome_json(
670
- "manifest.json",
671
- json!({
672
- "name": "naome",
673
- "harnessVersion": "1.1.0",
674
- "profile": "standard",
675
- "machineOwned": ["AGENTS.md"],
676
- "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
677
- "integrity": {}
678
- }),
679
- );
680
-
681
- let route = evaluate_route(
682
- repo.path(),
683
- "Add another line to README as a new task.",
684
- RouteOptions {
685
- execute: true,
686
- evaluation: EvaluationOptions::offline(),
687
- },
688
- )
689
- .unwrap();
690
-
691
- assert_eq!(
692
- route.policy_action,
693
- "auto_commit_harness_refresh_then_create_new_task"
694
- );
695
- assert_eq!(
696
- route.executed_actions,
697
- vec!["commit_harness_refresh_baseline".to_string()]
698
- );
699
- assert!(route.mutation_performed);
700
- assert!(route.can_create_task);
701
- assert!(route.worktree.is_none());
702
- assert_eq!(route.task_root, repo.path().to_string_lossy());
703
- assert_eq!(route.next_decision.state, "ready_for_task");
704
- assert_eq!(repo.git_status_short(), "");
705
-
706
- let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
707
- assert!(committed_paths.contains("AGENTS.md"));
708
- assert!(committed_paths.contains(".naome/manifest.json"));
709
- assert!(!committed_paths.contains("README.md"));
710
- }
711
-
712
- #[test]
713
- fn execute_route_repair_request_baselines_harness_refresh_only() {
714
- let repo = TestRepo::new("route-harness-refresh-repair-only");
715
- repo.init_git();
716
- repo.write_file("README.md", "# Baseline\n");
717
- repo.write_file("USER.md", "user baseline\n");
718
- repo.write_file("AGENTS.md", "# Agent Instructions\n\nOld harness.\n");
719
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
720
- repo.write_naome_json(
721
- "manifest.json",
722
- json!({
723
- "name": "naome",
724
- "harnessVersion": "1.1.0",
725
- "profile": "old",
726
- "machineOwned": ["AGENTS.md"],
727
- "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
728
- "integrity": {}
729
- }),
730
- );
731
- repo.git(&["add", "."]);
732
- repo.git(&["commit", "-m", "baseline"]);
733
- repo.write_file("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
734
- repo.write_naome_json(
735
- "manifest.json",
736
- json!({
737
- "name": "naome",
738
- "harnessVersion": "1.1.0",
739
- "profile": "standard",
740
- "machineOwned": ["AGENTS.md"],
741
- "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
742
- "integrity": {}
743
- }),
744
- );
745
- repo.write_file("USER.md", "user local edit\n");
746
-
747
- let route = evaluate_route(
748
- repo.path(),
749
- "please repair all",
750
- RouteOptions {
751
- execute: true,
752
- evaluation: EvaluationOptions::offline(),
753
- },
754
- )
755
- .unwrap();
756
-
757
- assert_eq!(route.policy_action, "auto_commit_harness_refresh_baseline");
758
- assert!(route.mutation_performed);
759
- assert_eq!(
760
- route.executed_actions,
761
- vec!["commit_harness_refresh_baseline".to_string()]
762
- );
763
- assert_eq!(repo.git_status_short(), "M USER.md");
764
-
765
- let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
766
- assert!(committed_paths.contains("AGENTS.md"));
767
- assert!(committed_paths.contains(".naome/manifest.json"));
768
- assert!(!committed_paths.contains("USER.md"));
769
- }
770
-
771
- #[test]
772
- fn execute_route_refuses_to_create_more_than_max_isolated_worktrees() {
773
- let repo = TestRepo::new("route-worktree-limit");
774
- repo.init_git();
775
- repo.write_file("README.md", "# Baseline\n");
776
- repo.write_file("USER.md", "user baseline\n");
777
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
778
- repo.git(&["add", "."]);
779
- repo.git(&["commit", "-m", "baseline"]);
780
- repo.write_file("USER.md", "user local edit\n");
781
- let common_dir = repo.git_stdout(&["rev-parse", "--git-common-dir"]);
782
- let worktree_root = repo.path().join(common_dir).join("naome").join("worktrees");
783
- fs::create_dir_all(&worktree_root).unwrap();
784
- for index in 0..25 {
785
- fs::create_dir_all(worktree_root.join(format!("stale-{index}"))).unwrap();
786
- }
787
-
788
- let error = evaluate_route(
789
- repo.path(),
790
- "Add another line to README as a new task.",
791
- RouteOptions {
792
- execute: true,
793
- evaluation: EvaluationOptions::offline(),
794
- },
795
- )
796
- .unwrap_err();
797
-
798
- assert!(error
799
- .to_string()
800
- .contains("Too many NAOME task worktrees are present"));
801
- }
802
-
803
- #[test]
804
- fn execute_route_preflights_worktree_before_completed_task_baseline() {
805
- let repo =
806
- TestRepo::completed_task_with_unrelated_user_edit("route-completed-worktree-preflight");
807
- let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
808
- let before_status = repo.git_status_short();
809
- let common_dir = repo.git_stdout(&["rev-parse", "--git-common-dir"]);
810
- let worktree_root = repo.path().join(common_dir).join("naome").join("worktrees");
811
- fs::create_dir_all(&worktree_root).unwrap();
812
- for index in 0..25 {
813
- fs::create_dir_all(worktree_root.join(format!("stale-{index}"))).unwrap();
814
- }
815
-
816
- let error = evaluate_route(
817
- repo.path(),
818
- "Add another line to README as a new task.",
819
- RouteOptions {
820
- execute: true,
821
- evaluation: EvaluationOptions::offline(),
822
- },
823
- )
824
- .unwrap_err();
825
-
826
- assert!(error
827
- .to_string()
828
- .contains("Too many NAOME task worktrees are present"));
829
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
830
- assert_eq!(repo.git_status_short(), before_status);
831
- }
832
-
833
- #[test]
834
- fn execute_route_uses_preflighted_worktree_name_after_completed_task_baseline() {
835
- let repo = TestRepo::completed_task_with_unrelated_user_edit("route-worktree-name-preflight");
836
- let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
837
- let before_short = &before_head[..12];
838
-
839
- let route = evaluate_route(
840
- repo.path(),
841
- "Add another line to README as a new task.",
842
- RouteOptions {
843
- execute: true,
844
- evaluation: EvaluationOptions::offline(),
845
- },
846
- )
847
- .unwrap();
848
-
849
- let worktree = route.worktree.expect("route should create a worktree");
850
- assert!(worktree.branch.contains(before_short));
851
- assert!(worktree.path.contains(before_short));
852
- }
853
-
854
- #[test]
855
- fn dry_route_plans_harness_refresh_split_before_completed_task_baseline() {
856
- let repo = TestRepo::completed_task_with_harness_refresh_diff("route-dry-harness-refresh");
857
- let before = repo.git_stdout(&["rev-parse", "HEAD"]);
858
-
859
- let route = evaluate_route(
860
- repo.path(),
861
- "Start a new task for README polish.",
862
- RouteOptions {
863
- execute: false,
864
- evaluation: EvaluationOptions::offline(),
865
- },
866
- )
867
- .unwrap();
868
-
869
- assert_eq!(
870
- route.policy_action,
871
- "auto_commit_harness_refresh_then_completed_task_then_create_new_task"
872
- );
873
- assert!(!route.mutation_performed);
874
- assert!(!route.can_create_task);
875
- assert!(route.human_options.is_empty());
876
- assert!(route.intent.risk_codes.is_empty());
877
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
878
- }
879
-
880
- #[test]
881
- fn execute_route_splits_harness_refresh_then_completed_task_baseline() {
882
- let repo = TestRepo::completed_task_with_harness_refresh_diff("route-execute-harness-refresh");
883
-
884
- let route = evaluate_route(
885
- repo.path(),
886
- "Start a new task for README polish.",
887
- RouteOptions {
888
- execute: true,
889
- evaluation: EvaluationOptions::offline(),
890
- },
891
- )
892
- .unwrap();
893
-
894
- assert_eq!(
895
- route.policy_action,
896
- "auto_commit_harness_refresh_then_completed_task_then_create_new_task"
897
- );
898
- assert!(route.mutation_performed);
899
- assert!(route.can_create_task);
900
- assert_eq!(
901
- route.executed_actions,
902
- vec![
903
- "commit_harness_refresh_baseline".to_string(),
904
- "commit_task_baseline".to_string()
905
- ]
906
- );
907
- assert_eq!(route.next_decision.state, "ready_for_task");
908
- assert!(repo.git_status_short().is_empty());
909
-
910
- let log = repo.git_stdout(&["log", "--format=%s", "-2"]);
911
- assert!(log.contains("chore(naome): baseline completed task"));
912
- assert!(log.contains("chore(naome): baseline harness refresh"));
913
-
914
- let journal = fs::read_to_string(repo.path().join(".naome/task-journal.jsonl")).unwrap();
915
- assert!(journal.contains("\"taskId\":\"readme-task\""));
916
- assert!(journal.contains("\"outcome\":\"route_auto_baseline\""));
917
- }
918
-
919
- #[test]
920
- fn execute_route_does_not_mutate_when_prompt_blocks_commit() {
921
- let repo = TestRepo::completed_task_with_diff("route-no-commit");
922
- let before = repo.git_stdout(&["rev-parse", "HEAD"]);
923
-
924
- let route = evaluate_route(
925
- repo.path(),
926
- "Do not commit. Start a new task after this.",
927
- RouteOptions {
928
- execute: true,
929
- evaluation: EvaluationOptions::offline(),
930
- },
931
- )
932
- .unwrap();
933
-
934
- assert_eq!(route.policy_action, "block_auto_baseline_due_to_no_commit");
935
- assert!(!route.mutation_performed);
936
- assert!(!route.can_create_task);
937
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
938
- assert!(repo.git_status_short().contains("README.md"));
939
- }
940
-
941
- #[test]
942
- fn explicit_route_commit_baseline_leaves_unrelated_user_edit_unstaged() {
943
- let repo = TestRepo::completed_task_with_diff("route-commit-task-scope");
944
- repo.write_file("USER.md", "user baseline\n");
945
- repo.git(&["add", "USER.md"]);
946
- repo.git(&["commit", "-m", "user baseline"]);
947
- repo.write_file("README.md", "# Changed\n");
948
- repo.write_file("USER.md", "user local edit\n");
949
- repo.git(&["add", "USER.md"]);
950
-
951
- let route = evaluate_route(
952
- repo.path(),
953
- "commit_task_baseline",
954
- RouteOptions {
955
- execute: true,
956
- evaluation: EvaluationOptions::offline(),
957
- },
958
- )
959
- .unwrap();
960
-
961
- assert_eq!(route.policy_action, "commit_task_baseline");
962
- assert!(route.mutation_performed);
963
- assert_eq!(repo.git_status_short(), "M USER.md");
964
-
965
- let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
966
- assert!(committed_paths.contains("README.md"));
967
- assert!(!committed_paths.contains("USER.md"));
968
- }
969
-
970
- #[test]
971
- fn execute_route_journals_external_commit_after_completed_task() {
972
- let repo = TestRepo::completed_task_with_diff("route-external-commit");
973
- repo.git(&["add", "-A"]);
974
- repo.git(&["commit", "-m", "manual baseline"]);
975
- assert!(repo.git_status_short().is_empty());
976
-
977
- let route = evaluate_route(
978
- repo.path(),
979
- "Create a new task for README polish.",
980
- RouteOptions {
981
- execute: true,
982
- evaluation: EvaluationOptions::offline(),
983
- },
984
- )
985
- .unwrap();
986
-
987
- assert_eq!(route.repo_state_before, "ready_for_task");
988
- assert!(route.mutation_performed);
989
- assert!(route.can_create_task);
990
- assert!(route
991
- .executed_actions
992
- .contains(&"journal_external_task_baseline".to_string()));
993
-
994
- let journal = fs::read_to_string(repo.path().join(".naome/task-journal.jsonl")).unwrap();
995
- assert!(journal.contains("\"outcome\":\"external_baseline\""));
996
- assert!(journal.contains("\"taskId\":\"readme-task\""));
997
- }
998
-
999
- #[test]
1000
- fn explain_reports_winning_rule_and_mutation_plan_without_executing() {
1001
- let repo = TestRepo::completed_task_with_diff("route-explain");
1002
- let before = repo.git_stdout(&["rev-parse", "HEAD"]);
1003
-
1004
- let explain = explain_route(
1005
- repo.path(),
1006
- "Start a new task for README polish.",
1007
- EvaluationOptions::offline(),
1008
- )
1009
- .unwrap();
1010
-
1011
- assert_eq!(
1012
- explain.winning_rule,
1013
- "completed_task_valid_new_task_auto_baseline"
1014
- );
1015
- assert!(explain.would_mutate);
1016
- assert!(explain
1017
- .discarded_candidate_actions
1018
- .contains(&"review_task_diff".to_string()));
1019
- assert!(explain
1020
- .required_context
1021
- .contains(&"docs/naome/execution.md".to_string()));
1022
- assert!(!explain.user_message.contains("commit_task_baseline"));
1023
- assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
1024
- }
1025
-
1026
- #[test]
1027
- fn unhealthy_harness_route_blocks_normal_work() {
1028
- let repo = TestRepo::new("route-unhealthy");
1029
- repo.init_git();
1030
- repo.write_file("README.md", "# Baseline\n");
1031
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
1032
- repo.write_file(".naome/bin/check-harness-health.js", "process.exit(1);\n");
1033
- repo.git(&["add", "."]);
1034
- repo.git(&["commit", "-m", "baseline"]);
1035
-
1036
- let route = evaluate_route(
1037
- repo.path(),
1038
- "Create a new task.",
1039
- RouteOptions {
1040
- execute: true,
1041
- evaluation: EvaluationOptions::online(),
1042
- },
1043
- )
1044
- .unwrap();
1045
-
1046
- assert_eq!(route.repo_state_before, "harness_unhealthy");
1047
- assert!(!route.mutation_performed);
1048
- assert!(!route.can_create_task);
1049
- assert!(!route.human_options.is_empty());
1050
- }
1051
-
1052
- struct TestRepo {
1053
- root: PathBuf,
1054
- }
1055
-
1056
- impl TestRepo {
1057
- fn new(name: &str) -> Self {
1058
- let nonce = SystemTime::now()
1059
- .duration_since(UNIX_EPOCH)
1060
- .unwrap()
1061
- .as_nanos();
1062
- let root = std::env::temp_dir().join(format!("naome-route-{name}-{nonce}"));
1063
- fs::create_dir_all(root.join(".naome")).unwrap();
1064
- Self { root }
1065
- }
1066
-
1067
- fn completed_task_with_diff(name: &str) -> Self {
1068
- let repo = Self::new(name);
1069
- repo.init_git();
1070
- repo.write_file("README.md", "# Baseline\n");
1071
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
1072
- repo.git(&["add", "."]);
1073
- repo.git(&["commit", "-m", "baseline"]);
1074
- let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
1075
- repo.write_base_naome_state(completed_task_state(&admission_head));
1076
- repo.git(&["add", ".naome/task-state.json"]);
1077
- repo.git(&["commit", "-m", "task state"]);
1078
- repo.write_file("README.md", "# Changed\n");
1079
- repo
1080
- }
1081
-
1082
- fn completed_task_with_unrelated_user_edit(name: &str) -> Self {
1083
- let repo = Self::new(name);
1084
- repo.init_git();
1085
- repo.write_file("README.md", "# Baseline\n");
1086
- repo.write_file("USER.md", "user baseline\n");
1087
- repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
1088
- repo.git(&["add", "."]);
1089
- repo.git(&["commit", "-m", "baseline"]);
1090
- let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
1091
- repo.write_base_naome_state(completed_task_state(&admission_head));
1092
- repo.git(&["add", ".naome/task-state.json"]);
1093
- repo.git(&["commit", "-m", "task state"]);
1094
- repo.write_file("README.md", "# Changed\n");
1095
- repo.write_file("USER.md", "user local edit\n");
1096
- repo
1097
- }
1098
-
1099
- fn completed_task_with_harness_refresh_diff(name: &str) -> Self {
1100
- let repo = Self::completed_task_with_diff(name);
1101
- repo.write_file("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
1102
- repo.write_file(
1103
- ".naome/manifest.json",
1104
- r#"{
1105
- "name": "naome",
1106
- "harnessVersion": "1.1.0",
1107
- "profile": "standard",
1108
- "machineOwned": ["AGENTS.md"],
1109
- "projectOwned": [".naome/manifest.json", ".naome/task-state.json"],
1110
- "integrity": {}
1111
- }
1112
- "#,
1113
- );
1114
- repo
1115
- }
1116
-
1117
- fn path(&self) -> &Path {
1118
- &self.root
1119
- }
1120
-
1121
- fn write_base_naome_state(&self, task_state: serde_json::Value) {
1122
- self.write_naome_json(
1123
- "init-state.json",
1124
- json!({ "initialized": true, "intakeStatus": "complete" }),
1125
- );
1126
- self.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
1127
- self.write_naome_json(
1128
- "verification.json",
1129
- json!({
1130
- "schema": "naome.verification.v1",
1131
- "version": 1,
1132
- "status": "ready",
1133
- "checks": [
1134
- {
1135
- "id": "diff-check",
1136
- "command": "git diff --check",
1137
- "cwd": ".",
1138
- "purpose": "Reject whitespace errors.",
1139
- "cost": "fast",
1140
- "source": "test",
1141
- "evidence": ["README.md"],
1142
- "lastVerified": null
1143
- }
1144
- ],
1145
- "changeTypes": [],
1146
- "releaseGates": []
1147
- }),
1148
- );
1149
- self.write_naome_json("task-state.json", task_state);
1150
- }
1151
-
1152
- fn write_readme_quality_verification(
1153
- &self,
1154
- extra_checks: Vec<Value>,
1155
- required_checks: Vec<&str>,
1156
- ) {
1157
- let mut checks = vec![json!({
1158
- "id": "diff-check",
1159
- "command": "git diff --check",
1160
- "cwd": ".",
1161
- "purpose": "Reject whitespace errors.",
1162
- "cost": "fast",
1163
- "source": "test",
1164
- "evidence": ["README.md"],
1165
- "lastVerified": null
1166
- })];
1167
- checks.extend(extra_checks);
1168
-
1169
- self.write_naome_json(
1170
- "verification.json",
1171
- json!({
1172
- "schema": "naome.verification.v1",
1173
- "version": 1,
1174
- "status": "ready",
1175
- "checks": checks,
1176
- "changeTypes": [
1177
- {
1178
- "id": "readme",
1179
- "description": "README changes.",
1180
- "paths": ["README.md"],
1181
- "requiredChecks": required_checks,
1182
- "recommendedChecks": [],
1183
- "humanReview": false
1184
- }
1185
- ],
1186
- "releaseGates": []
1187
- }),
1188
- );
1189
- }
1190
-
1191
- fn write_product_quality_verification(&self) {
1192
- self.write_naome_json(
1193
- "verification.json",
1194
- json!({
1195
- "schema": "naome.verification.v1",
1196
- "version": 1,
1197
- "status": "ready",
1198
- "checks": [
1199
- {
1200
- "id": "installer-tests",
1201
- "command": "npm run test:naome-installer",
1202
- "cwd": ".",
1203
- "purpose": "Validate installer behavior.",
1204
- "cost": "medium",
1205
- "source": "test",
1206
- "evidence": ["scripts/naome-installer.test.js"],
1207
- "lastVerified": null
1208
- },
1209
- {
1210
- "id": "rust-build",
1211
- "command": "npm run build:rust",
1212
- "cwd": ".",
1213
- "purpose": "Build native CLI.",
1214
- "cost": "medium",
1215
- "source": "test",
1216
- "evidence": ["packages/naome/Cargo.toml"],
1217
- "lastVerified": null
1218
- },
1219
- {
1220
- "id": "decision-engine-tests",
1221
- "command": "npm run test:decision-engine",
1222
- "cwd": ".",
1223
- "purpose": "Validate route decisions.",
1224
- "cost": "fast",
1225
- "source": "test",
1226
- "evidence": ["packages/naome/crates/naome-core/tests/route.rs"],
1227
- "lastVerified": null
1228
- },
1229
- {
1230
- "id": "package-dry-run",
1231
- "command": "npm run pack:dry-run",
1232
- "cwd": ".",
1233
- "purpose": "Validate package metadata.",
1234
- "cost": "medium",
1235
- "source": "test",
1236
- "evidence": ["packages/naome/package.json"],
1237
- "lastVerified": null
1238
- },
1239
- {
1240
- "id": "diff-check",
1241
- "command": "git diff --check",
1242
- "cwd": ".",
1243
- "purpose": "Reject whitespace errors.",
1244
- "cost": "fast",
1245
- "source": "test",
1246
- "evidence": ["packages/naome/crates/naome-core/src/route.rs"],
1247
- "lastVerified": null
1248
- }
1249
- ],
1250
- "changeTypes": [
1251
- {
1252
- "id": "product-installer-or-template",
1253
- "description": "NAOME product changes.",
1254
- "paths": ["packages/naome/**", "scripts/**"],
1255
- "requiredChecks": [
1256
- "installer-tests",
1257
- "rust-build",
1258
- "decision-engine-tests",
1259
- "package-dry-run"
1260
- ],
1261
- "recommendedChecks": [],
1262
- "humanReview": false
1263
- }
1264
- ],
1265
- "releaseGates": []
1266
- }),
1267
- );
1268
- }
1269
-
1270
- fn write_naome_json(&self, file_name: &str, value: serde_json::Value) {
1271
- let path = self.root.join(".naome").join(file_name);
1272
- fs::write(
1273
- path,
1274
- format!("{}\n", serde_json::to_string_pretty(&value).unwrap()),
1275
- )
1276
- .unwrap();
1277
- }
1278
-
1279
- fn write_file(&self, relative_path: &str, content: &str) {
1280
- let path = self.root.join(relative_path);
1281
- if let Some(parent) = path.parent() {
1282
- fs::create_dir_all(parent).unwrap();
1283
- }
1284
- fs::write(path, content).unwrap();
1285
- }
1286
-
1287
- fn init_git(&self) {
1288
- self.git(&["init"]);
1289
- self.git(&["config", "user.email", "naome@example.com"]);
1290
- self.git(&["config", "user.name", "NAOME Test"]);
1291
- }
1292
-
1293
- fn git(&self, args: &[&str]) {
1294
- let output = Command::new("git")
1295
- .args(args)
1296
- .current_dir(&self.root)
1297
- .output()
1298
- .unwrap();
1299
- assert!(
1300
- output.status.success(),
1301
- "git {:?} failed\nstdout:\n{}\nstderr:\n{}",
1302
- args,
1303
- String::from_utf8_lossy(&output.stdout),
1304
- String::from_utf8_lossy(&output.stderr)
1305
- );
1306
- }
1307
-
1308
- fn git_stdout(&self, args: &[&str]) -> String {
1309
- let output = Command::new("git")
1310
- .args(args)
1311
- .current_dir(&self.root)
1312
- .output()
1313
- .unwrap();
1314
- assert!(output.status.success());
1315
- String::from_utf8_lossy(&output.stdout).trim().to_string()
1316
- }
1317
-
1318
- fn git_status_short(&self) -> String {
1319
- self.git_stdout(&["status", "--short"])
1320
- }
1321
-
1322
- fn git_status_short_at(root: &Path) -> String {
1323
- let output = Command::new("git")
1324
- .args(["status", "--short"])
1325
- .current_dir(root)
1326
- .output()
1327
- .unwrap();
1328
- assert!(output.status.success());
1329
- String::from_utf8_lossy(&output.stdout).trim().to_string()
1330
- }
1331
- }
1332
-
1333
- fn completed_task_state(admission_head: &str) -> serde_json::Value {
1334
- json!({
1335
- "schema": "naome.task-state.v1",
1336
- "version": 1,
1337
- "status": "complete",
1338
- "activeTask": {
1339
- "id": "readme-task",
1340
- "request": "Change README.",
1341
- "userPrompt": {
1342
- "receivedAt": "2026-05-06T00:00:00.000Z",
1343
- "text": "Change README."
1344
- },
1345
- "admission": {
1346
- "command": "node .naome/bin/check-task-state.js --admission",
1347
- "cwd": ".",
1348
- "exitCode": 0,
1349
- "checkedAt": "2026-05-06T00:00:00.000Z",
1350
- "gitHead": admission_head,
1351
- "changedPaths": []
1352
- },
1353
- "allowedPaths": ["README.md"],
1354
- "declaredChangeTypes": ["product-docs"],
1355
- "requiredCheckIds": ["diff-check"],
1356
- "proofResults": [
1357
- {
1358
- "checkId": "diff-check",
1359
- "command": "git diff --check",
1360
- "cwd": ".",
1361
- "exitCode": 0,
1362
- "checkedAt": "2026-05-06T00:00:00.000Z",
1363
- "evidence": ["README.md"]
1364
- }
1365
- ],
1366
- "revisions": [],
1367
- "humanReview": {
1368
- "required": false,
1369
- "approved": false,
1370
- "reason": null
1371
- }
1372
- },
1373
- "blocker": null,
1374
- "updatedAt": "2026-05-06T00:00:00.000Z"
1375
- })
1376
- }
1
+ // Route integration tests are split by scenario in route_*.rs.