@lamentis/naome 1.2.0 → 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 (113) hide show
  1. package/Cargo.lock +2 -2
  2. package/bin/naome-node.js +2 -1579
  3. package/bin/naome.js +19 -5
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/dispatcher.rs +2 -1
  6. package/crates/naome-cli/src/main.rs +3 -0
  7. package/crates/naome-cli/src/quality_commands.rs +90 -2
  8. package/crates/naome-core/Cargo.toml +1 -1
  9. package/crates/naome-core/src/decision/checks.rs +64 -0
  10. package/crates/naome-core/src/decision/idle.rs +67 -0
  11. package/crates/naome-core/src/decision/json.rs +36 -0
  12. package/crates/naome-core/src/decision/states.rs +165 -0
  13. package/crates/naome-core/src/decision.rs +131 -353
  14. package/crates/naome-core/src/install_plan.rs +2 -0
  15. package/crates/naome-core/src/lib.rs +5 -3
  16. package/crates/naome-core/src/paths.rs +3 -1
  17. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  18. package/crates/naome-core/src/quality/adapters.rs +20 -67
  19. package/crates/naome-core/src/quality/cleanup.rs +13 -1
  20. package/crates/naome-core/src/quality/config.rs +8 -15
  21. package/crates/naome-core/src/quality/config_support.rs +24 -0
  22. package/crates/naome-core/src/quality/mod.rs +18 -0
  23. package/crates/naome-core/src/quality/scanner.rs +20 -8
  24. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  25. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  26. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  27. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  28. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  29. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  30. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  31. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  32. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  33. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  34. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  35. package/crates/naome-core/src/quality/types.rs +3 -0
  36. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  37. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  38. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  39. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  40. package/crates/naome-core/src/route/context.rs +180 -0
  41. package/crates/naome-core/src/route/execution.rs +96 -0
  42. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  43. package/crates/naome-core/src/route/execution_support.rs +57 -0
  44. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  45. package/crates/naome-core/src/route/git_ops.rs +72 -0
  46. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  47. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  48. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  49. package/crates/naome-core/src/route/worktree.rs +75 -0
  50. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  51. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  52. package/crates/naome-core/src/route.rs +44 -1217
  53. package/crates/naome-core/src/verification.rs +1 -0
  54. package/crates/naome-core/tests/decision.rs +24 -118
  55. package/crates/naome-core/tests/harness_health.rs +2 -0
  56. package/crates/naome-core/tests/quality.rs +12 -118
  57. package/crates/naome-core/tests/quality_structure.rs +116 -0
  58. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  59. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  60. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  61. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  62. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  63. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  64. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  65. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  66. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  67. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  68. package/crates/naome-core/tests/route.rs +1 -1376
  69. package/crates/naome-core/tests/route_baseline.rs +86 -0
  70. package/crates/naome-core/tests/route_completion.rs +141 -0
  71. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  72. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  73. package/crates/naome-core/tests/route_worktree.rs +54 -0
  74. package/crates/naome-core/tests/task_state.rs +60 -432
  75. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  76. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  77. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  78. package/crates/naome-core/tests/verification.rs +4 -45
  79. package/crates/naome-core/tests/verification_contract.rs +22 -78
  80. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  81. package/installer/agents.js +90 -0
  82. package/installer/context.js +67 -0
  83. package/installer/filesystem.js +166 -0
  84. package/installer/flows.js +84 -0
  85. package/installer/git-boundary.js +170 -0
  86. package/installer/git-hook-content.js +36 -0
  87. package/installer/git-hooks.js +134 -0
  88. package/installer/git-local.js +2 -0
  89. package/installer/git-shared.js +35 -0
  90. package/installer/harness-file-ops.js +140 -0
  91. package/installer/harness-files.js +56 -0
  92. package/installer/harness-verification.js +123 -0
  93. package/installer/install-plan.js +66 -0
  94. package/installer/main.js +25 -0
  95. package/installer/manifest-state.js +167 -0
  96. package/installer/native-build.js +24 -0
  97. package/installer/native-format.js +6 -0
  98. package/installer/native.js +162 -0
  99. package/installer/output.js +131 -0
  100. package/installer/version.js +32 -0
  101. package/native/darwin-arm64/naome +0 -0
  102. package/native/linux-x64/naome +0 -0
  103. package/package.json +2 -1
  104. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  105. package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
  106. package/templates/naome-root/.naome/bin/naome.js +25 -21
  107. package/templates/naome-root/.naome/manifest.json +4 -2
  108. package/templates/naome-root/.naome/repository-structure.json +90 -0
  109. package/templates/naome-root/.naome/verification.json +1 -0
  110. package/templates/naome-root/docs/naome/index.md +4 -3
  111. package/templates/naome-root/docs/naome/repository-quality.md +3 -0
  112. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  113. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -1,25 +1,32 @@
1
- use std::collections::{BTreeSet, HashMap};
2
- use std::fs;
3
- use std::path::{Path, PathBuf};
4
- use std::process::Command;
5
-
6
- use serde::Serialize;
7
- use serde_json::Value;
1
+ use std::path::Path;
8
2
 
9
3
  use crate::decision::{evaluate_decision, EvaluationOptions};
10
- use crate::git;
11
- use crate::harness_health::{validate_harness_health, HarnessHealthOptions};
12
- use crate::install_plan::{LOCAL_NATIVE_BINARY_PATHS, LOCAL_ONLY_MACHINE_OWNED_PATHS};
13
4
  use crate::intent::{evaluate_intent, IntentDecision};
14
- use crate::journal::{append_task_journal, TaskJournalEntry};
5
+ use crate::journal::TaskJournalEntry;
15
6
  use crate::models::{Decision, NaomeError};
