@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.
- package/Cargo.lock +2 -2
- package/bin/naome-node.js +2 -1579
- package/bin/naome.js +19 -5
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/dispatcher.rs +2 -1
- package/crates/naome-cli/src/main.rs +3 -0
- package/crates/naome-cli/src/quality_commands.rs +90 -2
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/decision/checks.rs +64 -0
- package/crates/naome-core/src/decision/idle.rs +67 -0
- package/crates/naome-core/src/decision/json.rs +36 -0
- package/crates/naome-core/src/decision/states.rs +165 -0
- package/crates/naome-core/src/decision.rs +131 -353
- package/crates/naome-core/src/install_plan.rs +2 -0
- package/crates/naome-core/src/lib.rs +5 -3
- package/crates/naome-core/src/paths.rs +3 -1
- package/crates/naome-core/src/quality/adapter_support.rs +89 -0
- package/crates/naome-core/src/quality/adapters.rs +20 -67
- package/crates/naome-core/src/quality/cleanup.rs +13 -1
- package/crates/naome-core/src/quality/config.rs +8 -15
- package/crates/naome-core/src/quality/config_support.rs +24 -0
- package/crates/naome-core/src/quality/mod.rs +18 -0
- package/crates/naome-core/src/quality/scanner.rs +20 -8
- package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
- package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
- package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
- package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
- package/crates/naome-core/src/quality/structure/checks.rs +124 -0
- package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
- package/crates/naome-core/src/quality/structure/classify.rs +94 -0
- package/crates/naome-core/src/quality/structure/config.rs +89 -0
- package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
- package/crates/naome-core/src/quality/structure/mod.rs +77 -0
- package/crates/naome-core/src/quality/structure/model.rs +124 -0
- package/crates/naome-core/src/quality/types.rs +3 -0
- package/crates/naome-core/src/route/builtin_checks.rs +155 -0
- package/crates/naome-core/src/route/builtin_context.rs +73 -0
- package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
- package/crates/naome-core/src/route/builtin_require.rs +40 -0
- package/crates/naome-core/src/route/context.rs +180 -0
- package/crates/naome-core/src/route/execution.rs +96 -0
- package/crates/naome-core/src/route/execution_baselines.rs +146 -0
- package/crates/naome-core/src/route/execution_support.rs +57 -0
- package/crates/naome-core/src/route/execution_tasks.rs +71 -0
- package/crates/naome-core/src/route/git_ops.rs +72 -0
- package/crates/naome-core/src/route/quality_gate.rs +73 -0
- package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
- package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
- package/crates/naome-core/src/route/worktree.rs +75 -0
- package/crates/naome-core/src/route/worktree_files.rs +32 -0
- package/crates/naome-core/src/route/worktree_plan.rs +131 -0
- package/crates/naome-core/src/route.rs +44 -1217
- package/crates/naome-core/src/verification.rs +1 -0
- package/crates/naome-core/tests/decision.rs +24 -118
- package/crates/naome-core/tests/harness_health.rs +2 -0
- package/crates/naome-core/tests/quality.rs +12 -118
- package/crates/naome-core/tests/quality_structure.rs +116 -0
- package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
- package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
- package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
- package/crates/naome-core/tests/repo_support/mod.rs +16 -0
- package/crates/naome-core/tests/repo_support/repo.rs +113 -0
- package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
- package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
- package/crates/naome-core/tests/repo_support/routes.rs +81 -0
- package/crates/naome-core/tests/repo_support/verification.rs +168 -0
- package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
- package/crates/naome-core/tests/route.rs +1 -1376
- package/crates/naome-core/tests/route_baseline.rs +86 -0
- package/crates/naome-core/tests/route_completion.rs +141 -0
- package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
- package/crates/naome-core/tests/route_user_diff.rs +198 -0
- package/crates/naome-core/tests/route_worktree.rs +54 -0
- package/crates/naome-core/tests/task_state.rs +60 -432
- package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
- package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
- package/crates/naome-core/tests/task_state_support/states.rs +84 -0
- package/crates/naome-core/tests/verification.rs +4 -45
- package/crates/naome-core/tests/verification_contract.rs +22 -78
- package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
- package/installer/agents.js +90 -0
- package/installer/context.js +67 -0
- package/installer/filesystem.js +166 -0
- package/installer/flows.js +84 -0
- package/installer/git-boundary.js +170 -0
- package/installer/git-hook-content.js +36 -0
- package/installer/git-hooks.js +134 -0
- package/installer/git-local.js +2 -0
- package/installer/git-shared.js +35 -0
- package/installer/harness-file-ops.js +140 -0
- package/installer/harness-files.js +56 -0
- package/installer/harness-verification.js +123 -0
- package/installer/install-plan.js +66 -0
- package/installer/main.js +25 -0
- package/installer/manifest-state.js +167 -0
- package/installer/native-build.js +24 -0
- package/installer/native-format.js +6 -0
- package/installer/native.js +162 -0
- package/installer/output.js +131 -0
- package/installer/version.js +32 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +2 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
- package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
- package/templates/naome-root/.naome/bin/naome.js +25 -21
- package/templates/naome-root/.naome/manifest.json +4 -2
- package/templates/naome-root/.naome/repository-structure.json +90 -0
- package/templates/naome-root/.naome/verification.json +1 -0
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/repository-quality.md +3 -0
- package/templates/naome-root/docs/naome/repository-structure.md +51 -0
- package/templates/naome-root/docs/naome/testing.md +2 -1
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
use std::
|
|
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::
|
|
5
|
+
use crate::journal::TaskJournalEntry;
|
|
15
6
|
use crate::models::{Decision, NaomeError};
|
|
16
|
-
use
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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(¤t_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(¤t_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
|
-
}
|