@lamentis/naome 1.1.0 → 1.1.2
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 +45 -4
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/route.rs +297 -21
- package/crates/naome-core/src/task_state.rs +51 -5
- package/crates/naome-core/tests/route.rs +372 -4
- package/crates/naome-core/tests/task_state.rs +140 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/manifest.json +1 -1
- package/templates/naome-root/.naome/upgrade-state.json +1 -1
package/Cargo.lock
CHANGED
|
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
76
76
|
|
|
77
77
|
[[package]]
|
|
78
78
|
name = "naome-cli"
|
|
79
|
-
version = "1.1.
|
|
79
|
+
version = "1.1.2"
|
|
80
80
|
dependencies = [
|
|
81
81
|
"naome-core",
|
|
82
82
|
"serde_json",
|
|
@@ -84,7 +84,7 @@ dependencies = [
|
|
|
84
84
|
|
|
85
85
|
[[package]]
|
|
86
86
|
name = "naome-core"
|
|
87
|
-
version = "1.1.
|
|
87
|
+
version = "1.1.2"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
package/bin/naome-node.js
CHANGED
|
@@ -18,6 +18,7 @@ const updated = [];
|
|
|
18
18
|
const skipped = [];
|
|
19
19
|
const unsafeSkipped = [];
|
|
20
20
|
const useColor = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
21
|
+
const verboseOutput = process.argv.includes("--verbose");
|
|
21
22
|
const healthCheckerRelativePath = ".naome/bin/check-harness-health.js";
|
|
22
23
|
const taskStateCheckerRelativePath = ".naome/bin/check-task-state.js";
|
|
23
24
|
const naomeCommandRelativePath = ".naome/bin/naome.js";
|
|
@@ -513,6 +514,7 @@ async function runFreshInstall() {
|
|
|
513
514
|
patchInstalledMachineOwnedIntegrity();
|
|
514
515
|
ensureBuiltInVerificationChecks();
|
|
515
516
|
patchManifestDate();
|
|
517
|
+
ensureCompleteUpgradeState(null);
|
|
516
518
|
ensureArchiveDirectory();
|
|
517
519
|
takeoverExistingAgents();
|
|
518
520
|
ensureLocalOnlySourceControlBoundary();
|
|
@@ -1454,26 +1456,31 @@ function printSummary() {
|
|
|
1454
1456
|
console.log("");
|
|
1455
1457
|
console.log(`${color.green("+")} ${summaryTitle}`);
|
|
1456
1458
|
|
|
1457
|
-
|
|
1459
|
+
const detailedOutput = shouldPrintDetailedSummary();
|
|
1460
|
+
if (!detailedOutput) {
|
|
1461
|
+
printCompactSummary();
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (detailedOutput && installed.length > 0) {
|
|
1458
1465
|
const installedItems = uniqueStrings(installed);
|
|
1459
1466
|
printSection(`Installed ${installedItems.length} ${installedItems.length === 1 ? "item" : "items"}`);
|
|
1460
1467
|
printList(installed, "+");
|
|
1461
1468
|
}
|
|
1462
1469
|
|
|
1463
|
-
if (updated.length > 0) {
|
|
1470
|
+
if (detailedOutput && updated.length > 0) {
|
|
1464
1471
|
const updatedItems = uniqueStrings(updated);
|
|
1465
1472
|
printSection(`Updated ${updatedItems.length} ${updatedItems.length === 1 ? "item" : "items"}`);
|
|
1466
1473
|
printList(updated, "~");
|
|
1467
1474
|
}
|
|
1468
1475
|
|
|
1469
|
-
if (archived.length > 0) {
|
|
1476
|
+
if (detailedOutput && archived.length > 0) {
|
|
1470
1477
|
printSection("Archived");
|
|
1471
1478
|
for (const entry of archived) {
|
|
1472
1479
|
console.log(`${color.dim(">")} ${entry.from} -> ${entry.to}`);
|
|
1473
1480
|
}
|
|
1474
1481
|
}
|
|
1475
1482
|
|
|
1476
|
-
if (skipped.length > 0) {
|
|
1483
|
+
if (detailedOutput && skipped.length > 0) {
|
|
1477
1484
|
const skippedItems = uniqueStrings(skipped);
|
|
1478
1485
|
printSection(`Skipped ${skippedItems.length} existing ${skippedItems.length === 1 ? "path" : "paths"}`);
|
|
1479
1486
|
printList(skipped, "-");
|
|
@@ -1484,6 +1491,11 @@ function printSummary() {
|
|
|
1484
1491
|
printList(unsafeSkipped, "!");
|
|
1485
1492
|
}
|
|
1486
1493
|
|
|
1494
|
+
if (isRepositoryInitialized()) {
|
|
1495
|
+
console.log("");
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1487
1499
|
printSection("Next step");
|
|
1488
1500
|
console.log("Copy this into your coding agent:");
|
|
1489
1501
|
console.log("");
|
|
@@ -1493,6 +1505,35 @@ function printSummary() {
|
|
|
1493
1505
|
console.log("");
|
|
1494
1506
|
}
|
|
1495
1507
|
|
|
1508
|
+
function shouldPrintDetailedSummary() {
|
|
1509
|
+
return verboseOutput || !existingInstall;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function printCompactSummary() {
|
|
1513
|
+
const installedCount = uniqueStrings(installed).length;
|
|
1514
|
+
const updatedCount = uniqueStrings(updated).length;
|
|
1515
|
+
const archivedCount = archived.length;
|
|
1516
|
+
|
|
1517
|
+
if (installedCount > 0) {
|
|
1518
|
+
console.log(`${color.dim("installed")} ${installedCount}`);
|
|
1519
|
+
}
|
|
1520
|
+
if (updatedCount > 0) {
|
|
1521
|
+
console.log(`${color.dim("updated")} ${updatedCount}`);
|
|
1522
|
+
}
|
|
1523
|
+
if (archivedCount > 0) {
|
|
1524
|
+
console.log(`${color.dim("archived")} ${archivedCount}`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function isRepositoryInitialized() {
|
|
1529
|
+
try {
|
|
1530
|
+
const initState = JSON.parse(readFileSync(join(targetRoot, ".naome", "init-state.json"), "utf8"));
|
|
1531
|
+
return initState.initialized === true;
|
|
1532
|
+
} catch {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1496
1537
|
assertTemplateRoot();
|
|
1497
1538
|
loadInstallPlan();
|
|
1498
1539
|
|
|
@@ -8,6 +8,7 @@ use serde_json::Value;
|
|
|
8
8
|
|
|
9
9
|
use crate::decision::{evaluate_decision, EvaluationOptions};
|
|
10
10
|
use crate::git;
|
|
11
|
+
use crate::harness_health::{validate_harness_health, HarnessHealthOptions};
|
|
11
12
|
use crate::install_plan::{LOCAL_NATIVE_BINARY_PATHS, LOCAL_ONLY_MACHINE_OWNED_PATHS};
|
|
12
13
|
use crate::intent::{evaluate_intent, IntentDecision};
|
|
13
14
|
use crate::journal::{append_task_journal, TaskJournalEntry};
|
|
@@ -15,8 +16,9 @@ use crate::models::{Decision, NaomeError};
|
|
|
15
16
|
use crate::paths;
|
|
16
17
|
use crate::task_state::{
|
|
17
18
|
completed_task_commit_paths, completed_task_harness_refresh_diff, harness_refresh_diff,
|
|
18
|
-
harness_refresh_with_unrelated_diff,
|
|
19
|
+
harness_refresh_with_unrelated_diff, validate_task_state, TaskStateMode, TaskStateOptions,
|
|
19
20
|
};
|
|
21
|
+
use crate::verification_contract::validate_verification_contract;
|
|
20
22
|
|
|
21
23
|
const MAX_NAOME_TASK_WORKTREES: usize = 25;
|
|
22
24
|
|
|
@@ -109,13 +111,19 @@ pub fn evaluate_route(
|
|
|
109
111
|
.to_string();
|
|
110
112
|
}
|
|
111
113
|
"auto_commit_completed_task_then_create_isolated_task_worktree" => {
|
|
112
|
-
let
|
|
114
|
+
let worktree_name_head = task_worktree_name_head(root)?;
|
|
115
|
+
preflight_isolated_task_worktree(root, prompt, &worktree_name_head)?;
|
|
116
|
+
let before = Some(worktree_name_head.clone());
|
|
113
117
|
git_add_completed_task_paths(root)?;
|
|
114
118
|
git_commit(root, "chore(naome): baseline completed task")?;
|
|
115
119
|
let after = git_head(root)?;
|
|
116
120
|
journal_entry =
|
|
117
121
|
append_task_journal(root, "route_auto_baseline", before, after.clone())?;
|
|
118
|
-
let created =
|
|
122
|
+
let created = create_isolated_task_worktree_with_name_head(
|
|
123
|
+
root,
|
|
124
|
+
prompt,
|
|
125
|
+
&worktree_name_head,
|
|
126
|
+
)?;
|
|
119
127
|
task_root = PathBuf::from(&created.path);
|
|
120
128
|
mutation_performed = true;
|
|
121
129
|
executed_actions.push("commit_task_baseline".to_string());
|
|
@@ -147,6 +155,8 @@ pub fn evaluate_route(
|
|
|
147
155
|
user_message = "NAOME baselined the harness refresh and completed task, then admitted the next task.".to_string();
|
|
148
156
|
}
|
|
149
157
|
"auto_commit_harness_refresh_then_create_isolated_task_worktree" => {
|
|
158
|
+
let worktree_name_head = task_worktree_name_head(root)?;
|
|
159
|
+
preflight_isolated_task_worktree(root, prompt, &worktree_name_head)?;
|
|
150
160
|
let Some(split) = harness_refresh_with_unrelated_diff(root)? else {
|
|
151
161
|
return Err(NaomeError::new(
|
|
152
162
|
"Unable to split harness refresh paths from unrelated dirty paths.",
|
|
@@ -154,7 +164,11 @@ pub fn evaluate_route(
|
|
|
154
164
|
};
|
|
155
165
|
git_stage_only_paths(root, &split.harness_paths)?;
|
|
156
166
|
git_commit(root, "chore(naome): baseline harness refresh")?;
|
|
157
|
-
let created =
|
|
167
|
+
let created = create_isolated_task_worktree_with_name_head(
|
|
168
|
+
root,
|
|
169
|
+
prompt,
|
|
170
|
+
&worktree_name_head,
|
|
171
|
+
)?;
|
|
158
172
|
task_root = PathBuf::from(&created.path);
|
|
159
173
|
mutation_performed = true;
|
|
160
174
|
executed_actions.push("commit_harness_refresh_baseline".to_string());
|
|
@@ -591,7 +605,7 @@ fn run_user_diff_quality_gate(
|
|
|
591
605
|
"Quality check {check_id} is referenced but not defined."
|
|
592
606
|
)));
|
|
593
607
|
};
|
|
594
|
-
run_quality_check(root, check)?;
|
|
608
|
+
run_quality_check(root, check_id, check)?;
|
|
595
609
|
}
|
|
596
610
|
|
|
597
611
|
let current_paths = git::changed_paths(root)?;
|
|
@@ -606,7 +620,7 @@ fn run_user_diff_quality_gate(
|
|
|
606
620
|
|
|
607
621
|
validate_changed_text_whitespace(root, changed_paths)?;
|
|
608
622
|
if let Some(check) = checks.get("diff-check") {
|
|
609
|
-
run_quality_check(root, check)?;
|
|
623
|
+
run_quality_check(root, "diff-check", check)?;
|
|
610
624
|
}
|
|
611
625
|
let current_paths = git::changed_paths(root)?;
|
|
612
626
|
let current_set = sorted_path_set(¤t_paths);
|
|
@@ -814,27 +828,238 @@ fn push_unique_string(values: &mut Vec<String>, value: &str) {
|
|
|
814
828
|
}
|
|
815
829
|
}
|
|
816
830
|
|
|
817
|
-
fn run_quality_check(root: &Path, check: &QualityCheck) -> Result<(), NaomeError> {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
831
|
+
fn run_quality_check(root: &Path, check_id: &str, check: &QualityCheck) -> Result<(), NaomeError> {
|
|
832
|
+
match check_id {
|
|
833
|
+
"installer-tests" => require_builtin_quality_check(
|
|
834
|
+
check_id,
|
|
835
|
+
check,
|
|
836
|
+
"npm run test:naome-installer",
|
|
837
|
+
),
|
|
838
|
+
"rust-build" => require_builtin_quality_check(check_id, check, "npm run build:rust"),
|
|
839
|
+
"decision-engine-tests" => {
|
|
840
|
+
require_builtin_quality_check(check_id, check, "npm run test:decision-engine")
|
|
841
|
+
}
|
|
842
|
+
"package-dry-run" => require_builtin_quality_check(check_id, check, "npm run pack:dry-run"),
|
|
843
|
+
"diff-check" => {
|
|
844
|
+
require_builtin_quality_check(check_id, check, "git diff --check")?;
|
|
845
|
+
let output = Command::new("git")
|
|
846
|
+
.args(["diff", "--check"])
|
|
847
|
+
.current_dir(root)
|
|
848
|
+
.output()?;
|
|
849
|
+
|
|
850
|
+
if output.status.success() {
|
|
851
|
+
Ok(())
|
|
852
|
+
} else {
|
|
853
|
+
Err(NaomeError::new(command_output(&output)))
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
"naome-harness-health" => {
|
|
857
|
+
require_builtin_quality_check(
|
|
858
|
+
check_id,
|
|
859
|
+
check,
|
|
860
|
+
"node .naome/bin/check-harness-health.js",
|
|
861
|
+
)?;
|
|
862
|
+
run_harness_health_check(root)
|
|
863
|
+
}
|
|
864
|
+
"dogfood-health" => {
|
|
865
|
+
require_builtin_quality_check(check_id, check, "npm run dogfood:health")?;
|
|
866
|
+
run_harness_health_check(root)
|
|
867
|
+
}
|
|
868
|
+
"task-state-check" => {
|
|
869
|
+
require_builtin_quality_check(check_id, check, "npm run check:task-state")?;
|
|
870
|
+
run_template_task_state_check(root)
|
|
871
|
+
}
|
|
872
|
+
"verification-contract-check" => {
|
|
873
|
+
require_builtin_quality_check(
|
|
874
|
+
check_id,
|
|
875
|
+
check,
|
|
876
|
+
"npm run check:verification-contract",
|
|
877
|
+
)?;
|
|
878
|
+
run_template_verification_contract_check(root)
|
|
879
|
+
}
|
|
880
|
+
"context-budget-check" => {
|
|
881
|
+
require_builtin_quality_check(check_id, check, "npm run check:context-budget")?;
|
|
882
|
+
run_context_budget_check(root)
|
|
883
|
+
}
|
|
884
|
+
_ => Err(NaomeError::new(format!(
|
|
885
|
+
"Quality check {check_id} is not a built-in safe check; NAOME will not execute repository-controlled verification commands."
|
|
886
|
+
))),
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
fn require_builtin_quality_check(
|
|
891
|
+
check_id: &str,
|
|
892
|
+
check: &QualityCheck,
|
|
893
|
+
expected_command: &str,
|
|
894
|
+
) -> Result<(), NaomeError> {
|
|
895
|
+
if check.cwd == "." && check.command == expected_command {
|
|
896
|
+
return Ok(());
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
Err(NaomeError::new(format!(
|
|
900
|
+
"Quality check {check_id} has an unsafe command or cwd; expected command `{expected_command}` with cwd `.`."
|
|
901
|
+
)))
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
fn run_harness_health_check(root: &Path) -> Result<(), NaomeError> {
|
|
905
|
+
let errors = validate_harness_health(
|
|
906
|
+
root,
|
|
907
|
+
HarnessHealthOptions {
|
|
908
|
+
expected_integrity: packaged_harness_integrity()?,
|
|
909
|
+
..HarnessHealthOptions::default()
|
|
910
|
+
},
|
|
911
|
+
)?;
|
|
912
|
+
if errors.is_empty() {
|
|
913
|
+
Ok(())
|
|
824
914
|
} else {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
.output()?
|
|
829
|
-
};
|
|
915
|
+
Err(NaomeError::new(errors.join("\n")))
|
|
916
|
+
}
|
|
917
|
+
}
|
|
830
918
|
|
|
831
|
-
|
|
919
|
+
fn packaged_harness_integrity() -> Result<std::collections::HashMap<String, String>, NaomeError> {
|
|
920
|
+
const CHECKER: &str =
|
|
921
|
+
include_str!("../../../templates/naome-root/.naome/bin/check-harness-health.js");
|
|
922
|
+
let start_marker = "const expectedMachineOwnedIntegrity = Object.freeze({";
|
|
923
|
+
let start = CHECKER
|
|
924
|
+
.find(start_marker)
|
|
925
|
+
.ok_or_else(|| NaomeError::new("Packaged harness integrity block is missing."))?;
|
|
926
|
+
let body_start = start + start_marker.len();
|
|
927
|
+
let end = CHECKER[body_start..]
|
|
928
|
+
.find("\n});")
|
|
929
|
+
.map(|offset| body_start + offset)
|
|
930
|
+
.ok_or_else(|| NaomeError::new("Packaged harness integrity block is incomplete."))?;
|
|
931
|
+
|
|
932
|
+
let mut integrity = std::collections::HashMap::new();
|
|
933
|
+
for line in CHECKER[body_start..end].lines() {
|
|
934
|
+
let line = line.trim().trim_end_matches(',').trim();
|
|
935
|
+
if line.is_empty() {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
let Some((path, hash)) = line.split_once(':') else {
|
|
939
|
+
return Err(NaomeError::new(format!(
|
|
940
|
+
"Packaged harness integrity entry is invalid: {line}"
|
|
941
|
+
)));
|
|
942
|
+
};
|
|
943
|
+
let path: String = serde_json::from_str(path.trim()).map_err(|error| {
|
|
944
|
+
NaomeError::new(format!(
|
|
945
|
+
"Packaged harness integrity path is invalid: {error}"
|
|
946
|
+
))
|
|
947
|
+
})?;
|
|
948
|
+
let hash: String = serde_json::from_str(hash.trim()).map_err(|error| {
|
|
949
|
+
NaomeError::new(format!(
|
|
950
|
+
"Packaged harness integrity hash is invalid: {error}"
|
|
951
|
+
))
|
|
952
|
+
})?;
|
|
953
|
+
integrity.insert(path, hash);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if integrity.is_empty() {
|
|
957
|
+
return Err(NaomeError::new(
|
|
958
|
+
"Packaged harness integrity block is empty.",
|
|
959
|
+
));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
Ok(integrity)
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
fn run_template_task_state_check(root: &Path) -> Result<(), NaomeError> {
|
|
966
|
+
let template_root = template_root(root);
|
|
967
|
+
let report = validate_task_state(
|
|
968
|
+
&template_root,
|
|
969
|
+
TaskStateOptions {
|
|
970
|
+
mode: TaskStateMode::State,
|
|
971
|
+
harness_health: Some(HarnessHealthOptions {
|
|
972
|
+
expected_integrity: packaged_harness_integrity()?,
|
|
973
|
+
allow_missing_archive: true,
|
|
974
|
+
..HarnessHealthOptions::default()
|
|
975
|
+
}),
|
|
976
|
+
},
|
|
977
|
+
)?;
|
|
978
|
+
if report.errors.is_empty() {
|
|
832
979
|
Ok(())
|
|
833
980
|
} else {
|
|
834
|
-
Err(NaomeError::new(
|
|
981
|
+
Err(NaomeError::new(report.errors.join("\n")))
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
fn run_template_verification_contract_check(root: &Path) -> Result<(), NaomeError> {
|
|
986
|
+
let errors = validate_verification_contract(&template_root(root))?;
|
|
987
|
+
if errors.is_empty() {
|
|
988
|
+
Ok(())
|
|
989
|
+
} else {
|
|
990
|
+
Err(NaomeError::new(errors.join("\n")))
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
fn run_context_budget_check(root: &Path) -> Result<(), NaomeError> {
|
|
995
|
+
let template_root = template_root(root);
|
|
996
|
+
let mut context_files = vec![
|
|
997
|
+
template_root.join("AGENTS.md"),
|
|
998
|
+
template_root.join(".naomeignore"),
|
|
999
|
+
];
|
|
1000
|
+
context_files.extend(markdown_files(&template_root.join("docs").join("naome"))?);
|
|
1001
|
+
context_files.sort();
|
|
1002
|
+
|
|
1003
|
+
let mut errors = Vec::new();
|
|
1004
|
+
for path in context_files {
|
|
1005
|
+
let content = fs::read_to_string(&path)?;
|
|
1006
|
+
let line_count = count_lines(&content);
|
|
1007
|
+
if line_count > 200 {
|
|
1008
|
+
errors.push(format!(
|
|
1009
|
+
"{}: {line_count} lines",
|
|
1010
|
+
display_repo_path(root, &path)
|
|
1011
|
+
));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if errors.is_empty() {
|
|
1016
|
+
Ok(())
|
|
1017
|
+
} else {
|
|
1018
|
+
Err(NaomeError::new(format!(
|
|
1019
|
+
"NAOME context budget exceeded. Limit: 200 lines per file.\n{}",
|
|
1020
|
+
errors.join("\n")
|
|
1021
|
+
)))
|
|
835
1022
|
}
|
|
836
1023
|
}
|
|
837
1024
|
|
|
1025
|
+
fn template_root(root: &Path) -> PathBuf {
|
|
1026
|
+
root.join("packages")
|
|
1027
|
+
.join("naome")
|
|
1028
|
+
.join("templates")
|
|
1029
|
+
.join("naome-root")
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
fn markdown_files(dir: &Path) -> Result<Vec<PathBuf>, NaomeError> {
|
|
1033
|
+
let mut files = Vec::new();
|
|
1034
|
+
for entry in fs::read_dir(dir)? {
|
|
1035
|
+
let entry = entry?;
|
|
1036
|
+
let path = entry.path();
|
|
1037
|
+
if path.is_dir() {
|
|
1038
|
+
files.extend(markdown_files(&path)?);
|
|
1039
|
+
} else if path.is_file() && path.extension().is_some_and(|extension| extension == "md") {
|
|
1040
|
+
files.push(path);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
Ok(files)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
fn count_lines(content: &str) -> usize {
|
|
1047
|
+
if content.is_empty() {
|
|
1048
|
+
0
|
|
1049
|
+
} else if content.ends_with('\n') {
|
|
1050
|
+
content.split('\n').count() - 1
|
|
1051
|
+
} else {
|
|
1052
|
+
content.split('\n').count()
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
fn display_repo_path(root: &Path, path: &Path) -> String {
|
|
1057
|
+
path.strip_prefix(root)
|
|
1058
|
+
.unwrap_or(path)
|
|
1059
|
+
.to_string_lossy()
|
|
1060
|
+
.to_string()
|
|
1061
|
+
}
|
|
1062
|
+
|
|
838
1063
|
fn git_commit(root: &Path, message: &str) -> Result<(), NaomeError> {
|
|
839
1064
|
let output = Command::new("git")
|
|
840
1065
|
.args(["commit", "-m", message])
|
|
@@ -848,10 +1073,19 @@ fn git_commit(root: &Path, message: &str) -> Result<(), NaomeError> {
|
|
|
848
1073
|
}
|
|
849
1074
|
|
|
850
1075
|
fn create_isolated_task_worktree(root: &Path, prompt: &str) -> Result<RouteWorktree, NaomeError> {
|
|
1076
|
+
let name_head = task_worktree_name_head(root)?;
|
|
1077
|
+
create_isolated_task_worktree_with_name_head(root, prompt, &name_head)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
fn create_isolated_task_worktree_with_name_head(
|
|
1081
|
+
root: &Path,
|
|
1082
|
+
prompt: &str,
|
|
1083
|
+
name_head: &str,
|
|
1084
|
+
) -> Result<RouteWorktree, NaomeError> {
|
|
851
1085
|
let base_head = git_head(root)?.ok_or_else(|| {
|
|
852
1086
|
NaomeError::new("Cannot create task worktree because the repository has no HEAD.")
|
|
853
1087
|
})?;
|
|
854
|
-
let short_head =
|
|
1088
|
+
let short_head = name_head.chars().take(12).collect::<String>();
|
|
855
1089
|
let slug = prompt_slug(prompt);
|
|
856
1090
|
let common_git_dir = git_common_dir(root)?;
|
|
857
1091
|
let worktree_base = common_git_dir.join("naome").join("worktrees");
|
|
@@ -900,6 +1134,48 @@ fn create_isolated_task_worktree(root: &Path, prompt: &str) -> Result<RouteWorkt
|
|
|
900
1134
|
))
|
|
901
1135
|
}
|
|
902
1136
|
|
|
1137
|
+
fn preflight_isolated_task_worktree(
|
|
1138
|
+
root: &Path,
|
|
1139
|
+
prompt: &str,
|
|
1140
|
+
name_head: &str,
|
|
1141
|
+
) -> Result<(), NaomeError> {
|
|
1142
|
+
let short_head = name_head.chars().take(12).collect::<String>();
|
|
1143
|
+
let slug = prompt_slug(prompt);
|
|
1144
|
+
let common_git_dir = git_common_dir(root)?;
|
|
1145
|
+
let worktree_base = common_git_dir.join("naome").join("worktrees");
|
|
1146
|
+
fs::create_dir_all(&worktree_base)?;
|
|
1147
|
+
let worktree_count = existing_naome_task_worktree_count(&worktree_base)?;
|
|
1148
|
+
if worktree_count >= MAX_NAOME_TASK_WORKTREES {
|
|
1149
|
+
return Err(NaomeError::new(format!(
|
|
1150
|
+
"Too many NAOME task worktrees are present ({worktree_count}). Finish or remove old task worktrees before creating another isolated task worktree."
|
|
1151
|
+
)));
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
for attempt in 1..100 {
|
|
1155
|
+
let suffix = if attempt == 1 {
|
|
1156
|
+
String::new()
|
|
1157
|
+
} else {
|
|
1158
|
+
format!("-{attempt}")
|
|
1159
|
+
};
|
|
1160
|
+
let name = format!("{slug}-{short_head}{suffix}");
|
|
1161
|
+
let branch = format!("naome/task/{name}");
|
|
1162
|
+
let path = worktree_base.join(&name);
|
|
1163
|
+
if !path.exists() && !git_branch_exists(root, &branch)? {
|
|
1164
|
+
return Ok(());
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
Err(NaomeError::new(
|
|
1169
|
+
"Cannot create a unique NAOME task worktree after 99 attempts.",
|
|
1170
|
+
))
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
fn task_worktree_name_head(root: &Path) -> Result<String, NaomeError> {
|
|
1174
|
+
git_head(root)?.ok_or_else(|| {
|
|
1175
|
+
NaomeError::new("Cannot create task worktree because the repository has no HEAD.")
|
|
1176
|
+
})
|
|
1177
|
+
}
|
|
1178
|
+
|
|
903
1179
|
fn existing_naome_task_worktree_count(worktree_base: &Path) -> Result<usize, NaomeError> {
|
|
904
1180
|
let mut count = 0;
|
|
905
1181
|
for entry in fs::read_dir(worktree_base)? {
|
|
@@ -1469,10 +1469,10 @@ fn validate_commit_gate(
|
|
|
1469
1469
|
.get("status")
|
|
1470
1470
|
.and_then(Value::as_str)
|
|
1471
1471
|
.unwrap_or("invalid");
|
|
1472
|
-
if status == "complete" &&
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1472
|
+
if status == "complete" && is_deterministic_harness_refresh_diff(&changed_paths) {
|
|
1473
|
+
validate_pending_upgrade(task_state, root, errors)?;
|
|
1474
|
+
validate_completed_task_for_harness_refresh(task_state, root, &staged_entries, errors)?;
|
|
1475
|
+
return Ok(());
|
|
1476
1476
|
}
|
|
1477
1477
|
|
|
1478
1478
|
if status == "complete" {
|
|
@@ -1525,6 +1525,49 @@ fn validate_commit_gate(
|
|
|
1525
1525
|
Ok(())
|
|
1526
1526
|
}
|
|
1527
1527
|
|
|
1528
|
+
fn validate_completed_task_for_harness_refresh(
|
|
1529
|
+
task_state: &Value,
|
|
1530
|
+
root: &Path,
|
|
1531
|
+
staged_entries: &[ChangedEntry],
|
|
1532
|
+
errors: &mut Vec<String>,
|
|
1533
|
+
) -> Result<(), NaomeError> {
|
|
1534
|
+
validate_active_task(task_state.get("activeTask"), errors);
|
|
1535
|
+
validate_active_task_references(task_state.get("activeTask"), root, errors, Some("complete"))?;
|
|
1536
|
+
if !task_state.get("blocker").is_some_and(Value::is_null) {
|
|
1537
|
+
errors.push("complete task state must have blocker set to null.".to_string());
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
let Some(active_task) = task_state.get("activeTask") else {
|
|
1541
|
+
return Ok(());
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
let check_ids = read_verification_check_ids(root, errors)?;
|
|
1545
|
+
validate_required_check_ids(active_task, &check_ids, errors);
|
|
1546
|
+
|
|
1547
|
+
let mut validation_errors = Vec::new();
|
|
1548
|
+
validate_complete_task_against_entries(
|
|
1549
|
+
active_task,
|
|
1550
|
+
root,
|
|
1551
|
+
&check_ids,
|
|
1552
|
+
staged_entries,
|
|
1553
|
+
&mut validation_errors,
|
|
1554
|
+
)?;
|
|
1555
|
+
|
|
1556
|
+
let staged_harness_paths = task_diff_from_entries(active_task, staged_entries).outside_paths;
|
|
1557
|
+
let allowed_scope_error = format!(
|
|
1558
|
+
"Changed files outside allowedPaths: {}",
|
|
1559
|
+
staged_harness_paths.join(", ")
|
|
1560
|
+
);
|
|
1561
|
+
|
|
1562
|
+
errors.extend(
|
|
1563
|
+
validation_errors
|
|
1564
|
+
.into_iter()
|
|
1565
|
+
.filter(|error| error != &allowed_scope_error),
|
|
1566
|
+
);
|
|
1567
|
+
|
|
1568
|
+
Ok(())
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1528
1571
|
fn validate_push_gate(task_state: &Value, errors: &mut Vec<String>) {
|
|
1529
1572
|
let status = task_state
|
|
1530
1573
|
.get("status")
|
|
@@ -1598,8 +1641,11 @@ fn is_safe_harness_refresh_path(path: &str) -> bool {
|
|
|
1598
1641
|
is_packaged_machine_owned_path(path) || is_repair_support_path(path)
|
|
1599
1642
|
}
|
|
1600
1643
|
|
|
1601
|
-
fn
|
|
1644
|
+
fn is_deterministic_harness_refresh_diff(changed_paths: &[String]) -> bool {
|
|
1602
1645
|
!changed_paths.is_empty()
|
|
1646
|
+
&& changed_paths
|
|
1647
|
+
.iter()
|
|
1648
|
+
.any(|path| is_packaged_machine_owned_path(path) || is_repair_archive_path(path))
|
|
1603
1649
|
&& changed_paths
|
|
1604
1650
|
.iter()
|
|
1605
1651
|
.all(|path| is_safe_harness_refresh_path(path))
|
|
@@ -203,6 +203,50 @@ fn execute_route_commits_user_diff_after_quality_gate_passes() {
|
|
|
203
203
|
assert!(route.user_message.contains("quality gates passed"));
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
#[test]
|
|
207
|
+
fn execute_route_commits_product_diff_with_declared_safe_quality_checks() {
|
|
208
|
+
let repo = TestRepo::new("route-product-diff-quality-pass");
|
|
209
|
+
repo.init_git();
|
|
210
|
+
repo.write_file(
|
|
211
|
+
"packages/naome/crates/naome-core/src/route.rs",
|
|
212
|
+
"pub fn baseline() {}\n",
|
|
213
|
+
);
|
|
214
|
+
repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
|
|
215
|
+
repo.write_product_quality_verification();
|
|
216
|
+
repo.git(&["add", "."]);
|
|
217
|
+
repo.git(&["commit", "-m", "baseline"]);
|
|
218
|
+
repo.write_file(
|
|
219
|
+
"packages/naome/crates/naome-core/src/route.rs",
|
|
220
|
+
"pub fn changed() {}\n",
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
let route = evaluate_route(
|
|
224
|
+
repo.path(),
|
|
225
|
+
"commit my changes",
|
|
226
|
+
RouteOptions {
|
|
227
|
+
execute: true,
|
|
228
|
+
evaluation: EvaluationOptions::offline(),
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
.unwrap();
|
|
232
|
+
|
|
233
|
+
assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
|
|
234
|
+
assert!(route.allowed);
|
|
235
|
+
assert!(route.mutation_performed);
|
|
236
|
+
assert_eq!(
|
|
237
|
+
route.executed_actions,
|
|
238
|
+
vec![
|
|
239
|
+
"run_user_diff_quality_gate".to_string(),
|
|
240
|
+
"commit_user_diff".to_string()
|
|
241
|
+
]
|
|
242
|
+
);
|
|
243
|
+
assert_eq!(repo.git_status_short(), "");
|
|
244
|
+
|
|
245
|
+
let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
|
|
246
|
+
assert!(committed_paths.contains("packages/naome/crates/naome-core/src/route.rs"));
|
|
247
|
+
assert!(route.user_message.contains("quality gates passed"));
|
|
248
|
+
}
|
|
249
|
+
|
|
206
250
|
#[test]
|
|
207
251
|
fn execute_route_preserves_staged_rename_when_committing_user_diff() {
|
|
208
252
|
let repo = TestRepo::new("route-user-diff-staged-rename");
|
|
@@ -334,11 +378,17 @@ fn execute_route_refuses_user_diff_commit_when_check_mutates_after_diff_check()
|
|
|
334
378
|
assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
|
|
335
379
|
assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
|
|
336
380
|
assert!(repo.git_status_short().contains("README.md"));
|
|
337
|
-
|
|
381
|
+
assert_eq!(
|
|
382
|
+
fs::read_to_string(repo.path().join("README.md")).unwrap(),
|
|
383
|
+
"# Manual edit\n"
|
|
384
|
+
);
|
|
385
|
+
assert!(route
|
|
386
|
+
.user_message
|
|
387
|
+
.contains("will not execute repository-controlled verification commands"));
|
|
338
388
|
}
|
|
339
389
|
|
|
340
390
|
#[test]
|
|
341
|
-
fn
|
|
391
|
+
fn execute_route_refuses_user_diff_commit_when_diff_check_command_is_unsafe() {
|
|
342
392
|
let repo = TestRepo::new("route-user-diff-mutating-diff-check");
|
|
343
393
|
repo.init_git();
|
|
344
394
|
repo.write_file("README.md", "# Baseline\n");
|
|
@@ -394,10 +444,10 @@ fn execute_route_refuses_user_diff_commit_when_diff_check_adds_paths() {
|
|
|
394
444
|
assert!(!route.mutation_performed);
|
|
395
445
|
assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
|
|
396
446
|
assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
|
|
397
|
-
assert!(repo.
|
|
447
|
+
assert!(!repo.path().join("NEW.md").exists());
|
|
398
448
|
assert!(route
|
|
399
449
|
.user_message
|
|
400
|
-
.contains("Quality
|
|
450
|
+
.contains("Quality check diff-check has an unsafe command or cwd"));
|
|
401
451
|
}
|
|
402
452
|
|
|
403
453
|
#[test]
|
|
@@ -432,6 +482,79 @@ fn execute_route_refuses_user_diff_commit_when_quality_gate_fails() {
|
|
|
432
482
|
assert!(route.user_message.contains("quality gate failed"));
|
|
433
483
|
}
|
|
434
484
|
|
|
485
|
+
#[test]
|
|
486
|
+
fn execute_route_refuses_user_diff_commit_when_dogfood_health_finds_integrity_mismatch() {
|
|
487
|
+
let repo = TestRepo::from_template("route-user-diff-dogfood-health-integrity");
|
|
488
|
+
repo.init_git();
|
|
489
|
+
repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
|
|
490
|
+
repo.write_dogfood_readme_quality_verification();
|
|
491
|
+
repo.write_file(
|
|
492
|
+
".naome/bin/check-task-state.js",
|
|
493
|
+
"#!/usr/bin/env node\nconsole.log('already tampered');\n",
|
|
494
|
+
);
|
|
495
|
+
repo.git(&["add", "."]);
|
|
496
|
+
repo.git(&["commit", "-m", "baseline"]);
|
|
497
|
+
repo.write_file("README.md", "# Local edit\n");
|
|
498
|
+
let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
|
|
499
|
+
|
|
500
|
+
let route = evaluate_route(
|
|
501
|
+
repo.path(),
|
|
502
|
+
"commit my changes",
|
|
503
|
+
RouteOptions {
|
|
504
|
+
execute: true,
|
|
505
|
+
evaluation: EvaluationOptions::offline(),
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
.unwrap();
|
|
509
|
+
|
|
510
|
+
assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
|
|
511
|
+
assert!(!route.allowed);
|
|
512
|
+
assert!(!route.mutation_performed);
|
|
513
|
+
assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
|
|
514
|
+
assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
|
|
515
|
+
assert!(repo.git_status_short().contains("README.md"));
|
|
516
|
+
assert!(route.user_message.contains("quality gate failed"));
|
|
517
|
+
assert!(route.user_message.contains("integrity mismatch"));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
#[test]
|
|
521
|
+
fn execute_route_refuses_user_diff_commit_when_task_state_check_finds_template_integrity_mismatch()
|
|
522
|
+
{
|
|
523
|
+
let repo = TestRepo::new("route-user-diff-task-state-integrity");
|
|
524
|
+
repo.init_git();
|
|
525
|
+
repo.write_file("README.md", "# Baseline\n");
|
|
526
|
+
repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
|
|
527
|
+
repo.write_task_state_readme_quality_verification();
|
|
528
|
+
repo.copy_packaged_template_to_route_template_root();
|
|
529
|
+
repo.write_file(
|
|
530
|
+
"packages/naome/templates/naome-root/.naome/bin/check-task-state.js",
|
|
531
|
+
"#!/usr/bin/env node\nconsole.log('already tampered');\n",
|
|
532
|
+
);
|
|
533
|
+
repo.git(&["add", "."]);
|
|
534
|
+
repo.git(&["commit", "-m", "baseline"]);
|
|
535
|
+
repo.write_file("README.md", "# Local edit\n");
|
|
536
|
+
let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
|
|
537
|
+
|
|
538
|
+
let route = evaluate_route(
|
|
539
|
+
repo.path(),
|
|
540
|
+
"commit my changes",
|
|
541
|
+
RouteOptions {
|
|
542
|
+
execute: true,
|
|
543
|
+
evaluation: EvaluationOptions::offline(),
|
|
544
|
+
},
|
|
545
|
+
)
|
|
546
|
+
.unwrap();
|
|
547
|
+
|
|
548
|
+
assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
|
|
549
|
+
assert!(!route.allowed);
|
|
550
|
+
assert!(!route.mutation_performed);
|
|
551
|
+
assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
|
|
552
|
+
assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
|
|
553
|
+
assert!(repo.git_status_short().contains("README.md"));
|
|
554
|
+
assert!(route.user_message.contains("quality gate failed"));
|
|
555
|
+
assert!(route.user_message.contains("integrity mismatch"));
|
|
556
|
+
}
|
|
557
|
+
|
|
435
558
|
#[test]
|
|
436
559
|
fn execute_route_baselines_harness_refresh_before_dirty_repo_worktree() {
|
|
437
560
|
let repo = TestRepo::new("route-dirty-harness-refresh-worktree");
|
|
@@ -662,6 +785,57 @@ fn execute_route_refuses_to_create_more_than_max_isolated_worktrees() {
|
|
|
662
785
|
.contains("Too many NAOME task worktrees are present"));
|
|
663
786
|
}
|
|
664
787
|
|
|
788
|
+
#[test]
|
|
789
|
+
fn execute_route_preflights_worktree_before_completed_task_baseline() {
|
|
790
|
+
let repo =
|
|
791
|
+
TestRepo::completed_task_with_unrelated_user_edit("route-completed-worktree-preflight");
|
|
792
|
+
let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
|
|
793
|
+
let before_status = repo.git_status_short();
|
|
794
|
+
let common_dir = repo.git_stdout(&["rev-parse", "--git-common-dir"]);
|
|
795
|
+
let worktree_root = repo.path().join(common_dir).join("naome").join("worktrees");
|
|
796
|
+
fs::create_dir_all(&worktree_root).unwrap();
|
|
797
|
+
for index in 0..25 {
|
|
798
|
+
fs::create_dir_all(worktree_root.join(format!("stale-{index}"))).unwrap();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
let error = evaluate_route(
|
|
802
|
+
repo.path(),
|
|
803
|
+
"Add another line to README as a new task.",
|
|
804
|
+
RouteOptions {
|
|
805
|
+
execute: true,
|
|
806
|
+
evaluation: EvaluationOptions::offline(),
|
|
807
|
+
},
|
|
808
|
+
)
|
|
809
|
+
.unwrap_err();
|
|
810
|
+
|
|
811
|
+
assert!(error
|
|
812
|
+
.to_string()
|
|
813
|
+
.contains("Too many NAOME task worktrees are present"));
|
|
814
|
+
assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
|
|
815
|
+
assert_eq!(repo.git_status_short(), before_status);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
#[test]
|
|
819
|
+
fn execute_route_uses_preflighted_worktree_name_after_completed_task_baseline() {
|
|
820
|
+
let repo = TestRepo::completed_task_with_unrelated_user_edit("route-worktree-name-preflight");
|
|
821
|
+
let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
|
|
822
|
+
let before_short = &before_head[..12];
|
|
823
|
+
|
|
824
|
+
let route = evaluate_route(
|
|
825
|
+
repo.path(),
|
|
826
|
+
"Add another line to README as a new task.",
|
|
827
|
+
RouteOptions {
|
|
828
|
+
execute: true,
|
|
829
|
+
evaluation: EvaluationOptions::offline(),
|
|
830
|
+
},
|
|
831
|
+
)
|
|
832
|
+
.unwrap();
|
|
833
|
+
|
|
834
|
+
let worktree = route.worktree.expect("route should create a worktree");
|
|
835
|
+
assert!(worktree.branch.contains(before_short));
|
|
836
|
+
assert!(worktree.path.contains(before_short));
|
|
837
|
+
}
|
|
838
|
+
|
|
665
839
|
#[test]
|
|
666
840
|
fn dry_route_plans_harness_refresh_split_before_completed_task_baseline() {
|
|
667
841
|
let repo = TestRepo::completed_task_with_harness_refresh_diff("route-dry-harness-refresh");
|
|
@@ -875,6 +1049,27 @@ impl TestRepo {
|
|
|
875
1049
|
Self { root }
|
|
876
1050
|
}
|
|
877
1051
|
|
|
1052
|
+
fn from_template(name: &str) -> Self {
|
|
1053
|
+
let repo = Self::new(name);
|
|
1054
|
+
let template_root = packaged_template_root();
|
|
1055
|
+
copy_dir(&template_root, repo.path());
|
|
1056
|
+
fs::create_dir_all(repo.path().join(".naome/archive")).unwrap();
|
|
1057
|
+
repo
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
fn copy_packaged_template_to_route_template_root(&self) {
|
|
1061
|
+
copy_dir(&packaged_template_root(), &self.route_template_root());
|
|
1062
|
+
fs::create_dir_all(self.route_template_root().join(".naome/archive")).unwrap();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
fn route_template_root(&self) -> PathBuf {
|
|
1066
|
+
self.root
|
|
1067
|
+
.join("packages")
|
|
1068
|
+
.join("naome")
|
|
1069
|
+
.join("templates")
|
|
1070
|
+
.join("naome-root")
|
|
1071
|
+
}
|
|
1072
|
+
|
|
878
1073
|
fn completed_task_with_diff(name: &str) -> Self {
|
|
879
1074
|
let repo = Self::new(name);
|
|
880
1075
|
repo.init_git();
|
|
@@ -999,6 +1194,153 @@ impl TestRepo {
|
|
|
999
1194
|
);
|
|
1000
1195
|
}
|
|
1001
1196
|
|
|
1197
|
+
fn write_product_quality_verification(&self) {
|
|
1198
|
+
self.write_naome_json(
|
|
1199
|
+
"verification.json",
|
|
1200
|
+
json!({
|
|
1201
|
+
"schema": "naome.verification.v1",
|
|
1202
|
+
"version": 1,
|
|
1203
|
+
"status": "ready",
|
|
1204
|
+
"checks": [
|
|
1205
|
+
{
|
|
1206
|
+
"id": "installer-tests",
|
|
1207
|
+
"command": "npm run test:naome-installer",
|
|
1208
|
+
"cwd": ".",
|
|
1209
|
+
"purpose": "Validate installer behavior.",
|
|
1210
|
+
"cost": "medium",
|
|
1211
|
+
"source": "test",
|
|
1212
|
+
"evidence": ["scripts/naome-installer.test.js"],
|
|
1213
|
+
"lastVerified": null
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
"id": "rust-build",
|
|
1217
|
+
"command": "npm run build:rust",
|
|
1218
|
+
"cwd": ".",
|
|
1219
|
+
"purpose": "Build native CLI.",
|
|
1220
|
+
"cost": "medium",
|
|
1221
|
+
"source": "test",
|
|
1222
|
+
"evidence": ["packages/naome/Cargo.toml"],
|
|
1223
|
+
"lastVerified": null
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
"id": "decision-engine-tests",
|
|
1227
|
+
"command": "npm run test:decision-engine",
|
|
1228
|
+
"cwd": ".",
|
|
1229
|
+
"purpose": "Validate route decisions.",
|
|
1230
|
+
"cost": "fast",
|
|
1231
|
+
"source": "test",
|
|
1232
|
+
"evidence": ["packages/naome/crates/naome-core/tests/route.rs"],
|
|
1233
|
+
"lastVerified": null
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
"id": "package-dry-run",
|
|
1237
|
+
"command": "npm run pack:dry-run",
|
|
1238
|
+
"cwd": ".",
|
|
1239
|
+
"purpose": "Validate package metadata.",
|
|
1240
|
+
"cost": "medium",
|
|
1241
|
+
"source": "test",
|
|
1242
|
+
"evidence": ["packages/naome/package.json"],
|
|
1243
|
+
"lastVerified": null
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
"id": "diff-check",
|
|
1247
|
+
"command": "git diff --check",
|
|
1248
|
+
"cwd": ".",
|
|
1249
|
+
"purpose": "Reject whitespace errors.",
|
|
1250
|
+
"cost": "fast",
|
|
1251
|
+
"source": "test",
|
|
1252
|
+
"evidence": ["packages/naome/crates/naome-core/src/route.rs"],
|
|
1253
|
+
"lastVerified": null
|
|
1254
|
+
}
|
|
1255
|
+
],
|
|
1256
|
+
"changeTypes": [
|
|
1257
|
+
{
|
|
1258
|
+
"id": "product-installer-or-template",
|
|
1259
|
+
"description": "NAOME product changes.",
|
|
1260
|
+
"paths": ["packages/naome/**", "scripts/**"],
|
|
1261
|
+
"requiredChecks": [
|
|
1262
|
+
"installer-tests",
|
|
1263
|
+
"rust-build",
|
|
1264
|
+
"decision-engine-tests",
|
|
1265
|
+
"package-dry-run"
|
|
1266
|
+
],
|
|
1267
|
+
"recommendedChecks": [],
|
|
1268
|
+
"humanReview": false
|
|
1269
|
+
}
|
|
1270
|
+
],
|
|
1271
|
+
"releaseGates": []
|
|
1272
|
+
}),
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
fn write_dogfood_readme_quality_verification(&self) {
|
|
1277
|
+
self.write_naome_json(
|
|
1278
|
+
"verification.json",
|
|
1279
|
+
json!({
|
|
1280
|
+
"schema": "naome.verification.v1",
|
|
1281
|
+
"version": 1,
|
|
1282
|
+
"status": "ready",
|
|
1283
|
+
"checks": [
|
|
1284
|
+
{
|
|
1285
|
+
"id": "dogfood-health",
|
|
1286
|
+
"command": "npm run dogfood:health",
|
|
1287
|
+
"cwd": ".",
|
|
1288
|
+
"purpose": "Validate installed harness integrity.",
|
|
1289
|
+
"cost": "fast",
|
|
1290
|
+
"source": "test",
|
|
1291
|
+
"evidence": [".naome/bin/check-task-state.js"],
|
|
1292
|
+
"lastVerified": null
|
|
1293
|
+
}
|
|
1294
|
+
],
|
|
1295
|
+
"changeTypes": [
|
|
1296
|
+
{
|
|
1297
|
+
"id": "installed-self-hosted-harness",
|
|
1298
|
+
"description": "Installed harness files.",
|
|
1299
|
+
"paths": ["README.md"],
|
|
1300
|
+
"requiredChecks": ["dogfood-health"],
|
|
1301
|
+
"recommendedChecks": [],
|
|
1302
|
+
"humanReview": false
|
|
1303
|
+
}
|
|
1304
|
+
],
|
|
1305
|
+
"releaseGates": []
|
|
1306
|
+
}),
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
fn write_task_state_readme_quality_verification(&self) {
|
|
1311
|
+
self.write_naome_json(
|
|
1312
|
+
"verification.json",
|
|
1313
|
+
json!({
|
|
1314
|
+
"schema": "naome.verification.v1",
|
|
1315
|
+
"version": 1,
|
|
1316
|
+
"status": "ready",
|
|
1317
|
+
"checks": [
|
|
1318
|
+
{
|
|
1319
|
+
"id": "task-state-check",
|
|
1320
|
+
"command": "npm run check:task-state",
|
|
1321
|
+
"cwd": ".",
|
|
1322
|
+
"purpose": "Validate template task-state and harness integrity.",
|
|
1323
|
+
"cost": "fast",
|
|
1324
|
+
"source": "test",
|
|
1325
|
+
"evidence": ["packages/naome/templates/naome-root/.naome/bin/check-task-state.js"],
|
|
1326
|
+
"lastVerified": null
|
|
1327
|
+
}
|
|
1328
|
+
],
|
|
1329
|
+
"changeTypes": [
|
|
1330
|
+
{
|
|
1331
|
+
"id": "installed-self-hosted-harness",
|
|
1332
|
+
"description": "Installed harness files.",
|
|
1333
|
+
"paths": ["README.md"],
|
|
1334
|
+
"requiredChecks": ["task-state-check"],
|
|
1335
|
+
"recommendedChecks": [],
|
|
1336
|
+
"humanReview": false
|
|
1337
|
+
}
|
|
1338
|
+
],
|
|
1339
|
+
"releaseGates": []
|
|
1340
|
+
}),
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1002
1344
|
fn write_naome_json(&self, file_name: &str, value: serde_json::Value) {
|
|
1003
1345
|
let path = self.root.join(".naome").join(file_name);
|
|
1004
1346
|
fs::write(
|
|
@@ -1062,6 +1404,32 @@ impl TestRepo {
|
|
|
1062
1404
|
}
|
|
1063
1405
|
}
|
|
1064
1406
|
|
|
1407
|
+
fn packaged_template_root() -> PathBuf {
|
|
1408
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
1409
|
+
.join("..")
|
|
1410
|
+
.join("..")
|
|
1411
|
+
.join("templates")
|
|
1412
|
+
.join("naome-root")
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
fn copy_dir(from: &Path, to: &Path) {
|
|
1416
|
+
fs::create_dir_all(to).unwrap();
|
|
1417
|
+
for entry in fs::read_dir(from).unwrap() {
|
|
1418
|
+
let entry = entry.unwrap();
|
|
1419
|
+
let from_path = entry.path();
|
|
1420
|
+
let to_path = to.join(entry.file_name());
|
|
1421
|
+
let file_type = entry.file_type().unwrap();
|
|
1422
|
+
if file_type.is_dir() {
|
|
1423
|
+
copy_dir(&from_path, &to_path);
|
|
1424
|
+
} else if file_type.is_file() {
|
|
1425
|
+
if let Some(parent) = to_path.parent() {
|
|
1426
|
+
fs::create_dir_all(parent).unwrap();
|
|
1427
|
+
}
|
|
1428
|
+
fs::copy(&from_path, &to_path).unwrap();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1065
1433
|
fn completed_task_state(admission_head: &str) -> serde_json::Value {
|
|
1066
1434
|
json!({
|
|
1067
1435
|
"schema": "naome.task-state.v1",
|
|
@@ -222,6 +222,146 @@ fn commit_gate_allows_staged_harness_refresh_split_from_completed_task() {
|
|
|
222
222
|
assert!(report.errors.is_empty(), "{:#?}", report.errors);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
#[test]
|
|
226
|
+
fn commit_gate_allows_staged_harness_refresh_after_completed_task_is_baselined() {
|
|
227
|
+
let repo = TaskFixture::new(complete_task_state(json!({
|
|
228
|
+
"allowedPaths": ["README.md"],
|
|
229
|
+
"proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
|
|
230
|
+
})));
|
|
231
|
+
repo.install_healthy_harness();
|
|
232
|
+
repo.write("README.md", "# Completed task result\n");
|
|
233
|
+
repo.init_git();
|
|
234
|
+
repo.write("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
|
|
235
|
+
repo.write_json(
|
|
236
|
+
".naome/manifest.json",
|
|
237
|
+
json!({
|
|
238
|
+
"name": "naome",
|
|
239
|
+
"harnessVersion": "1.1.1",
|
|
240
|
+
"profile": "standard",
|
|
241
|
+
"machineOwned": MACHINE_OWNED_PATHS,
|
|
242
|
+
"projectOwned": PROJECT_OWNED_PATHS,
|
|
243
|
+
"integrity": {}
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
repo.git(["add", "AGENTS.md", ".naome/manifest.json"]);
|
|
247
|
+
|
|
248
|
+
let report = validate_task_state(
|
|
249
|
+
repo.path(),
|
|
250
|
+
TaskStateOptions {
|
|
251
|
+
mode: TaskStateMode::CommitGate,
|
|
252
|
+
..TaskStateOptions::default()
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
.unwrap();
|
|
256
|
+
|
|
257
|
+
assert!(report.errors.is_empty(), "{:#?}", report.errors);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn commit_gate_rejects_harness_refresh_when_completed_task_proof_is_missing() {
|
|
262
|
+
let repo = TaskFixture::new(complete_task_state(json!({
|
|
263
|
+
"allowedPaths": ["README.md"],
|
|
264
|
+
"proofResults": []
|
|
265
|
+
})));
|
|
266
|
+
repo.install_healthy_harness();
|
|
267
|
+
repo.write("README.md", "# Completed task result\n");
|
|
268
|
+
repo.init_git();
|
|
269
|
+
repo.write("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
|
|
270
|
+
repo.git(["add", "AGENTS.md"]);
|
|
271
|
+
|
|
272
|
+
let report = validate_task_state(
|
|
273
|
+
repo.path(),
|
|
274
|
+
TaskStateOptions {
|
|
275
|
+
mode: TaskStateMode::CommitGate,
|
|
276
|
+
..TaskStateOptions::default()
|
|
277
|
+
},
|
|
278
|
+
)
|
|
279
|
+
.unwrap();
|
|
280
|
+
|
|
281
|
+
assert!(
|
|
282
|
+
report
|
|
283
|
+
.errors
|
|
284
|
+
.iter()
|
|
285
|
+
.any(|error| error.contains("activeTask.proofResults missing proof result: diff-check")),
|
|
286
|
+
"{:#?}",
|
|
287
|
+
report.errors
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#[test]
|
|
292
|
+
fn commit_gate_rejects_harness_refresh_with_pending_upgrade_state() {
|
|
293
|
+
let repo = TaskFixture::new(complete_task_state(json!({
|
|
294
|
+
"allowedPaths": ["README.md"],
|
|
295
|
+
"proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
|
|
296
|
+
})));
|
|
297
|
+
repo.install_healthy_harness();
|
|
298
|
+
repo.write("README.md", "# Completed task result\n");
|
|
299
|
+
repo.init_git();
|
|
300
|
+
repo.write("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
|
|
301
|
+
repo.write_json(
|
|
302
|
+
".naome/upgrade-state.json",
|
|
303
|
+
json!({
|
|
304
|
+
"status": "needs_agent_upgrade",
|
|
305
|
+
"fromVersion": "1.1.0",
|
|
306
|
+
"toVersion": "1.1.1",
|
|
307
|
+
"pending": ["manual-step"],
|
|
308
|
+
"completed": []
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
repo.git(["add", "AGENTS.md", ".naome/upgrade-state.json"]);
|
|
312
|
+
|
|
313
|
+
let report = validate_task_state(
|
|
314
|
+
repo.path(),
|
|
315
|
+
TaskStateOptions {
|
|
316
|
+
mode: TaskStateMode::CommitGate,
|
|
317
|
+
..TaskStateOptions::default()
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
.unwrap();
|
|
321
|
+
|
|
322
|
+
assert!(
|
|
323
|
+
report
|
|
324
|
+
.errors
|
|
325
|
+
.iter()
|
|
326
|
+
.any(|error| error.contains("NAOME upgrade is pending")),
|
|
327
|
+
"{:#?}",
|
|
328
|
+
report.errors
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn commit_gate_rejects_support_only_harness_refresh_after_completed_task_is_baselined() {
|
|
334
|
+
let repo = TaskFixture::new(complete_task_state(json!({
|
|
335
|
+
"allowedPaths": ["README.md"],
|
|
336
|
+
"proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
|
|
337
|
+
})));
|
|
338
|
+
repo.install_healthy_harness();
|
|
339
|
+
repo.write("README.md", "# Completed task result\n");
|
|
340
|
+
repo.init_git();
|
|
341
|
+
repo.write_json(
|
|
342
|
+
".naome/upgrade-state.json",
|
|
343
|
+
json!({
|
|
344
|
+
"status": "complete",
|
|
345
|
+
"fromVersion": null,
|
|
346
|
+
"toVersion": "1.1.1",
|
|
347
|
+
"pending": [],
|
|
348
|
+
"completed": []
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
repo.git(["add", ".naome/upgrade-state.json"]);
|
|
352
|
+
|
|
353
|
+
let report = validate_task_state(
|
|
354
|
+
repo.path(),
|
|
355
|
+
TaskStateOptions {
|
|
356
|
+
mode: TaskStateMode::CommitGate,
|
|
357
|
+
..TaskStateOptions::default()
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
.unwrap();
|
|
361
|
+
|
|
362
|
+
assert!(!report.errors.is_empty(), "{:#?}", report.errors);
|
|
363
|
+
}
|
|
364
|
+
|
|
225
365
|
#[test]
|
|
226
366
|
fn commit_gate_ignores_unstaged_user_edits_outside_completed_task_scope() {
|
|
227
367
|
let repo = TaskFixture::new(complete_task_state(json!({
|
|
Binary file
|
package/native/linux-x64/naome
CHANGED
|
Binary file
|
package/package.json
CHANGED