16
- use crate::paths;
17
- use crate::quality::{check_repository_quality, QualityMode};
18
- use crate::task_state::{
19
- completed_task_commit_paths, completed_task_harness_refresh_diff, harness_refresh_diff,
20
- harness_refresh_with_unrelated_diff, validate_task_state, TaskStateMode, TaskStateOptions,
7
+ use serde::Serialize;
8
+ mod builtin_checks;
9
+ mod builtin_context;
10
+ mod builtin_integrity;
11
+ mod builtin_require;
12
+ mod context;
13
+ mod execution;
14
+ mod execution_baselines;
15
+ mod execution_support;
16
+ mod execution_tasks;
17
+ mod git_ops;
18
+ mod quality_gate;
19
+ mod quality_gate_config;
20
+ mod quality_gate_snapshot;
21
+ mod worktree;
22
+ mod worktree_files;
23
+ mod worktree_plan;
24
+
25
+ use self::context::{
26
+ can_create_task, discarded_actions, required_context_for_intent, required_context_for_route,
27
+ winning_rule, would_mutate,
21
28
  };
22
- use crate::verification_contract::validate_verification_contract;
29
+ use self::execution::{execute_route_policy, RouteExecution};
23
30
 
24
31
  const MAX_NAOME_TASK_WORKTREES: usize = 25;
25
32
 
@@ -87,219 +94,27 @@ pub fn evaluate_route(
87
94
  let initial_decision = evaluate_decision(root, options.evaluation)?;
88
95
  let intent = evaluate_intent(root, prompt, options.evaluation)?;
89
96
  let repo_state_before = intent.repo_state.clone();
90
- let mut mutation_performed = false;
91
- let mut executed_actions = Vec::new();
92
- let mut journal_entry = None;
93
- let mut user_message = intent.user_message.clone();
94
- let mut task_root = root.to_path_buf();
95
- let mut worktree = None;
96
- let mut route_allowed = intent.allowed;
97
- let mut human_options = intent.human_options.clone();
98
-
97
+ let mut execution = RouteExecution::from_intent(root, &intent);
99
98
  if options.execute {
100
- match intent.policy_action.as_str() {
101
- "auto_commit_completed_task_then_create_new_task" => {
102
- let before = git_head(root)?;
103
- git_add_completed_task_paths(root)?;
104
- git_commit(root, "chore(naome): baseline completed task")?;
105
- let after = git_head(root)?;
106
- journal_entry =
107
- append_task_journal(root, "route_auto_baseline", before, after.clone())?;
108
- mutation_performed = true;
109
- executed_actions.push("commit_task_baseline".to_string());
110
- user_message =
111
- "NAOME baselined the completed task and is ready to create the next task."
112
- .to_string();
113
- }
114
- "auto_commit_completed_task_then_create_isolated_task_worktree" => {
115
- let worktree_name_head = task_worktree_name_head(root)?;
116
- preflight_isolated_task_worktree(root, prompt, &worktree_name_head)?;
117
- let before = Some(worktree_name_head.clone());
118
- git_add_completed_task_paths(root)?;
119
- git_commit(root, "chore(naome): baseline completed task")?;
120
- let after = git_head(root)?;
121
- journal_entry =
122
- append_task_journal(root, "route_auto_baseline", before, after.clone())?;
123
- let created = create_isolated_task_worktree_with_name_head(
124
- root,
125
- prompt,
126
- &worktree_name_head,
127
- )?;
128
- task_root = PathBuf::from(&created.path);
129
- mutation_performed = true;
130
- executed_actions.push("commit_task_baseline".to_string());
131
- executed_actions.push("create_task_worktree".to_string());
132
- user_message = format!(
133
- "NAOME baselined the completed task and created an isolated task worktree at {}. Continue the new task there; unrelated user edits remain untouched in the original worktree.",
134
- created.path
135
- );
136
- worktree = Some(created);
137
- }
138
- "auto_commit_harness_refresh_then_completed_task_then_create_new_task" => {
139
- let Some(split) = completed_task_harness_refresh_diff(root)? else {
140
- return Err(NaomeError::new(
141
- "Unable to split harness refresh paths from completed task diff.",
142
- ));
143
- };
144
- git_stage_only_paths(root, &split.harness_paths)?;
145
- git_commit(root, "chore(naome): baseline harness refresh")?;
146
- executed_actions.push("commit_harness_refresh_baseline".to_string());
147
-
148
- let before = git_head(root)?;
149
- git_add_completed_task_paths(root)?;
150
- git_commit(root, "chore(naome): baseline completed task")?;
151
- let after = git_head(root)?;
152
- journal_entry =
153
- append_task_journal(root, "route_auto_baseline", before, after.clone())?;
154
- mutation_performed = true;
155
- executed_actions.push("commit_task_baseline".to_string());
156
- user_message = "NAOME baselined the harness refresh and completed task, then admitted the next task.".to_string();
157
- }
158
- "auto_commit_harness_refresh_then_create_isolated_task_worktree" => {
159
- let worktree_name_head = task_worktree_name_head(root)?;
160
- preflight_isolated_task_worktree(root, prompt, &worktree_name_head)?;
161
- let Some(split) = harness_refresh_with_unrelated_diff(root)? else {
162
- return Err(NaomeError::new(
163
- "Unable to split harness refresh paths from unrelated dirty paths.",
164
- ));
165
- };
166
- git_stage_only_paths(root, &split.harness_paths)?;
167
- git_commit(root, "chore(naome): baseline harness refresh")?;
168
- let created = create_isolated_task_worktree_with_name_head(
169
- root,
170
- prompt,
171
- &worktree_name_head,
172
- )?;
173
- task_root = PathBuf::from(&created.path);
174
- mutation_performed = true;
175
- executed_actions.push("commit_harness_refresh_baseline".to_string());
176
- executed_actions.push("create_task_worktree".to_string());
177
- user_message = format!(
178
- "NAOME baselined the harness refresh and created an isolated task worktree at {}. Continue the new task there; unrelated user edits remain untouched in the original worktree.",
179
- created.path
180
- );
181
- worktree = Some(created);
182
- }
183
- "auto_commit_harness_refresh_then_create_new_task" => {
184
- let Some(diff) = harness_refresh_diff(root)? else {
185
- return Err(NaomeError::new(
186
- "Unable to find deterministic harness refresh paths.",
187
- ));
188
- };
189
- if !diff.unrelated_paths.is_empty() {
190
- return Err(NaomeError::new(
191
- "Harness refresh baseline expected no unrelated dirty paths.",
192
- ));
193
- }
194
- git_stage_only_paths(root, &diff.harness_paths)?;
195
- git_commit(root, "chore(naome): baseline harness refresh")?;
196
- mutation_performed = true;
197
- executed_actions.push("commit_harness_refresh_baseline".to_string());
198
- user_message = "NAOME baselined the harness refresh and is ready to create the next task in the same worktree.".to_string();
199
- }
200
- "auto_commit_harness_refresh_baseline" => {
201
- let Some(split) = harness_refresh_diff(root)? else {
202
- return Err(NaomeError::new(
203
- "Unable to find deterministic harness refresh paths.",
204
- ));
205
- };
206
- git_stage_only_paths(root, &split.harness_paths)?;
207
- git_commit(root, "chore(naome): baseline harness refresh")?;
208
- mutation_performed = true;
209
- executed_actions.push("commit_harness_refresh_baseline".to_string());
210
- user_message = "NAOME baselined the deterministic harness refresh and left unrelated user edits untouched.".to_string();
211
- }
212
- "auto_commit_upgrade_baseline_then_create_new_task" => {
213
- git_add_all(root)?;
214
- git_commit(root, "chore(naome): baseline setup")?;
215
- mutation_performed = true;
216
- executed_actions.push("commit_upgrade_baseline".to_string());
217
- user_message =
218
- "NAOME baselined the setup changes and is ready to create the next task."
219
- .to_string();
220
- }
221
- "commit_task_baseline" => {
222
- let before = git_head(root)?;
223
- git_add_completed_task_paths(root)?;
224
- git_commit(root, "chore(naome): baseline completed task")?;
225
- let after = git_head(root)?;
226
- journal_entry =
227
- append_task_journal(root, "naome_commit_baseline", before, after.clone())?;
228
- mutation_performed = true;
229
- executed_actions.push("commit_task_baseline".to_string());
230
- user_message =
231
- "NAOME baselined the completed task and is ready for the next request."
232
- .to_string();
233
- }
234
- "commit_upgrade_baseline" => {
235
- git_add_all(root)?;
236
- git_commit(root, "chore(naome): baseline setup")?;
237
- mutation_performed = true;
238
- executed_actions.push("commit_upgrade_baseline".to_string());
239
- }
240
- "create_isolated_task_worktree" => {
241
- let created = create_isolated_task_worktree(root, prompt)?;
242
- task_root = PathBuf::from(&created.path);
243
- mutation_performed = true;
244
- executed_actions.push("create_task_worktree".to_string());
245
- user_message = format!(
246
- "NAOME created an isolated task worktree at {}. Continue the new task there; unrelated user edits remain untouched in the original worktree.",
247
- created.path
248
- );
249
- worktree = Some(created);
250
- }
251
- "commit_user_diff_with_quality_gate" => {
252
- let paths = git::changed_paths(root)?;
253
- executed_actions.push("run_user_diff_quality_gate".to_string());
254
- match run_user_diff_quality_gate(root, &paths) {
255
- Ok(check_ids) => {
256
- git_stage_only_paths(root, &paths)?;
257
- git_commit(root, "chore(user): baseline verified user changes")?;
258
- mutation_performed = true;
259
- executed_actions.push("commit_user_diff".to_string());
260
- user_message = format!(
261
- "NAOME quality gates passed ({}) and committed only the current user-owned changed paths.",
262
- check_ids.join(", ")
263
- );
264
- }
265
- Err(error) => {
266
- route_allowed = false;
267
- human_options = vec!["review_unowned_diff".to_string()];
268
- user_message = format!(
269
- "NAOME user-diff quality gate failed, so no commit was created. {error}"
270
- );
271
- }
272
- }
273
- }
274
- "create_new_task" | "create_new_task_without_auto_baseline"
275
- if repo_state_before == "ready_for_task"
276
- && initial_decision
277
- .task
278
- .as_ref()
279
- .map(|task| task.status.clone())
280
- .as_deref()
281
- .is_some_and(|status| status == "complete") =>
282
- {
283
- journal_entry =
284
- append_task_journal(root, "external_baseline", None, git_head(root)?)?;
285
- if journal_entry.is_some() {
286
- mutation_performed = true;
287
- executed_actions.push("journal_external_task_baseline".to_string());
288
- }
289
- }
290
- _ => {}
291
- }
99
+ execute_route_policy(
100
+ root,
101
+ prompt,
102
+ &initial_decision,
103
+ &intent,
104
+ &repo_state_before,
105
+ &mut execution,
106
+ )?;
292
107
  }
293
108
 
294
- let next_decision = evaluate_decision(&task_root, options.evaluation)?;
109
+ let next_decision = evaluate_decision(&execution.task_root, options.evaluation)?;
295
110
  let can_create_task = can_create_task(&intent, &next_decision, options.execute);
296
111
  let required_context = required_context_for_route(&intent, &next_decision);
297
112
  let mut internal_notes = intent.internal_notes.clone();
298
113
  internal_notes.push(format!("route_execute:{}", options.execute));
299
- if mutation_performed {
114
+ if execution.mutation_performed {
300
115
  internal_notes.push("route_mutation_performed:true".to_string());
301
116
  }
302
- if worktree.is_some() {
117
+ if execution.worktree.is_some() {
303
118
  internal_notes.push("route_task_root:isolation_worktree".to_string());
304
119
  }
305
120
 
@@ -309,17 +124,17 @@ pub fn evaluate_route(
309
124
  repo_state_before,
310
125
  prompt_intent: intent.prompt_intent.clone(),
311
126
  policy_action: intent.policy_action.clone(),
312
- allowed: route_allowed,
313
- mutation_performed,
314
- executed_actions,
127
+ allowed: execution.route_allowed,
128
+ mutation_performed: execution.mutation_performed,
129
+ executed_actions: execution.executed_actions,
315
130
  can_create_task,
316
- user_message,
317
- human_options,
131
+ user_message: execution.user_message,
132
+ human_options: execution.human_options,
318
133
  internal_notes,
319
134
  required_context,
320
- task_root: task_root.to_string_lossy().to_string(),
321
- worktree,
322
- journal_entry,
135
+ task_root: execution.task_root.to_string_lossy().to_string(),
136
+ worktree: execution.worktree,
137
+ journal_entry: execution.journal_entry,
323
138
  next_decision,
324
139
  intent,
325
140
  })
@@ -348,991 +163,3 @@ pub fn explain_route(
348
163
  intent,
349
164
  })
350
165
  }
351
-
352
- fn can_create_task(intent: &IntentDecision, decision: &Decision, execute: bool) -> bool {
353
- let creation_intent = matches!(
354
- intent.policy_action.as_str(),
355
- "create_new_task"
356
- | "create_new_task_without_auto_baseline"
357
- | "auto_commit_completed_task_then_create_new_task"
358
- | "auto_commit_completed_task_then_create_isolated_task_worktree"
359
- | "auto_commit_harness_refresh_then_completed_task_then_create_new_task"
360
- | "auto_commit_harness_refresh_then_create_new_task"
361
- | "auto_commit_harness_refresh_then_create_isolated_task_worktree"
362
- | "auto_commit_upgrade_baseline_then_create_new_task"
363
- | "create_isolated_task_worktree"
364
- );
365
- if !creation_intent || decision.state != "ready_for_task" || decision.blocked {
366
- return false;
367
- }
368
-
369
- execute
370
- || matches!(
371
- intent.policy_action.as_str(),
372
- "create_new_task" | "create_new_task_without_auto_baseline"
373
- )
374
- }
375
-
376
- fn winning_rule(intent: &IntentDecision) -> String {
377
- match intent.policy_action.as_str() {
378
- "block_unsafe_intent" => "unsafe_intent_precedence",
379
- "block_auto_baseline_due_to_no_commit" => "no_commit_blocks_auto_baseline",
380
- "continue_current_task_without_commit" => "no_commit_continues_active_task",
381
- "review_task_diff" | "review_diff_first" | "review_current_task_diff" => {
382
- "explicit_review_overrides_auto_baseline"
383
- }
384
- "cancel_task_changes" | "cancel_upgrade_baseline" | "cancel_current_task" => {
385
- "explicit_cancel_overrides_auto_baseline"
386
- }
387
- "auto_commit_completed_task_then_create_new_task" => {
388
- "completed_task_valid_new_task_auto_baseline"
389
- }
390
- "auto_commit_completed_task_then_create_isolated_task_worktree" => {
391
- "completed_task_valid_with_unrelated_dirty_worktree"
392
- }
393
- "auto_commit_harness_refresh_then_completed_task_then_create_new_task" => {
394
- "completed_task_harness_refresh_split_auto_baseline"
395
- }
396
- "auto_commit_harness_refresh_then_create_new_task" => {
397
- "pure_harness_refresh_new_task_auto_baseline"
398
- }
399
- "auto_commit_harness_refresh_then_create_isolated_task_worktree" => {
400
- "dirty_repo_harness_refresh_worktree_isolation"
401
- }
402
- "auto_commit_harness_refresh_baseline" => "dirty_repo_harness_refresh_repair_baseline",
403
- "auto_commit_upgrade_baseline_then_create_new_task" => "setup_diff_new_task_auto_baseline",
404
- "reopen_completed_task_revision" | "continue_current_task" => {
405
- "current_task_revision_continues_task"
406
- }
407
- "answer_status_only" => "status_request_read_only",
408
- "create_new_task" | "create_new_task_without_auto_baseline" => "ready_repo_new_task",
409
- "create_isolated_task_worktree" => "dirty_repo_new_task_worktree_isolation",
410
- "commit_user_diff_with_quality_gate" => "explicit_user_diff_commit_quality_gate",
411
- _ => "fallback_policy",
412
- }
413
- .to_string()
414
- }
415
-
416
- fn discarded_actions(intent: &IntentDecision) -> Vec<String> {
417
- let Some(actions_note) = intent
418
- .internal_notes
419
- .iter()
420
- .find(|note| note.starts_with("internal_allowed_actions:"))
421
- else {
422
- return Vec::new();
423
- };
424
- let selected = selected_internal_action(&intent.policy_action);
425
- actions_note
426
- .trim_start_matches("internal_allowed_actions:")
427
- .split(',')
428
- .filter(|action| !action.is_empty() && Some(*action) != selected.as_deref())
429
- .map(ToString::to_string)
430
- .collect()
431
- }
432
-
433
- fn selected_internal_action(policy_action: &str) -> Option<String> {
434
- match policy_action {
435
- "auto_commit_completed_task_then_create_new_task"
436
- | "auto_commit_completed_task_then_create_isolated_task_worktree"
437
- | "commit_task_baseline" => Some("commit_task_baseline".to_string()),
438
- "auto_commit_harness_refresh_then_create_new_task"
439
- | "auto_commit_harness_refresh_then_create_isolated_task_worktree"
440
- | "auto_commit_harness_refresh_baseline" => Some("commit_upgrade_baseline".to_string()),
441
- "auto_commit_harness_refresh_then_completed_task_then_create_new_task" => {
442
- Some("commit_task_baseline".to_string())
443
- }
444
- "auto_commit_upgrade_baseline_then_create_new_task" | "commit_upgrade_baseline" => {
445
- Some("commit_upgrade_baseline".to_string())
446
- }
447
- other if other.starts_with("review_") || other.starts_with("cancel_") => {
448
- Some(other.to_string())
449
- }
450
- _ => None,
451
- }
452
- }
453
-
454
- fn would_mutate(intent: &IntentDecision) -> bool {
455
- matches!(
456
- intent.policy_action.as_str(),
457
- "auto_commit_completed_task_then_create_new_task"
458
- | "auto_commit_completed_task_then_create_isolated_task_worktree"
459
- | "auto_commit_upgrade_baseline_then_create_new_task"
460
- | "auto_commit_harness_refresh_then_completed_task_then_create_new_task"
461
- | "auto_commit_harness_refresh_then_create_new_task"
462
- | "auto_commit_harness_refresh_then_create_isolated_task_worktree"
463
- | "auto_commit_harness_refresh_baseline"
464
- | "create_isolated_task_worktree"
465
- | "commit_task_baseline"
466
- | "commit_upgrade_baseline"
467
- | "commit_user_diff_with_quality_gate"
468
- )
469
- }
470
-
471
- fn required_context_for_route(intent: &IntentDecision, decision: &Decision) -> Vec<String> {
472
- let mut context = required_context_for_intent(intent);
473
- if decision.state == "ready_for_task" {
474
- push_unique(&mut context, "docs/naome/agent-workflow.md");
475
- push_unique(&mut context, "docs/naome/testing.md");
476
- push_unique(&mut context, ".naome/verification.json");
477
- }
478
- context
479
- }
480
-
481
- fn required_context_for_intent(intent: &IntentDecision) -> Vec<String> {
482
- let mut context = intent.required_context.clone();
483
- match intent.policy_action.as_str() {
484
- "create_new_task" | "create_new_task_without_auto_baseline" => {
485
- push_unique(&mut context, "docs/naome/agent-workflow.md");
486
- push_unique(&mut context, "docs/naome/testing.md");
487
- push_unique(&mut context, ".naome/verification.json");
488
- push_unique(&mut context, "docs/naome/architecture.md");
489
- }
490
- "create_isolated_task_worktree" => {
491
- push_unique(&mut context, "docs/naome/agent-workflow.md");
492
- push_unique(&mut context, "docs/naome/testing.md");
493
- push_unique(&mut context, ".naome/verification.json");
494
- push_unique(&mut context, "docs/naome/architecture.md");
495
- }
496
- "auto_commit_completed_task_then_create_new_task"
497
- | "auto_commit_completed_task_then_create_isolated_task_worktree"
498
- | "auto_commit_harness_refresh_then_create_new_task"
499
- | "auto_commit_harness_refresh_then_create_isolated_task_worktree"
500
- | "auto_commit_harness_refresh_baseline"
501
- | "auto_commit_harness_refresh_then_completed_task_then_create_new_task"
502
- | "reopen_completed_task_revision"
503
- | "review_task_diff"
504
- | "cancel_task_changes"
505
- | "block_auto_baseline_due_to_no_commit" => {
506
- push_unique(&mut context, "docs/naome/execution.md");
507
- push_unique(&mut context, ".naome/task-state.json");
508
- }
509
- "answer_status_only" => {
510
- push_unique(&mut context, "docs/naome/index.md");
511
- }
512
- "repair_harness_only" => {
513
- push_unique(&mut context, ".naome/manifest.json");
514
- push_unique(&mut context, "docs/naome/index.md");
515
- }
516
- _ => {}
517
- }
518
- context
519
- }
520
-
521
- fn push_unique(context: &mut Vec<String>, value: &str) {
522
- if !context.iter().any(|entry| entry == value) {
523
- context.push(value.to_string());
524
- }
525
- }
526
-
527
- fn git_head(root: &Path) -> Result<Option<String>, NaomeError> {
528
- let output = Command::new("git")
529
- .args(["rev-parse", "HEAD"])
530
- .current_dir(root)
531
- .output()?;
532
- if !output.status.success() {
533
- return Ok(None);
534
- }
535
- Ok(Some(
536
- String::from_utf8_lossy(&output.stdout).trim().to_string(),
537
- ))
538
- }
539
-
540
- fn git_add_all(root: &Path) -> Result<(), NaomeError> {
541
- let output = Command::new("git")
542
- .args(["add", "-A"])
543
- .current_dir(root)
544
- .output()?;
545
- if output.status.success() {
546
- Ok(())
547
- } else {
548
- Err(NaomeError::new(command_output(&output)))
549
- }
550
- }
551
-
552
- fn git_add_completed_task_paths(root: &Path) -> Result<(), NaomeError> {
553
- let paths = completed_task_commit_paths(root)?;
554
- if paths.is_empty() {
555
- return Err(NaomeError::new(
556
- "No task-owned paths are available to commit.",
557
- ));
558
- }
559
- git_stage_only_paths(root, &paths)
560
- }
561
-
562
- fn git_stage_only_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
563
- let output = Command::new("git")
564
- .args(["reset", "-q", "--", "."])
565
- .current_dir(root)
566
- .output()?;
567
- if !output.status.success() {
568
- return Err(NaomeError::new(command_output(&output)));
569
- }
570
- git_add_paths(root, paths)
571
- }
572
-
573
- fn git_add_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
574
- if paths.is_empty() {
575
- return Ok(());
576
- }
577
- let mut args = vec!["add", "--"];
578
- args.extend(paths.iter().map(String::as_str));
579
- let output = Command::new("git").args(args).current_dir(root).output()?;
580
- if output.status.success() {
581
- Ok(())
582
- } else {
583
- Err(NaomeError::new(command_output(&output)))
584
- }
585
- }
586
-
587
- fn run_user_diff_quality_gate(
588
- root: &Path,
589
- changed_paths: &[String],
590
- ) -> Result<Vec<String>, NaomeError> {
591
- if changed_paths.is_empty() {
592
- return Err(NaomeError::new("No changed paths are available to commit."));
593
- }
594
- let verification = read_head_verification(root)?;
595
- let checks = verification_checks(&verification);
596
- let check_ids = user_diff_check_ids(&verification, changed_paths, &checks)?;
597
-
598
- let initial_paths = sorted_path_set(changed_paths);
599
- let mut previous_snapshot = changed_path_snapshot(root, changed_paths)?;
600
- for _ in 0..3 {
601
- validate_changed_text_whitespace(root, changed_paths)?;
602
-
603
- for check_id in &check_ids {
604
- let Some(check) = checks.get(check_id.as_str()) else {
605
- return Err(NaomeError::new(format!(
606
- "Quality check {check_id} is referenced but not defined."
607
- )));
608
- };
609
- run_quality_check(root, check_id, check)?;
610
- }
611
-
612
- let current_paths = git::changed_paths(root)?;
613
- let current_set = sorted_path_set(&current_paths);
614
- if current_set != initial_paths {
615
- return Err(NaomeError::new(format!(
616
- "Quality checks changed the diff path set from [{}] to [{}].",
617
- initial_paths.iter().cloned().collect::<Vec<_>>().join(", "),
618
- current_set.iter().cloned().collect::<Vec<_>>().join(", ")
619
- )));
620
- }
621
-
622
- validate_changed_text_whitespace(root, changed_paths)?;
623
- if let Some(check) = checks.get("diff-check") {
624
- run_quality_check(root, "diff-check", check)?;
625
- }
626
- let current_paths = git::changed_paths(root)?;
627
- let current_set = sorted_path_set(&current_paths);
628
- if current_set != initial_paths {
629
- return Err(NaomeError::new(format!(
630
- "Quality checks changed the diff path set from [{}] to [{}].",
631
- initial_paths.iter().cloned().collect::<Vec<_>>().join(", "),
632
- current_set.iter().cloned().collect::<Vec<_>>().join(", ")
633
- )));
634
- }
635
-
636
- let next_snapshot = changed_path_snapshot(root, changed_paths)?;
637
- if next_snapshot == previous_snapshot {
638
- return Ok(check_ids);
639
- }
640
- previous_snapshot = next_snapshot;
641
- }
642
-
643
- Err(NaomeError::new(
644
- "Quality checks did not stabilize the user-owned diff after three runs.",
645
- ))
646
- }
647
-
648
- fn validate_changed_text_whitespace(
649
- root: &Path,
650
- changed_paths: &[String],
651
- ) -> Result<(), NaomeError> {
652
- for relative_path in changed_paths {
653
- let path = root.join(relative_path);
654
- if !path.is_file() {
655
- continue;
656
- }
657
- let Ok(content) = fs::read_to_string(&path) else {
658
- continue;
659
- };
660
- for (index, line) in content.lines().enumerate() {
661
- if line.ends_with(' ') || line.ends_with('\t') {
662
- return Err(NaomeError::new(format!(
663
- "{relative_path}:{} has trailing whitespace.",
664
- index + 1
665
- )));
666
- }
667
- }
668
- }
669
- Ok(())
670
- }
671
-
672
- fn read_head_verification(root: &Path) -> Result<Value, NaomeError> {
673
- let output = Command::new("git")
674
- .args(["show", "HEAD:.naome/verification.json"])
675
- .current_dir(root)
676
- .output()?;
677
-
678
- if !output.status.success() {
679
- return Err(NaomeError::new(
680
- "No committed NAOME verification profile is available for user-diff quality gating.",
681
- ));
682
- }
683
-
684
- Ok(serde_json::from_slice(&output.stdout)?)
685
- }
686
-
687
- #[derive(Debug, Clone)]
688
- struct QualityCheck {
689
- command: String,
690
- cwd: String,
691
- }
692
-
693
- fn verification_checks(verification: &Value) -> HashMap<&str, QualityCheck> {
694
- let mut checks = HashMap::new();
695
- let Some(items) = verification.get("checks").and_then(Value::as_array) else {
696
- return checks;
697
- };
698
-
699
- for item in items {
700
- let Some(id) = item.get("id").and_then(Value::as_str) else {
701
- continue;
702
- };
703
- let Some(command) = item.get("command").and_then(Value::as_str) else {
704
- continue;
705
- };
706
- let cwd = item
707
- .get("cwd")
708
- .and_then(Value::as_str)
709
- .unwrap_or(".")
710
- .to_string();
711
- checks.insert(
712
- id,
713
- QualityCheck {
714
- command: command.to_string(),
715
- cwd,
716
- },
717
- );
718
- }
719
-
720
- checks
721
- }
722
-
723
- fn user_diff_check_ids(
724
- verification: &Value,
725
- changed_paths: &[String],
726
- checks: &HashMap<&str, QualityCheck>,
727
- ) -> Result<Vec<String>, NaomeError> {
728
- let mut ids = Vec::new();
729
- if checks.contains_key("diff-check") {
730
- push_unique_string(&mut ids, "diff-check");
731
- }
732
- if checks.contains_key("naome-harness-health") {
733
- push_unique_string(&mut ids, "naome-harness-health");
734
- }
735
-
736
- let Some(change_types) = verification.get("changeTypes").and_then(Value::as_array) else {
737
- return Err(NaomeError::new(
738
- "No quality coverage is configured for user-owned changed paths.",
739
- ));
740
- };
741
-
742
- if change_types.is_empty() {
743
- return Err(NaomeError::new(
744
- "No quality coverage is configured for user-owned changed paths.",
745
- ));
746
- }
747
-
748
- let mut uncovered_paths = Vec::new();
749
-
750
- for change_type in change_types {
751
- let patterns = string_array(change_type.get("paths"));
752
- if patterns.is_empty()
753
- || !changed_paths
754
- .iter()
755
- .any(|path| paths::matches_any(path, &patterns))
756
- {
757
- continue;
758
- }
759
-
760
- for check_id in string_array(change_type.get("requiredChecks")) {
761
- push_unique_string(&mut ids, &check_id);
762
- }
763
- }
764
-
765
- for path in changed_paths {
766
- let covered = change_types.iter().any(|change_type| {
767
- let patterns = string_array(change_type.get("paths"));
768
- !patterns.is_empty() && paths::matches_any(path, &patterns)
769
- });
770
- if !covered {
771
- uncovered_paths.push(path.clone());
772
- }
773
- }
774
-
775
- if !uncovered_paths.is_empty() {
776
- return Err(NaomeError::new(format!(
777
- "No quality coverage is configured for changed path(s): {}.",
778
- uncovered_paths.join(", ")
779
- )));
780
- }
781
-
782
- if ids.is_empty() {
783
- return Err(NaomeError::new(
784
- "No quality checks are configured for these changed paths.",
785
- ));
786
- }
787
-
788
- Ok(ids)
789
- }
790
-
791
- fn sorted_path_set(paths: &[String]) -> BTreeSet<String> {
792
- paths.iter().cloned().collect()
793
- }
794
-
795
- fn changed_path_snapshot(
796
- root: &Path,
797
- changed_paths: &[String],
798
- ) -> Result<Vec<(String, Option<Vec<u8>>)>, NaomeError> {
799
- let mut snapshot = Vec::new();
800
- for relative_path in changed_paths {
801
- let path = root.join(relative_path);
802
- let content = if path.is_file() {
803
- Some(fs::read(&path)?)
804
- } else {
805
- None
806
- };
807
- snapshot.push((relative_path.clone(), content));
808
- }
809
- snapshot.sort_by(|left, right| left.0.cmp(&right.0));
810
- Ok(snapshot)
811
- }
812
-
813
- fn string_array(value: Option<&Value>) -> Vec<String> {
814
- value
815
- .and_then(Value::as_array)
816
- .map(|items| {
817
- items
818
- .iter()
819
- .filter_map(Value::as_str)
820
- .map(ToString::to_string)
821
- .collect()
822
- })
823
- .unwrap_or_default()
824
- }
825
-
826
- fn push_unique_string(values: &mut Vec<String>, value: &str) {
827
- if !values.iter().any(|item| item == value) {
828
- values.push(value.to_string());
829
- }
830
- }
831
-
832
- fn run_quality_check(root: &Path, check_id: &str, check: &QualityCheck) -> Result<(), NaomeError> {
833
- match check_id {
834
- "installer-tests" => require_builtin_quality_check(
835
- check_id,
836
- check,
837
- "npm run test:naome-installer",
838
- ),
839
- "rust-build" => require_builtin_quality_check(check_id, check, "npm run build:rust"),
840
- "decision-engine-tests" => {
841
- require_builtin_quality_check(check_id, check, "npm run test:decision-engine")
842
- }
843
- "package-dry-run" => require_builtin_quality_check(check_id, check, "npm run pack:dry-run"),
844
- "diff-check" => {
845
- require_builtin_quality_check(check_id, check, "git diff --check")?;
846
- let output = Command::new("git")
847
- .args(["diff", "--check"])
848
- .current_dir(root)
849
- .output()?;
850
-
851
- if output.status.success() {
852
- Ok(())
853
- } else {
854
- Err(NaomeError::new(command_output(&output)))
855
- }
856
- }
857
- "naome-harness-health" => {
858
- require_builtin_quality_check(
859
- check_id,
860
- check,
861
- "node .naome/bin/check-harness-health.js",
862
- )?;
863
- run_harness_health_check(root)
864
- }
865
- "dogfood-health" => {
866
- require_builtin_quality_check(check_id, check, "npm run dogfood:health")?;
867
- run_harness_health_check(root)
868
- }
869
- "task-state-check" => {
870
- require_builtin_quality_check(check_id, check, "npm run check:task-state")?;
871
- run_template_task_state_check(root)
872
- }
873
- "verification-contract-check" => {
874
- require_builtin_quality_check(
875
- check_id,
876
- check,
877
- "npm run check:verification-contract",
878
- )?;
879
- run_template_verification_contract_check(root)
880
- }
881
- "context-budget-check" => {
882
- require_builtin_quality_check(check_id, check, "npm run check:context-budget")?;
883
- run_context_budget_check(root)
884
- }
885
- "repository-quality-check" => {
886
- require_builtin_quality_check_any(
887
- check_id,
888
- check,
889
- &[
890
- "naome quality check --changed",
891
- "node .naome/bin/naome.js quality check --changed",
892
- "npm run check:repository-quality",
893
- ],
894
- )?;
895
- run_repository_quality_check(root)
896
- }
897
- _ => Err(NaomeError::new(format!(
898
- "Quality check {check_id} is not a built-in safe check; NAOME will not execute repository-controlled verification commands."
899
- ))),
900
- }
901
- }
902
-
903
- fn require_builtin_quality_check_any(
904
- check_id: &str,
905
- check: &QualityCheck,
906
- expected_commands: &[&str],
907
- ) -> Result<(), NaomeError> {
908
- if check.cwd == "."
909
- && expected_commands
910
- .iter()
911
- .any(|expected_command| check.command == *expected_command)
912
- {
913
- return Ok(());
914
- }
915
-
916
- Err(NaomeError::new(format!(
917
- "Quality check {check_id} has an unsafe command or cwd; expected one of [{}] with cwd `.`.",
918
- expected_commands
919
- .iter()
920
- .map(|command| format!("`{command}`"))
921
- .collect::<Vec<_>>()
922
- .join(", ")
923
- )))
924
- }
925
-
926
- fn require_builtin_quality_check(
927
- check_id: &str,
928
- check: &QualityCheck,
929
- expected_command: &str,
930
- ) -> Result<(), NaomeError> {
931
- if check.cwd == "." && check.command == expected_command {
932
- return Ok(());
933
- }
934
-
935
- Err(NaomeError::new(format!(
936
- "Quality check {check_id} has an unsafe command or cwd; expected command `{expected_command}` with cwd `.`."
937
- )))
938
- }
939
-
940
- fn run_repository_quality_check(root: &Path) -> Result<(), NaomeError> {
941
- let report = check_repository_quality(root, QualityMode::Changed)?;
942
- if report.ok {
943
- return Ok(());
944
- }
945
-
946
- let details = report
947
- .violations
948
- .iter()
949
- .take(20)
950
- .map(|violation| {
951
- let location = violation
952
- .line
953
- .map(|line| format!("{}:{line}", violation.path))
954
- .unwrap_or_else(|| violation.path.clone());
955
- format!("{location} {}: {}", violation.check_id, violation.message)
956
- })
957
- .collect::<Vec<_>>()
958
- .join("\n");
959
- Err(NaomeError::new(format!(
960
- "repository-quality-check failed with {} violation(s).\n{}",
961
- report.violations.len(),
962
- details
963
- )))
964
- }
965
-
966
- fn run_harness_health_check(root: &Path) -> Result<(), NaomeError> {
967
- let errors = validate_harness_health(
968
- root,
969
- HarnessHealthOptions {
970
- expected_integrity: packaged_harness_integrity()?,
971
- ..HarnessHealthOptions::default()
972
- },
973
- )?;
974
- if errors.is_empty() {
975
- Ok(())
976
- } else {
977
- Err(NaomeError::new(errors.join("\n")))
978
- }
979
- }
980
-
981
- fn packaged_harness_integrity() -> Result<std::collections::HashMap<String, String>, NaomeError> {
982
- const CHECKER: &str =
983
- include_str!("../../../templates/naome-root/.naome/bin/check-harness-health.js");
984
- let start_marker = "const expectedMachineOwnedIntegrity = Object.freeze({";
985
- let start = CHECKER
986
- .find(start_marker)
987
- .ok_or_else(|| NaomeError::new("Packaged harness integrity block is missing."))?;
988
- let body_start = start + start_marker.len();
989
- let end = CHECKER[body_start..]
990
- .find("\n});")
991
- .map(|offset| body_start + offset)
992
- .ok_or_else(|| NaomeError::new("Packaged harness integrity block is incomplete."))?;
993
-
994
- let mut integrity = std::collections::HashMap::new();
995
- for line in CHECKER[body_start..end].lines() {
996
- let line = line.trim().trim_end_matches(',').trim();
997
- if line.is_empty() {
998
- continue;
999
- }
1000
- let Some((path, hash)) = line.split_once(':') else {
1001
- return Err(NaomeError::new(format!(
1002
- "Packaged harness integrity entry is invalid: {line}"
1003
- )));
1004
- };
1005
- let path: String = serde_json::from_str(path.trim()).map_err(|error| {
1006
- NaomeError::new(format!(
1007
- "Packaged harness integrity path is invalid: {error}"
1008
- ))
1009
- })?;
1010
- let hash: String = serde_json::from_str(hash.trim()).map_err(|error| {
1011
- NaomeError::new(format!(
1012
- "Packaged harness integrity hash is invalid: {error}"
1013
- ))
1014
- })?;
1015
- integrity.insert(path, hash);
1016
- }
1017
-
1018
- if integrity.is_empty() {
1019
- return Err(NaomeError::new(
1020
- "Packaged harness integrity block is empty.",
1021
- ));
1022
- }
1023
-
1024
- Ok(integrity)
1025
- }
1026
-
1027
- fn run_template_task_state_check(root: &Path) -> Result<(), NaomeError> {
1028
- let template_root = template_root(root);
1029
- let report = validate_task_state(
1030
- &template_root,
1031
- TaskStateOptions {
1032
- mode: TaskStateMode::State,
1033
- harness_health: Some(HarnessHealthOptions {
1034
- expected_integrity: packaged_harness_integrity()?,
1035
- allow_missing_archive: true,
1036
- ..HarnessHealthOptions::default()
1037
- }),
1038
- },
1039
- )?;
1040
- if report.errors.is_empty() {
1041
- Ok(())
1042
- } else {
1043
- Err(NaomeError::new(report.errors.join("\n")))
1044
- }
1045
- }
1046
-
1047
- fn run_template_verification_contract_check(root: &Path) -> Result<(), NaomeError> {
1048
- let errors = validate_verification_contract(&template_root(root))?;
1049
- if errors.is_empty() {
1050
- Ok(())
1051
- } else {
1052
- Err(NaomeError::new(errors.join("\n")))
1053
- }
1054
- }
1055
-
1056
- fn run_context_budget_check(root: &Path) -> Result<(), NaomeError> {
1057
- let template_root = template_root(root);
1058
- let mut context_files = vec![
1059
- template_root.join("AGENTS.md"),
1060
- template_root.join(".naomeignore"),
1061
- ];
1062
- context_files.extend(markdown_files(&template_root.join("docs").join("naome"))?);
1063
- context_files.sort();
1064
-
1065
- let mut errors = Vec::new();
1066
- for path in context_files {
1067
- let content = fs::read_to_string(&path)?;
1068
- let line_count = count_lines(&content);
1069
- if line_count > 200 {
1070
- errors.push(format!(
1071
- "{}: {line_count} lines",
1072
- display_repo_path(root, &path)
1073
- ));
1074
- }
1075
- }
1076
-
1077
- if errors.is_empty() {
1078
- Ok(())
1079
- } else {
1080
- Err(NaomeError::new(format!(
1081
- "NAOME context budget exceeded. Limit: 200 lines per file.\n{}",
1082
- errors.join("\n")
1083
- )))
1084
- }
1085
- }
1086
-
1087
- fn template_root(root: &Path) -> PathBuf {
1088
- root.join("packages")
1089
- .join("naome")
1090
- .join("templates")
1091
- .join("naome-root")
1092
- }
1093
-
1094
- fn markdown_files(dir: &Path) -> Result<Vec<PathBuf>, NaomeError> {
1095
- let mut files = Vec::new();
1096
- for entry in fs::read_dir(dir)? {
1097
- let entry = entry?;
1098
- let path = entry.path();
1099
- if path.is_dir() {
1100
- files.extend(markdown_files(&path)?);
1101
- } else if path.is_file() && path.extension().is_some_and(|extension| extension == "md") {
1102
- files.push(path);
1103
- }
1104
- }
1105
- Ok(files)
1106
- }
1107
-
1108
- fn count_lines(content: &str) -> usize {
1109
- if content.is_empty() {
1110
- 0
1111
- } else if content.ends_with('\n') {
1112
- content.split('\n').count() - 1
1113
- } else {
1114
- content.split('\n').count()
1115
- }
1116
- }
1117
-
1118
- fn display_repo_path(root: &Path, path: &Path) -> String {
1119
- path.strip_prefix(root)
1120
- .unwrap_or(path)
1121
- .to_string_lossy()
1122
- .to_string()
1123
- }
1124
-
1125
- fn git_commit(root: &Path, message: &str) -> Result<(), NaomeError> {
1126
- let output = Command::new("git")
1127
- .args(["commit", "-m", message])
1128
- .current_dir(root)
1129
- .output()?;
1130
- if output.status.success() {
1131
- Ok(())
1132
- } else {
1133
- Err(NaomeError::new(command_output(&output)))
1134
- }
1135
- }
1136
-
1137
- fn create_isolated_task_worktree(root: &Path, prompt: &str) -> Result<RouteWorktree, NaomeError> {
1138
- let name_head = task_worktree_name_head(root)?;
1139
- create_isolated_task_worktree_with_name_head(root, prompt, &name_head)
1140
- }
1141
-
1142
- fn create_isolated_task_worktree_with_name_head(
1143
- root: &Path,
1144
- prompt: &str,
1145
- name_head: &str,
1146
- ) -> Result<RouteWorktree, NaomeError> {
1147
- let base_head = git_head(root)?.ok_or_else(|| {
1148
- NaomeError::new("Cannot create task worktree because the repository has no HEAD.")
1149
- })?;
1150
- let short_head = name_head.chars().take(12).collect::<String>();
1151
- let slug = prompt_slug(prompt);
1152
- let common_git_dir = git_common_dir(root)?;
1153
- let worktree_base = common_git_dir.join("naome").join("worktrees");
1154
- fs::create_dir_all(&worktree_base)?;
1155
- let worktree_count = existing_naome_task_worktree_count(&worktree_base)?;
1156
- if worktree_count >= MAX_NAOME_TASK_WORKTREES {
1157
- return Err(NaomeError::new(format!(
1158
- "Too many NAOME task worktrees are present ({worktree_count}). Finish or remove old task worktrees before creating another isolated task worktree."
1159
- )));
1160
- }
1161
-
1162
- for attempt in 1..100 {
1163
- let suffix = if attempt == 1 {
1164
- String::new()
1165
- } else {
1166
- format!("-{attempt}")
1167
- };
1168
- let name = format!("{slug}-{short_head}{suffix}");
1169
- let branch = format!("naome/task/{name}");
1170
- let path = worktree_base.join(&name);
1171
- if path.exists() || git_branch_exists(root, &branch)? {
1172
- continue;
1173
- }
1174
-
1175
- let path_text = path.to_string_lossy().to_string();
1176
- let output = Command::new("git")
1177
- .args(["worktree", "add", "-b", &branch, &path_text, "HEAD"])
1178
- .current_dir(root)
1179
- .output()?;
1180
- if !output.status.success() {
1181
- return Err(NaomeError::new(command_output(&output)));
1182
- }
1183
-
1184
- copy_local_harness_files(root, &path)?;
1185
-
1186
- return Ok(RouteWorktree {
1187
- path: path_text,
1188
- branch,
1189
- base_head,
1190
- source_root: root.to_string_lossy().to_string(),
1191
- });
1192
- }
1193
-
1194
- Err(NaomeError::new(
1195
- "Cannot create a unique NAOME task worktree after 99 attempts.",
1196
- ))
1197
- }
1198
-
1199
- fn preflight_isolated_task_worktree(
1200
- root: &Path,
1201
- prompt: &str,
1202
- name_head: &str,
1203
- ) -> Result<(), NaomeError> {
1204
- let short_head = name_head.chars().take(12).collect::<String>();
1205
- let slug = prompt_slug(prompt);
1206
- let common_git_dir = git_common_dir(root)?;
1207
- let worktree_base = common_git_dir.join("naome").join("worktrees");
1208
- fs::create_dir_all(&worktree_base)?;
1209
- let worktree_count = existing_naome_task_worktree_count(&worktree_base)?;
1210
- if worktree_count >= MAX_NAOME_TASK_WORKTREES {
1211
- return Err(NaomeError::new(format!(
1212
- "Too many NAOME task worktrees are present ({worktree_count}). Finish or remove old task worktrees before creating another isolated task worktree."
1213
- )));
1214
- }
1215
-
1216
- for attempt in 1..100 {
1217
- let suffix = if attempt == 1 {
1218
- String::new()
1219
- } else {
1220
- format!("-{attempt}")
1221
- };
1222
- let name = format!("{slug}-{short_head}{suffix}");
1223
- let branch = format!("naome/task/{name}");
1224
- let path = worktree_base.join(&name);
1225
- if !path.exists() && !git_branch_exists(root, &branch)? {
1226
- return Ok(());
1227
- }
1228
- }
1229
-
1230
- Err(NaomeError::new(
1231
- "Cannot create a unique NAOME task worktree after 99 attempts.",
1232
- ))
1233
- }
1234
-
1235
- fn task_worktree_name_head(root: &Path) -> Result<String, NaomeError> {
1236
- git_head(root)?.ok_or_else(|| {
1237
- NaomeError::new("Cannot create task worktree because the repository has no HEAD.")
1238
- })
1239
- }
1240
-
1241
- fn existing_naome_task_worktree_count(worktree_base: &Path) -> Result<usize, NaomeError> {
1242
- let mut count = 0;
1243
- for entry in fs::read_dir(worktree_base)? {
1244
- let entry = entry?;
1245
- if entry.file_type()?.is_dir() {
1246
- count += 1;
1247
- }
1248
- }
1249
- Ok(count)
1250
- }
1251
-
1252
- fn git_common_dir(root: &Path) -> Result<PathBuf, NaomeError> {
1253
- let output = Command::new("git")
1254
- .args(["rev-parse", "--git-common-dir"])
1255
- .current_dir(root)
1256
- .output()?;
1257
- if !output.status.success() {
1258
- return Err(NaomeError::new(command_output(&output)));
1259
- }
1260
-
1261
- let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
1262
- let path = PathBuf::from(value);
1263
- if path.is_absolute() {
1264
- Ok(path)
1265
- } else {
1266
- Ok(root.join(path))
1267
- }
1268
- }
1269
-
1270
- fn git_branch_exists(root: &Path, branch: &str) -> Result<bool, NaomeError> {
1271
- let ref_name = format!("refs/heads/{branch}");
1272
- let output = Command::new("git")
1273
- .args(["show-ref", "--verify", "--quiet", &ref_name])
1274
- .current_dir(root)
1275
- .output()?;
1276
- match output.status.code() {
1277
- Some(0) => Ok(true),
1278
- Some(1) => Ok(false),
1279
- _ => Err(NaomeError::new(command_output(&output))),
1280
- }
1281
- }
1282
-
1283
- fn copy_local_harness_files(source_root: &Path, worktree_root: &Path) -> Result<(), NaomeError> {
1284
- let mut paths = Vec::new();
1285
- paths.extend_from_slice(LOCAL_ONLY_MACHINE_OWNED_PATHS);
1286
- paths.extend_from_slice(LOCAL_NATIVE_BINARY_PATHS);
1287
-
1288
- for relative_path in paths {
1289
- if relative_path == ".naome/archive" || relative_path == ".naome/task-journal.jsonl" {
1290
- continue;
1291
- }
1292
-
1293
- let source = source_root.join(relative_path);
1294
- if !source.is_file() {
1295
- continue;
1296
- }
1297
- let destination = worktree_root.join(relative_path);
1298
- if let Some(parent) = destination.parent() {
1299
- fs::create_dir_all(parent)?;
1300
- }
1301
- fs::copy(&source, &destination)?;
1302
- }
1303
-
1304
- Ok(())
1305
- }
1306
-
1307
- fn prompt_slug(prompt: &str) -> String {
1308
- let mut slug = String::new();
1309
- let mut previous_dash = false;
1310
- for character in prompt.chars().flat_map(char::to_lowercase) {
1311
- if character.is_ascii_alphanumeric() {
1312
- slug.push(character);
1313
- previous_dash = false;
1314
- } else if !previous_dash && !slug.is_empty() {
1315
- slug.push('-');
1316
- previous_dash = true;
1317
- }
1318
-
1319
- if slug.len() >= 40 {
1320
- break;
1321
- }
1322
- }
1323
-
1324
- let slug = slug.trim_matches('-').to_string();
1325
- if slug.is_empty() {
1326
- "task".to_string()
1327
- } else {
1328
- slug
1329
- }
1330
- }
1331
-
1332
- fn command_output(output: &std::process::Output) -> String {
1333
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1334
- if !stderr.is_empty() {
1335
- return stderr;
1336
- }
1337
- String::from_utf8_lossy(&output.stdout).trim().to_string()
1338
- }