@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 CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.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.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
- if (installed.length > 0) {
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
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.1.1"
3
+ version = "1.1.2"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.1.1"
3
+ version = "1.1.2"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -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(&current_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
- let cwd = root.join(&check.cwd);
831
- let output = if cfg!(windows) {
832
- Command::new("cmd")
833
- .args(["/C", &check.command])
834
- .current_dir(cwd)
835
- .output()?
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
- Command::new("sh")
838
- .args(["-c", &check.command])
839
- .current_dir(cwd)
840
- .output()?
841
- };
915
+ Err(NaomeError::new(errors.join("\n")))
916
+ }
917
+ }
842
918
 
843
- if output.status.success() {
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(command_output(&output)))
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
- assert!(route.user_message.contains("trailing whitespace"));
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 execute_route_refuses_user_diff_commit_when_diff_check_adds_paths() {
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.git_status_short().contains("NEW.md"));
447
+ assert!(!repo.path().join("NEW.md").exists());
398
448
  assert!(route
399
449
  .user_message
400
- .contains("Quality checks changed the diff path set"));
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
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "MIT",
6
6
  "type": "module",