@lamentis/naome 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2 -2
- package/README.md +8 -1
- package/bin/naome-node.js +121 -4
- package/bin/naome.js +198 -3
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/main.rs +110 -13
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/decision.rs +82 -11
- package/crates/naome-core/src/git.rs +12 -1
- package/crates/naome-core/src/harness_health.rs +3 -1
- package/crates/naome-core/src/install_plan.rs +8 -3
- package/crates/naome-core/src/intent.rs +914 -0
- package/crates/naome-core/src/journal.rs +169 -0
- package/crates/naome-core/src/lib.rs +10 -1
- package/crates/naome-core/src/models.rs +63 -4
- package/crates/naome-core/src/route.rs +1000 -0
- package/crates/naome-core/src/task_state.rs +326 -21
- package/crates/naome-core/tests/decision.rs +8 -6
- package/crates/naome-core/tests/install_plan.rs +12 -3
- package/crates/naome-core/tests/intent.rs +826 -0
- package/crates/naome-core/tests/route.rs +1108 -0
- package/crates/naome-core/tests/task_state.rs +63 -4
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +7 -6
- package/templates/naome-root/.naome/bin/check-task-state.js +7 -6
- package/templates/naome-root/.naome/bin/naome.js +143 -13
- package/templates/naome-root/.naome/manifest.json +8 -7
- package/templates/naome-root/.naome/upgrade-state.json +1 -1
- package/templates/naome-root/AGENTS.md +30 -5
- package/templates/naome-root/docs/naome/agent-workflow.md +45 -24
- package/templates/naome-root/docs/naome/execution.md +55 -51
- package/templates/naome-root/docs/naome/index.md +10 -3
|
@@ -6,6 +6,7 @@ use std::process::Command;
|
|
|
6
6
|
use serde_json::Value;
|
|
7
7
|
|
|
8
8
|
use crate::harness_health::{validate_harness_health, HarnessHealthOptions};
|
|
9
|
+
use crate::install_plan::MACHINE_OWNED_PATHS;
|
|
9
10
|
use crate::models::NaomeError;
|
|
10
11
|
|
|
11
12
|
const CONTROL_STATE_PATH: &str = ".naome/task-state.json";
|
|
@@ -65,6 +66,30 @@ struct ChangedEntry {
|
|
|
65
66
|
status: String,
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
70
|
+
pub struct CompletedTaskHarnessRefreshDiff {
|
|
71
|
+
pub harness_paths: Vec<String>,
|
|
72
|
+
pub task_paths: Vec<String>,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
76
|
+
pub struct HarnessRefreshWithUnrelatedDiff {
|
|
77
|
+
pub harness_paths: Vec<String>,
|
|
78
|
+
pub unrelated_paths: Vec<String>,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
82
|
+
pub struct HarnessRefreshDiff {
|
|
83
|
+
pub harness_paths: Vec<String>,
|
|
84
|
+
pub unrelated_paths: Vec<String>,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
88
|
+
pub struct CompletedTaskCommitDiff {
|
|
89
|
+
pub task_paths: Vec<String>,
|
|
90
|
+
pub unrelated_paths: Vec<String>,
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
pub fn validate_task_state(
|
|
69
94
|
root: &Path,
|
|
70
95
|
options: TaskStateOptions,
|
|
@@ -175,6 +200,128 @@ pub fn validate_task_state(
|
|
|
175
200
|
Ok(report)
|
|
176
201
|
}
|
|
177
202
|
|
|
203
|
+
pub fn completed_task_commit_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
204
|
+
Ok(completed_task_commit_diff(root)?
|
|
205
|
+
.map(|diff| diff.task_paths)
|
|
206
|
+
.unwrap_or_default())
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
pub fn completed_task_commit_diff(
|
|
210
|
+
root: &Path,
|
|
211
|
+
) -> Result<Option<CompletedTaskCommitDiff>, NaomeError> {
|
|
212
|
+
let mut read_errors = Vec::new();
|
|
213
|
+
let Some(task_state) = read_json(root, ".naome/task-state.json", &mut read_errors)? else {
|
|
214
|
+
return Ok(None);
|
|
215
|
+
};
|
|
216
|
+
if !read_errors.is_empty()
|
|
217
|
+
|| task_state.get("status").and_then(Value::as_str) != Some("complete")
|
|
218
|
+
{
|
|
219
|
+
return Ok(None);
|
|
220
|
+
}
|
|
221
|
+
let Some(active_task) = task_state.get("activeTask") else {
|
|
222
|
+
return Ok(None);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
|
|
226
|
+
let mut task_entries = Vec::new();
|
|
227
|
+
let mut unrelated_paths = Vec::new();
|
|
228
|
+
for entry in read_git_changed_entries(root)? {
|
|
229
|
+
if entry.path == CONTROL_STATE_PATH || matches_any_pattern(&entry.path, &allowed_paths) {
|
|
230
|
+
task_entries.push(entry);
|
|
231
|
+
} else {
|
|
232
|
+
unrelated_paths.push(entry.path);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if task_entries.is_empty() {
|
|
237
|
+
return Ok(None);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let mut errors = Vec::new();
|
|
241
|
+
validate_task_state_shape(&task_state, &mut errors);
|
|
242
|
+
validate_active_task(Some(active_task), &mut errors);
|
|
243
|
+
validate_pending_upgrade(&task_state, root, &mut errors)?;
|
|
244
|
+
validate_active_task_references(Some(active_task), root, &mut errors, Some("complete"))?;
|
|
245
|
+
if !task_state.get("blocker").is_some_and(Value::is_null) {
|
|
246
|
+
errors.push("complete task state must have blocker set to null.".to_string());
|
|
247
|
+
}
|
|
248
|
+
let check_ids = read_verification_check_ids(root, &mut errors)?;
|
|
249
|
+
validate_required_check_ids(active_task, &check_ids, &mut errors);
|
|
250
|
+
validate_complete_task_against_entries(
|
|
251
|
+
active_task,
|
|
252
|
+
root,
|
|
253
|
+
&check_ids,
|
|
254
|
+
&task_entries,
|
|
255
|
+
&mut errors,
|
|
256
|
+
)?;
|
|
257
|
+
if !errors.is_empty() {
|
|
258
|
+
return Ok(None);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let mut task_paths: Vec<String> = task_entries
|
|
262
|
+
.into_iter()
|
|
263
|
+
.map(|entry| entry.path)
|
|
264
|
+
.collect::<HashSet<_>>()
|
|
265
|
+
.into_iter()
|
|
266
|
+
.collect();
|
|
267
|
+
task_paths.sort();
|
|
268
|
+
unrelated_paths.sort();
|
|
269
|
+
|
|
270
|
+
Ok(Some(CompletedTaskCommitDiff {
|
|
271
|
+
task_paths,
|
|
272
|
+
unrelated_paths,
|
|
273
|
+
}))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
pub fn harness_refresh_diff(root: &Path) -> Result<Option<HarnessRefreshDiff>, NaomeError> {
|
|
277
|
+
let changed_paths = read_git_changed_paths(root)?;
|
|
278
|
+
let has_repair_signal = changed_paths
|
|
279
|
+
.iter()
|
|
280
|
+
.any(|path| is_packaged_machine_owned_path(path) || is_repair_archive_path(path));
|
|
281
|
+
if !has_repair_signal {
|
|
282
|
+
return Ok(None);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let mut harness_paths = Vec::new();
|
|
286
|
+
let mut unrelated_paths = Vec::new();
|
|
287
|
+
|
|
288
|
+
for path in changed_paths {
|
|
289
|
+
if is_safe_harness_refresh_path(&path) {
|
|
290
|
+
harness_paths.push(path);
|
|
291
|
+
} else {
|
|
292
|
+
unrelated_paths.push(path);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if harness_paths.is_empty() {
|
|
297
|
+
return Ok(None);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
harness_paths.sort();
|
|
301
|
+
unrelated_paths.sort();
|
|
302
|
+
|
|
303
|
+
Ok(Some(HarnessRefreshDiff {
|
|
304
|
+
harness_paths,
|
|
305
|
+
unrelated_paths,
|
|
306
|
+
}))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
pub fn harness_refresh_with_unrelated_diff(
|
|
310
|
+
root: &Path,
|
|
311
|
+
) -> Result<Option<HarnessRefreshWithUnrelatedDiff>, NaomeError> {
|
|
312
|
+
let Some(diff) = harness_refresh_diff(root)? else {
|
|
313
|
+
return Ok(None);
|
|
314
|
+
};
|
|
315
|
+
if diff.unrelated_paths.is_empty() {
|
|
316
|
+
return Ok(None);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
Ok(Some(HarnessRefreshWithUnrelatedDiff {
|
|
320
|
+
harness_paths: diff.harness_paths,
|
|
321
|
+
unrelated_paths: diff.unrelated_paths,
|
|
322
|
+
}))
|
|
323
|
+
}
|
|
324
|
+
|
|
178
325
|
fn validate_harness_health_gate(
|
|
179
326
|
root: &Path,
|
|
180
327
|
options: &TaskStateOptions,
|
|
@@ -677,7 +824,23 @@ fn validate_proof_evidence_covers_changed_paths(
|
|
|
677
824
|
return Ok(());
|
|
678
825
|
}
|
|
679
826
|
|
|
680
|
-
let
|
|
827
|
+
let entries = read_git_changed_entries(root)?;
|
|
828
|
+
validate_proof_evidence_covers_changed_entries(active_task, &entries, errors)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
fn validate_proof_evidence_covers_changed_entries(
|
|
832
|
+
active_task: &Value,
|
|
833
|
+
entries: &[ChangedEntry],
|
|
834
|
+
errors: &mut Vec<String>,
|
|
835
|
+
) -> Result<(), NaomeError> {
|
|
836
|
+
let Some(proofs) = active_task.get("proofResults").and_then(Value::as_array) else {
|
|
837
|
+
return Ok(());
|
|
838
|
+
};
|
|
839
|
+
if proofs.is_empty() {
|
|
840
|
+
return Ok(());
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
let changed_paths = task_diff_from_entries(active_task, entries);
|
|
681
844
|
let evidence_paths: HashSet<String> = proofs
|
|
682
845
|
.iter()
|
|
683
846
|
.flat_map(|proof| {
|
|
@@ -945,7 +1108,16 @@ fn validate_changed_paths(
|
|
|
945
1108
|
root: &Path,
|
|
946
1109
|
errors: &mut Vec<String>,
|
|
947
1110
|
) -> Result<(), NaomeError> {
|
|
948
|
-
let
|
|
1111
|
+
let entries = read_git_changed_entries(root)?;
|
|
1112
|
+
validate_changed_entries(active_task, &entries, errors)
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
fn validate_changed_entries(
|
|
1116
|
+
active_task: &Value,
|
|
1117
|
+
entries: &[ChangedEntry],
|
|
1118
|
+
errors: &mut Vec<String>,
|
|
1119
|
+
) -> Result<(), NaomeError> {
|
|
1120
|
+
let diff = task_diff_from_entries(active_task, entries);
|
|
949
1121
|
if !diff.outside_paths.is_empty() {
|
|
950
1122
|
errors.push(format!(
|
|
951
1123
|
"Changed files outside allowedPaths: {}",
|
|
@@ -1000,10 +1172,14 @@ struct TaskDiff {
|
|
|
1000
1172
|
|
|
1001
1173
|
fn read_task_diff(active_task: &Value, root: &Path) -> Result<TaskDiff, NaomeError> {
|
|
1002
1174
|
let entries = read_git_changed_entries(root)?;
|
|
1175
|
+
Ok(task_diff_from_entries(active_task, &entries))
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
fn task_diff_from_entries(active_task: &Value, entries: &[ChangedEntry]) -> TaskDiff {
|
|
1003
1179
|
let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
|
|
1004
1180
|
let diff_paths: Vec<String> = entries
|
|
1005
|
-
.
|
|
1006
|
-
.map(|entry| entry.path)
|
|
1181
|
+
.iter()
|
|
1182
|
+
.map(|entry| entry.path.clone())
|
|
1007
1183
|
.filter(|path| path != CONTROL_STATE_PATH)
|
|
1008
1184
|
.collect();
|
|
1009
1185
|
let outside_paths = diff_paths
|
|
@@ -1012,10 +1188,10 @@ fn read_task_diff(active_task: &Value, root: &Path) -> Result<TaskDiff, NaomeErr
|
|
|
1012
1188
|
.cloned()
|
|
1013
1189
|
.collect();
|
|
1014
1190
|
|
|
1015
|
-
|
|
1191
|
+
TaskDiff {
|
|
1016
1192
|
diff_paths,
|
|
1017
1193
|
outside_paths,
|
|
1018
|
-
}
|
|
1194
|
+
}
|
|
1019
1195
|
}
|
|
1020
1196
|
|
|
1021
1197
|
fn validate_complete_task(
|
|
@@ -1051,9 +1227,8 @@ fn validate_complete_task(
|
|
|
1051
1227
|
|
|
1052
1228
|
let check_ids = read_verification_check_ids(root, errors)?;
|
|
1053
1229
|
validate_required_check_ids(active_task, &check_ids, errors);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
validate_proof_evidence_covers_changed_paths(Some(active_task), root, errors)?;
|
|
1230
|
+
let entries = read_git_changed_entries(root)?;
|
|
1231
|
+
validate_complete_task_against_entries(active_task, root, &check_ids, &entries, errors)?;
|
|
1057
1232
|
|
|
1058
1233
|
if errors.len() == error_start {
|
|
1059
1234
|
add_completed_task_diff_notice(root, notices)?;
|
|
@@ -1062,6 +1237,19 @@ fn validate_complete_task(
|
|
|
1062
1237
|
Ok(())
|
|
1063
1238
|
}
|
|
1064
1239
|
|
|
1240
|
+
fn validate_complete_task_against_entries(
|
|
1241
|
+
active_task: &Value,
|
|
1242
|
+
root: &Path,
|
|
1243
|
+
check_ids: &HashSet<String>,
|
|
1244
|
+
entries: &[ChangedEntry],
|
|
1245
|
+
errors: &mut Vec<String>,
|
|
1246
|
+
) -> Result<(), NaomeError> {
|
|
1247
|
+
validate_proof_results(active_task, check_ids, root, errors)?;
|
|
1248
|
+
validate_changed_entries(active_task, entries, errors)?;
|
|
1249
|
+
validate_proof_evidence_covers_changed_entries(active_task, entries, errors)?;
|
|
1250
|
+
Ok(())
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1065
1253
|
fn validate_progress(
|
|
1066
1254
|
task_state: &Value,
|
|
1067
1255
|
root: &Path,
|
|
@@ -1268,7 +1456,11 @@ fn validate_commit_gate(
|
|
|
1268
1456
|
errors: &mut Vec<String>,
|
|
1269
1457
|
notices: &mut Vec<String>,
|
|
1270
1458
|
) -> Result<(), NaomeError> {
|
|
1271
|
-
let
|
|
1459
|
+
let staged_entries = read_git_staged_changed_entries(root)?;
|
|
1460
|
+
let changed_paths: Vec<String> = staged_entries
|
|
1461
|
+
.iter()
|
|
1462
|
+
.map(|entry| entry.path.clone())
|
|
1463
|
+
.collect();
|
|
1272
1464
|
if changed_paths.is_empty() {
|
|
1273
1465
|
return Ok(());
|
|
1274
1466
|
}
|
|
@@ -1277,16 +1469,35 @@ fn validate_commit_gate(
|
|
|
1277
1469
|
.get("status")
|
|
1278
1470
|
.and_then(Value::as_str)
|
|
1279
1471
|
.unwrap_or("invalid");
|
|
1472
|
+
if status == "complete" && completed_task_harness_refresh_diff(root)?.is_some() {
|
|
1473
|
+
if is_safe_harness_refresh_diff(&changed_paths) {
|
|
1474
|
+
return Ok(());
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1280
1478
|
if status == "complete" {
|
|
1281
1479
|
validate_active_task(task_state.get("activeTask"), errors);
|
|
1282
1480
|
validate_active_task_references(task_state.get("activeTask"), root, errors, Some(status))?;
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
errors
|
|
1288
|
-
|
|
1289
|
-
|
|
1481
|
+
if !task_state.get("blocker").is_some_and(Value::is_null) {
|
|
1482
|
+
errors.push("complete task state must have blocker set to null.".to_string());
|
|
1483
|
+
}
|
|
1484
|
+
if let Some(active_task) = task_state.get("activeTask") {
|
|
1485
|
+
let check_ids = read_verification_check_ids(root, errors)?;
|
|
1486
|
+
validate_required_check_ids(active_task, &check_ids, errors);
|
|
1487
|
+
validate_complete_task_against_entries(
|
|
1488
|
+
active_task,
|
|
1489
|
+
root,
|
|
1490
|
+
&check_ids,
|
|
1491
|
+
&staged_entries,
|
|
1492
|
+
errors,
|
|
1493
|
+
)?;
|
|
1494
|
+
if errors.is_empty() {
|
|
1495
|
+
notices.push(format!(
|
|
1496
|
+
"Commit gate accepted task-owned staged paths: {}.",
|
|
1497
|
+
changed_paths.join(", ")
|
|
1498
|
+
));
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1290
1501
|
return Ok(());
|
|
1291
1502
|
}
|
|
1292
1503
|
|
|
@@ -1341,15 +1552,15 @@ fn format_dirty_diff_admission_blocker(
|
|
|
1341
1552
|
);
|
|
1342
1553
|
|
|
1343
1554
|
if is_harness_repair_diff(root, changed_paths)? {
|
|
1344
|
-
return Ok(format!("{prefix} These look like completed Harness Repair changes.
|
|
1555
|
+
return Ok(format!("{prefix} These look like completed Harness Repair changes. Run NAOME intent for the next natural-language request before deciding whether to baseline, review, or cancel the repair diff."));
|
|
1345
1556
|
}
|
|
1346
1557
|
|
|
1347
1558
|
if is_completed_task_diff(task_state, changed_paths) {
|
|
1348
|
-
return Ok(format!("{prefix} These look like completed task changes.
|
|
1559
|
+
return Ok(format!("{prefix} These look like completed task changes. Run NAOME intent for the next natural-language request; deterministic policy can baseline a valid completed task before creating the next task."));
|
|
1349
1560
|
}
|
|
1350
1561
|
|
|
1351
1562
|
if is_naome_baseline_diff(changed_paths) {
|
|
1352
|
-
return Ok(format!("{prefix} These look like completed NAOME install or upgrade changes.
|
|
1563
|
+
return Ok(format!("{prefix} These look like completed NAOME install or upgrade changes. Run NAOME intent for the next natural-language request; deterministic policy can baseline setup before creating the next task."));
|
|
1353
1564
|
}
|
|
1354
1565
|
|
|
1355
1566
|
Ok(format!("{prefix} Ask the user to choose exactly one: review_task_diff, request_task_changes, cancel_task_changes. Do not start new feature work or commit without explicit user selection."))
|
|
@@ -1379,6 +1590,21 @@ fn is_repair_support_path(path: &str) -> bool {
|
|
|
1379
1590
|
|| is_repair_archive_path(path)
|
|
1380
1591
|
}
|
|
1381
1592
|
|
|
1593
|
+
fn is_packaged_machine_owned_path(path: &str) -> bool {
|
|
1594
|
+
MACHINE_OWNED_PATHS.iter().any(|owned| *owned == path)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
fn is_safe_harness_refresh_path(path: &str) -> bool {
|
|
1598
|
+
is_packaged_machine_owned_path(path) || is_repair_support_path(path)
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
fn is_safe_harness_refresh_diff(changed_paths: &[String]) -> bool {
|
|
1602
|
+
!changed_paths.is_empty()
|
|
1603
|
+
&& changed_paths
|
|
1604
|
+
.iter()
|
|
1605
|
+
.all(|path| is_safe_harness_refresh_path(path))
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1382
1608
|
fn is_repair_archive_path(path: &str) -> bool {
|
|
1383
1609
|
path.starts_with(".naome/archive/repair-")
|
|
1384
1610
|
}
|
|
@@ -1500,10 +1726,74 @@ fn add_completed_task_diff_notice(
|
|
|
1500
1726
|
return Ok(());
|
|
1501
1727
|
}
|
|
1502
1728
|
|
|
1503
|
-
notices.push(format!("
|
|
1729
|
+
notices.push(format!("Task is complete and verified. Changed paths: {}. NAOME intent can baseline it automatically before the next distinct task; only surface human choices when intent blocks or the user explicitly asks to review, revise, cancel, or commit.", changed_paths.join(", ")));
|
|
1504
1730
|
Ok(())
|
|
1505
1731
|
}
|
|
1506
1732
|
|
|
1733
|
+
pub fn completed_task_harness_refresh_diff(
|
|
1734
|
+
root: &Path,
|
|
1735
|
+
) -> Result<Option<CompletedTaskHarnessRefreshDiff>, NaomeError> {
|
|
1736
|
+
let mut read_errors = Vec::new();
|
|
1737
|
+
let Some(task_state) = read_json(root, ".naome/task-state.json", &mut read_errors)? else {
|
|
1738
|
+
return Ok(None);
|
|
1739
|
+
};
|
|
1740
|
+
if !read_errors.is_empty() {
|
|
1741
|
+
return Ok(None);
|
|
1742
|
+
}
|
|
1743
|
+
if task_state.get("status").and_then(Value::as_str) != Some("complete") {
|
|
1744
|
+
return Ok(None);
|
|
1745
|
+
}
|
|
1746
|
+
let Some(active_task) = task_state.get("activeTask") else {
|
|
1747
|
+
return Ok(None);
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
|
|
1751
|
+
let mut harness_paths = Vec::new();
|
|
1752
|
+
let mut task_paths = Vec::new();
|
|
1753
|
+
let mut other_paths = Vec::new();
|
|
1754
|
+
|
|
1755
|
+
for path in read_git_changed_paths(root)? {
|
|
1756
|
+
if path == CONTROL_STATE_PATH {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
if matches_any_pattern(&path, &allowed_paths) {
|
|
1760
|
+
task_paths.push(path);
|
|
1761
|
+
} else if is_safe_harness_refresh_path(&path) {
|
|
1762
|
+
harness_paths.push(path);
|
|
1763
|
+
} else {
|
|
1764
|
+
other_paths.push(path);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
if task_paths.is_empty() || harness_paths.is_empty() || !other_paths.is_empty() {
|
|
1769
|
+
return Ok(None);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
let report = validate_task_state(
|
|
1773
|
+
root,
|
|
1774
|
+
TaskStateOptions {
|
|
1775
|
+
mode: TaskStateMode::State,
|
|
1776
|
+
harness_health: None,
|
|
1777
|
+
},
|
|
1778
|
+
)?;
|
|
1779
|
+
let allowed_scope_error = format!(
|
|
1780
|
+
"Changed files outside allowedPaths: {}",
|
|
1781
|
+
harness_paths.join(", ")
|
|
1782
|
+
);
|
|
1783
|
+
if report
|
|
1784
|
+
.errors
|
|
1785
|
+
.iter()
|
|
1786
|
+
.all(|error| error == &allowed_scope_error)
|
|
1787
|
+
{
|
|
1788
|
+
Ok(Some(CompletedTaskHarnessRefreshDiff {
|
|
1789
|
+
harness_paths,
|
|
1790
|
+
task_paths,
|
|
1791
|
+
}))
|
|
1792
|
+
} else {
|
|
1793
|
+
Ok(None)
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1507
1797
|
fn read_git_changed_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
1508
1798
|
Ok(read_git_changed_entries(root)?
|
|
1509
1799
|
.into_iter()
|
|
@@ -1511,6 +1801,21 @@ fn read_git_changed_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
|
1511
1801
|
.collect())
|
|
1512
1802
|
}
|
|
1513
1803
|
|
|
1804
|
+
fn read_git_staged_changed_entries(root: &Path) -> Result<Vec<ChangedEntry>, NaomeError> {
|
|
1805
|
+
let output = Command::new("git")
|
|
1806
|
+
.args(["diff", "--name-status", "--cached", "-z"])
|
|
1807
|
+
.current_dir(root)
|
|
1808
|
+
.output()?;
|
|
1809
|
+
if !output.status.success() {
|
|
1810
|
+
return Err(NaomeError::new(format!(
|
|
1811
|
+
"git diff --name-status --cached -z failed: {}",
|
|
1812
|
+
command_output(&output)
|
|
1813
|
+
)));
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
Ok(parse_name_status_output(&output.stdout))
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1514
1819
|
fn read_git_changed_entries(root: &Path) -> Result<Vec<ChangedEntry>, NaomeError> {
|
|
1515
1820
|
let git_check = run_git(root, ["rev-parse", "--is-inside-work-tree"])?;
|
|
1516
1821
|
if !git_check.status.success() {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
use std::fs;
|
|
2
|
-
use std::sync::Mutex;
|
|
3
2
|
use std::path::{Path, PathBuf};
|
|
4
3
|
use std::process::Command;
|
|
4
|
+
use std::sync::Mutex;
|
|
5
5
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
6
6
|
|
|
7
|
-
use naome_core::{evaluate_decision, EvaluationOptions};
|
|
7
|
+
use naome_core::{evaluate_decision, format_decision, EvaluationOptions};
|
|
8
8
|
use serde_json::json;
|
|
9
9
|
|
|
10
10
|
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
|
@@ -62,6 +62,11 @@ fn completed_task_diff_returns_baseline_choices() {
|
|
|
62
62
|
.allowed_actions
|
|
63
63
|
.contains(&"request_task_changes".to_string()));
|
|
64
64
|
assert_eq!(decision.changed_paths, vec!["README.md"]);
|
|
65
|
+
|
|
66
|
+
let human_output = format_decision(&decision, "next");
|
|
67
|
+
assert!(human_output.contains("Intent routing"));
|
|
68
|
+
assert!(!human_output.contains("Allowed actions:"));
|
|
69
|
+
assert!(!human_output.contains("commit_task_baseline"));
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
#[test]
|
|
@@ -82,10 +87,7 @@ fn idle_unowned_diff_blocks_new_feature_work() {
|
|
|
82
87
|
let decision = evaluate_decision(repo.path(), EvaluationOptions::offline()).unwrap();
|
|
83
88
|
|
|
84
89
|
assert_eq!(decision.state, "dirty_unowned_diff");
|
|
85
|
-
assert_eq!(
|
|
86
|
-
decision.allowed_actions,
|
|
87
|
-
vec!["review_unowned_diff", "clear_or_commit_unowned_diff"]
|
|
88
|
-
);
|
|
90
|
+
assert_eq!(decision.allowed_actions, vec!["review_unowned_diff"]);
|
|
89
91
|
assert_eq!(decision.changed_paths, vec!["README.md"]);
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -21,15 +21,24 @@ fn install_plan_marks_machine_docs_and_bins_local_only() {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
#[test]
|
|
24
|
-
fn
|
|
24
|
+
fn install_plan_includes_git_exclude_and_untrack_policy() {
|
|
25
25
|
let plan = install_plan("1.0.0");
|
|
26
26
|
|
|
27
|
-
assert!(plan.gitignore_entries.contains(&".naome/archive/"));
|
|
28
|
-
assert!(plan.gitignore_entries.contains(&".naome/bin/naome-rust*"));
|
|
29
27
|
assert!(plan
|
|
30
28
|
.gitignore_entries
|
|
29
|
+
.contains(&".naome/task-journal.jsonl"));
|
|
30
|
+
assert!(plan.git_exclude_entries.contains(&".naome/archive/"));
|
|
31
|
+
assert!(plan.git_exclude_entries.contains(&".naome/bin/naome-rust*"));
|
|
32
|
+
assert!(plan
|
|
33
|
+
.git_exclude_entries
|
|
34
|
+
.contains(&".naome/task-journal.jsonl"));
|
|
35
|
+
assert!(plan
|
|
36
|
+
.git_exclude_entries
|
|
31
37
|
.contains(&".naome/bin/check-harness-health.js"));
|
|
32
38
|
assert!(plan.git_untrack_paths.contains(&".naome/archive"));
|
|
33
39
|
assert!(plan.git_untrack_paths.contains(&".naome/bin/naome-rust"));
|
|
40
|
+
assert!(plan
|
|
41
|
+
.git_untrack_paths
|
|
42
|
+
.contains(&".naome/task-journal.jsonl"));
|
|
34
43
|
assert!(plan.git_untrack_paths.contains(&"docs/naome/first-run.md"));
|
|
35
44
|
}
|