@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 CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.1.0"
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.0"
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
- if (installed.length > 0) {
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
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.1.0"
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.0"
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
 
@@ -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 before = git_head(root)?;
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 = create_isolated_task_worktree(root, prompt)?;
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 = create_isolated_task_worktree(root, prompt)?;
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(&current_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
- let cwd = root.join(&check.cwd);
819
- let output = if cfg!(windows) {
820
- Command::new("cmd")
821
- .args(["/C", &check.command])
822
- .current_dir(cwd)
823
- .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(())
824
914
  } else {
825
- Command::new("sh")
826
- .args(["-c", &check.command])
827
- .current_dir(cwd)
828
- .output()?
829
- };
915
+ Err(NaomeError::new(errors.join("\n")))
916
+ }
917
+ }
830
918
 
831
- 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() {
832
979
  Ok(())
833
980
  } else {
834
- 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
+ }
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 = base_head.chars().take(12).collect::<String>();
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" && completed_task_harness_refresh_diff(root)?.is_some() {
1473
- if is_safe_harness_refresh_diff(&changed_paths) {
1474
- return Ok(());
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 is_safe_harness_refresh_diff(changed_paths: &[String]) -> bool {
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
- 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");
@@ -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
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naome",
3
- "harnessVersion": "1.1.0",
3
+ "harnessVersion": "1.1.1",
4
4
  "profile": "standard",
5
5
  "installedAt": null,
6
6
  "machineOwned": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "status": "complete",
3
3
  "fromVersion": null,
4
- "toVersion": "1.1.0",
4
+ "toVersion": "1.1.1",
5
5
  "pending": [],
6
6
  "completed": []
7
7
  }