@lamentis/naome 1.1.1 → 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 +44 -4
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/route.rs +230 -17
- package/crates/naome-core/tests/route.rs +321 -4
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.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";
|
|
@@ -1455,26 +1456,31 @@ function printSummary() {
|
|
|
1455
1456
|
console.log("");
|
|
1456
1457
|
console.log(`${color.green("+")} ${summaryTitle}`);
|
|
1457
1458
|
|
|
1458
|
-
|
|
1459
|
+
const detailedOutput = shouldPrintDetailedSummary();
|
|
1460
|
+
if (!detailedOutput) {
|
|
1461
|
+
printCompactSummary();
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (detailedOutput && installed.length > 0) {
|
|
1459
1465
|
const installedItems = uniqueStrings(installed);
|
|
1460
1466
|
printSection(`Installed ${installedItems.length} ${installedItems.length === 1 ? "item" : "items"}`);
|
|
1461
1467
|
printList(installed, "+");
|
|
1462
1468
|
}
|
|
1463
1469
|
|
|
1464
|
-
if (updated.length > 0) {
|
|
1470
|
+
if (detailedOutput && updated.length > 0) {
|
|
1465
1471
|
const updatedItems = uniqueStrings(updated);
|
|
1466
1472
|
printSection(`Updated ${updatedItems.length} ${updatedItems.length === 1 ? "item" : "items"}`);
|
|
1467
1473
|
printList(updated, "~");
|
|
1468
1474
|
}
|
|
1469
1475
|
|
|
1470
|
-
if (archived.length > 0) {
|
|
1476
|
+
if (detailedOutput && archived.length > 0) {
|
|
1471
1477
|
printSection("Archived");
|
|
1472
1478
|
for (const entry of archived) {
|
|
1473
1479
|
console.log(`${color.dim(">")} ${entry.from} -> ${entry.to}`);
|
|
1474
1480
|
}
|
|
1475
1481
|
}
|
|
1476
1482
|
|
|
1477
|
-
if (skipped.length > 0) {
|
|
1483
|
+
if (detailedOutput && skipped.length > 0) {
|
|
1478
1484
|
const skippedItems = uniqueStrings(skipped);
|
|
1479
1485
|
printSection(`Skipped ${skippedItems.length} existing ${skippedItems.length === 1 ? "path" : "paths"}`);
|
|
1480
1486
|
printList(skipped, "-");
|
|
@@ -1485,6 +1491,11 @@ function printSummary() {
|
|
|
1485
1491
|
printList(unsafeSkipped, "!");
|
|
1486
1492
|
}
|
|
1487
1493
|
|
|
1494
|
+
if (isRepositoryInitialized()) {
|
|
1495
|
+
console.log("");
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1488
1499
|
printSection("Next step");
|
|
1489
1500
|
console.log("Copy this into your coding agent:");
|
|
1490
1501
|
console.log("");
|
|
@@ -1494,6 +1505,35 @@ function printSummary() {
|
|
|
1494
1505
|
console.log("");
|
|
1495
1506
|
}
|
|
1496
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
|
+
|
|
1497
1537
|
assertTemplateRoot();
|
|
1498
1538
|
loadInstallPlan();
|
|
1499
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
|
|
|
@@ -603,7 +605,7 @@ fn run_user_diff_quality_gate(
|
|
|
603
605
|
"Quality check {check_id} is referenced but not defined."
|
|
604
606
|
)));
|
|
605
607
|
};
|
|
606
|
-
run_quality_check(root, check)?;
|
|
608
|
+
run_quality_check(root, check_id, check)?;
|
|
607
609
|
}
|
|
608
610
|
|
|
609
611
|
let current_paths = git::changed_paths(root)?;
|
|
@@ -618,7 +620,7 @@ fn run_user_diff_quality_gate(
|
|
|
618
620
|
|
|
619
621
|
validate_changed_text_whitespace(root, changed_paths)?;
|
|
620
622
|
if let Some(check) = checks.get("diff-check") {
|
|
621
|
-
run_quality_check(root, check)?;
|
|
623
|
+
run_quality_check(root, "diff-check", check)?;
|
|
622
624
|
}
|
|
623
625
|
let current_paths = git::changed_paths(root)?;
|
|
624
626
|
let current_set = sorted_path_set(¤t_paths);
|
|
@@ -826,25 +828,236 @@ fn push_unique_string(values: &mut Vec<String>, value: &str) {
|
|
|
826
828
|
}
|
|
827
829
|
}
|
|
828
830
|
|
|
829
|
-
fn run_quality_check(root: &Path, check: &QualityCheck) -> Result<(), NaomeError> {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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(())
|
|
836
914
|
} else {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
.output()?
|
|
841
|
-
};
|
|
915
|
+
Err(NaomeError::new(errors.join("\n")))
|
|
916
|
+
}
|
|
917
|
+
}
|
|
842
918
|
|
|
843
|
-
|
|
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() {
|
|
844
979
|
Ok(())
|
|
845
980
|
} else {
|
|
846
|
-
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
|
+
}
|
|
847
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
|
+
)))
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
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()
|
|
848
1061
|
}
|
|
849
1062
|
|
|
850
1063
|
fn git_commit(root: &Path, message: &str) -> Result<(), NaomeError> {
|
|
@@ -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");
|
|
@@ -926,6 +1049,27 @@ impl TestRepo {
|
|
|
926
1049
|
Self { root }
|
|
927
1050
|
}
|
|
928
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
|
+
|
|
929
1073
|
fn completed_task_with_diff(name: &str) -> Self {
|
|
930
1074
|
let repo = Self::new(name);
|
|
931
1075
|
repo.init_git();
|
|
@@ -1050,6 +1194,153 @@ impl TestRepo {
|
|
|
1050
1194
|
);
|
|
1051
1195
|
}
|
|
1052
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
|
+
|
|
1053
1344
|
fn write_naome_json(&self, file_name: &str, value: serde_json::Value) {
|
|
1054
1345
|
let path = self.root.join(".naome").join(file_name);
|
|
1055
1346
|
fs::write(
|
|
@@ -1113,6 +1404,32 @@ impl TestRepo {
|
|
|
1113
1404
|
}
|
|
1114
1405
|
}
|
|
1115
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
|
+
|
|
1116
1433
|
fn completed_task_state(admission_head: &str) -> serde_json::Value {
|
|
1117
1434
|
json!({
|
|
1118
1435
|
"schema": "naome.task-state.v1",
|
|
Binary file
|
package/native/linux-x64/naome
CHANGED
|
Binary file
|