@lamentis/naome 1.1.2 → 1.2.1

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