@lamentis/naome 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/Cargo.lock +2 -2
  2. package/Cargo.toml +1 -1
  3. package/LICENSE +180 -21
  4. package/README.md +49 -6
  5. package/bin/naome-node.js +44 -4
  6. package/bin/naome.js +54 -16
  7. package/crates/naome-cli/Cargo.toml +1 -1
  8. package/crates/naome-cli/src/check_commands.rs +135 -0
  9. package/crates/naome-cli/src/cli_args.rs +5 -0
  10. package/crates/naome-cli/src/dispatcher.rs +36 -0
  11. package/crates/naome-cli/src/install_bridge.rs +83 -0
  12. package/crates/naome-cli/src/main.rs +57 -341
  13. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  14. package/crates/naome-cli/src/quality_commands.rs +141 -0
  15. package/crates/naome-cli/src/simple_commands.rs +53 -0
  16. package/crates/naome-cli/src/workflow_commands.rs +153 -0
  17. package/crates/naome-core/Cargo.toml +1 -1
  18. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  19. package/crates/naome-core/src/harness_health.rs +14 -126
  20. package/crates/naome-core/src/install_plan.rs +3 -0
  21. package/crates/naome-core/src/intent/classifier.rs +171 -0
  22. package/crates/naome-core/src/intent/envelope.rs +108 -0
  23. package/crates/naome-core/src/intent/legacy.rs +138 -0
  24. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  25. package/crates/naome-core/src/intent/model.rs +71 -0
  26. package/crates/naome-core/src/intent/patterns.rs +170 -0
  27. package/crates/naome-core/src/intent/resolver.rs +162 -0
  28. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  29. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  30. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  31. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  32. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  33. package/crates/naome-core/src/intent/risk.rs +40 -0
  34. package/crates/naome-core/src/intent/segment.rs +170 -0
  35. package/crates/naome-core/src/intent.rs +64 -879
  36. package/crates/naome-core/src/journal.rs +9 -20
  37. package/crates/naome-core/src/lib.rs +13 -0
  38. package/crates/naome-core/src/quality/adapters.rs +178 -0
  39. package/crates/naome-core/src/quality/baseline.rs +75 -0
  40. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  41. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  42. package/crates/naome-core/src/quality/checks.rs +228 -0
  43. package/crates/naome-core/src/quality/cleanup.rs +72 -0
  44. package/crates/naome-core/src/quality/config.rs +109 -0
  45. package/crates/naome-core/src/quality/mod.rs +90 -0
  46. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  47. package/crates/naome-core/src/quality/scanner.rs +367 -0
  48. package/crates/naome-core/src/quality/types.rs +289 -0
  49. package/crates/naome-core/src/route.rs +292 -17
  50. package/crates/naome-core/src/task_state/admission.rs +63 -0
  51. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  52. package/crates/naome-core/src/task_state/api.rs +130 -0
  53. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  54. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  55. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  56. package/crates/naome-core/src/task_state/completion.rs +72 -0
  57. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  58. package/crates/naome-core/src/task_state/diff.rs +95 -0
  59. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  60. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  61. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  62. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  63. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  64. package/crates/naome-core/src/task_state/mod.rs +38 -0
  65. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  66. package/crates/naome-core/src/task_state/progress.rs +123 -0
  67. package/crates/naome-core/src/task_state/proof.rs +139 -0
  68. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  69. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  70. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  71. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  72. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  73. package/crates/naome-core/src/task_state/repair.rs +168 -0
  74. package/crates/naome-core/src/task_state/shape.rs +117 -0
  75. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  76. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  77. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  78. package/crates/naome-core/src/task_state/types.rs +87 -0
  79. package/crates/naome-core/src/task_state/util.rs +137 -0
  80. package/crates/naome-core/src/verification/render.rs +122 -0
  81. package/crates/naome-core/src/verification.rs +176 -58
  82. package/crates/naome-core/src/verification_contract.rs +49 -21
  83. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  84. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  85. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  86. package/crates/naome-core/src/workflow/mod.rs +18 -0
  87. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  88. package/crates/naome-core/src/workflow/output.rs +111 -0
  89. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  90. package/crates/naome-core/src/workflow/phases.rs +169 -0
  91. package/crates/naome-core/src/workflow/policy.rs +156 -0
  92. package/crates/naome-core/src/workflow/processes.rs +91 -0
  93. package/crates/naome-core/src/workflow/types.rs +42 -0
  94. package/crates/naome-core/tests/harness_health.rs +3 -0
  95. package/crates/naome-core/tests/intent.rs +97 -792
  96. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  97. package/crates/naome-core/tests/intent_v2.rs +90 -0
  98. package/crates/naome-core/tests/quality.rs +425 -0
  99. package/crates/naome-core/tests/route.rs +221 -4
  100. package/crates/naome-core/tests/task_state.rs +3 -0
  101. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  102. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  103. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  104. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  105. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  106. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  107. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  108. package/native/darwin-arm64/naome +0 -0
  109. package/native/linux-x64/naome +0 -0
  110. package/package.json +2 -2
  111. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  112. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  113. package/templates/naome-root/.naome/bin/naome.js +34 -63
  114. package/templates/naome-root/.naome/manifest.json +20 -18
  115. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  116. package/templates/naome-root/.naome/repository-quality.json +24 -0
  117. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  118. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  119. package/templates/naome-root/.naome/verification.json +37 -0
  120. package/templates/naome-root/AGENTS.md +3 -0
  121. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  122. package/templates/naome-root/docs/naome/execution.md +25 -21
  123. package/templates/naome-root/docs/naome/index.md +4 -3
  124. package/templates/naome-root/docs/naome/repository-quality.md +43 -0
  125. package/templates/naome-root/docs/naome/testing.md +12 -0
  126. package/crates/naome-core/src/task_state.rs +0 -2210
@@ -8,15 +8,18 @@ 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};
14
15
  use crate::models::{Decision, NaomeError};
15
16
  use crate::paths;
17
+ use crate::quality::{check_repository_quality, QualityMode};
16
18
  use crate::task_state::{
17
19
  completed_task_commit_paths, completed_task_harness_refresh_diff, harness_refresh_diff,
18
- harness_refresh_with_unrelated_diff,
20
+ harness_refresh_with_unrelated_diff, validate_task_state, TaskStateMode, TaskStateOptions,
19
21
  };
22
+ use crate::verification_contract::validate_verification_contract;
20
23
 
21
24
  const MAX_NAOME_TASK_WORKTREES: usize = 25;
22
25
 
@@ -603,7 +606,7 @@ fn run_user_diff_quality_gate(
603
606
  "Quality check {check_id} is referenced but not defined."
604
607
  )));
605
608
  };
606
- run_quality_check(root, check)?;
609
+ run_quality_check(root, check_id, check)?;
607
610
  }
608
611
 
609
612
  let current_paths = git::changed_paths(root)?;
@@ -618,7 +621,7 @@ fn run_user_diff_quality_gate(
618
621
 
619
622
  validate_changed_text_whitespace(root, changed_paths)?;
620
623
  if let Some(check) = checks.get("diff-check") {
621
- run_quality_check(root, check)?;
624
+ run_quality_check(root, "diff-check", check)?;
622
625
  }
623
626
  let current_paths = git::changed_paths(root)?;
624
627
  let current_set = sorted_path_set(&current_paths);
@@ -826,27 +829,299 @@ fn push_unique_string(values: &mut Vec<String>, value: &str) {
826
829
  }
827
830
  }
828
831
 
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()?
832
+ fn run_quality_check(root: &Path, check_id: &str, check: &QualityCheck) -> Result<(), NaomeError> {
833
+ match check_id {
834
+ "installer-tests" => require_builtin_quality_check(
835
+ check_id,
836
+ check,
837
+ "npm run test:naome-installer",
838
+ ),
839
+ "rust-build" => require_builtin_quality_check(check_id, check, "npm run build:rust"),
840
+ "decision-engine-tests" => {
841
+ require_builtin_quality_check(check_id, check, "npm run test:decision-engine")
842
+ }
843
+ "package-dry-run" => require_builtin_quality_check(check_id, check, "npm run pack:dry-run"),
844
+ "diff-check" => {
845
+ require_builtin_quality_check(check_id, check, "git diff --check")?;
846
+ let output = Command::new("git")
847
+ .args(["diff", "--check"])
848
+ .current_dir(root)
849
+ .output()?;
850
+
851
+ if output.status.success() {
852
+ Ok(())
853
+ } else {
854
+ Err(NaomeError::new(command_output(&output)))
855
+ }
856
+ }
857
+ "naome-harness-health" => {
858
+ require_builtin_quality_check(
859
+ check_id,
860
+ check,
861
+ "node .naome/bin/check-harness-health.js",
862
+ )?;
863
+ run_harness_health_check(root)
864
+ }
865
+ "dogfood-health" => {
866
+ require_builtin_quality_check(check_id, check, "npm run dogfood:health")?;
867
+ run_harness_health_check(root)
868
+ }
869
+ "task-state-check" => {
870
+ require_builtin_quality_check(check_id, check, "npm run check:task-state")?;
871
+ run_template_task_state_check(root)
872
+ }
873
+ "verification-contract-check" => {
874
+ require_builtin_quality_check(
875
+ check_id,
876
+ check,
877
+ "npm run check:verification-contract",
878
+ )?;
879
+ run_template_verification_contract_check(root)
880
+ }
881
+ "context-budget-check" => {
882
+ require_builtin_quality_check(check_id, check, "npm run check:context-budget")?;
883
+ run_context_budget_check(root)
884
+ }
885
+ "repository-quality-check" => {
886
+ require_builtin_quality_check_any(
887
+ check_id,
888
+ check,
889
+ &[
890
+ "naome quality check --changed",
891
+ "node .naome/bin/naome.js quality check --changed",
892
+ "npm run check:repository-quality",
893
+ ],
894
+ )?;
895
+ run_repository_quality_check(root)
896
+ }
897
+ _ => Err(NaomeError::new(format!(
898
+ "Quality check {check_id} is not a built-in safe check; NAOME will not execute repository-controlled verification commands."
899
+ ))),
900
+ }
901
+ }
902
+
903
+ fn require_builtin_quality_check_any(
904
+ check_id: &str,
905
+ check: &QualityCheck,
906
+ expected_commands: &[&str],
907
+ ) -> Result<(), NaomeError> {
908
+ if check.cwd == "."
909
+ && expected_commands
910
+ .iter()
911
+ .any(|expected_command| check.command == *expected_command)
912
+ {
913
+ return Ok(());
914
+ }
915
+
916
+ Err(NaomeError::new(format!(
917
+ "Quality check {check_id} has an unsafe command or cwd; expected one of [{}] with cwd `.`.",
918
+ expected_commands
919
+ .iter()
920
+ .map(|command| format!("`{command}`"))
921
+ .collect::<Vec<_>>()
922
+ .join(", ")
923
+ )))
924
+ }
925
+
926
+ fn require_builtin_quality_check(
927
+ check_id: &str,
928
+ check: &QualityCheck,
929
+ expected_command: &str,
930
+ ) -> Result<(), NaomeError> {
931
+ if check.cwd == "." && check.command == expected_command {
932
+ return Ok(());
933
+ }
934
+
935
+ Err(NaomeError::new(format!(
936
+ "Quality check {check_id} has an unsafe command or cwd; expected command `{expected_command}` with cwd `.`."
937
+ )))
938
+ }
939
+
940
+ fn run_repository_quality_check(root: &Path) -> Result<(), NaomeError> {
941
+ let report = check_repository_quality(root, QualityMode::Changed)?;
942
+ if report.ok {
943
+ return Ok(());
944
+ }
945
+
946
+ let details = report
947
+ .violations
948
+ .iter()
949
+ .take(20)
950
+ .map(|violation| {
951
+ let location = violation
952
+ .line
953
+ .map(|line| format!("{}:{line}", violation.path))
954
+ .unwrap_or_else(|| violation.path.clone());
955
+ format!("{location} {}: {}", violation.check_id, violation.message)
956
+ })
957
+ .collect::<Vec<_>>()
958
+ .join("\n");
959
+ Err(NaomeError::new(format!(
960
+ "repository-quality-check failed with {} violation(s).\n{}",
961
+ report.violations.len(),
962
+ details
963
+ )))
964
+ }
965
+
966
+ fn run_harness_health_check(root: &Path) -> Result<(), NaomeError> {
967
+ let errors = validate_harness_health(
968
+ root,
969
+ HarnessHealthOptions {
970
+ expected_integrity: packaged_harness_integrity()?,
971
+ ..HarnessHealthOptions::default()
972
+ },
973
+ )?;
974
+ if errors.is_empty() {
975
+ Ok(())
836
976
  } else {
837
- Command::new("sh")
838
- .args(["-c", &check.command])
839
- .current_dir(cwd)
840
- .output()?
841
- };
977
+ Err(NaomeError::new(errors.join("\n")))
978
+ }
979
+ }
842
980
 
843
- if output.status.success() {
981
+ fn packaged_harness_integrity() -> Result<std::collections::HashMap<String, String>, NaomeError> {
982
+ const CHECKER: &str =
983
+ include_str!("../../../templates/naome-root/.naome/bin/check-harness-health.js");
984
+ let start_marker = "const expectedMachineOwnedIntegrity = Object.freeze({";
985
+ let start = CHECKER
986
+ .find(start_marker)
987
+ .ok_or_else(|| NaomeError::new("Packaged harness integrity block is missing."))?;
988
+ let body_start = start + start_marker.len();
989
+ let end = CHECKER[body_start..]
990
+ .find("\n});")
991
+ .map(|offset| body_start + offset)
992
+ .ok_or_else(|| NaomeError::new("Packaged harness integrity block is incomplete."))?;
993
+
994
+ let mut integrity = std::collections::HashMap::new();
995
+ for line in CHECKER[body_start..end].lines() {
996
+ let line = line.trim().trim_end_matches(',').trim();
997
+ if line.is_empty() {
998
+ continue;
999
+ }
1000
+ let Some((path, hash)) = line.split_once(':') else {
1001
+ return Err(NaomeError::new(format!(
1002
+ "Packaged harness integrity entry is invalid: {line}"
1003
+ )));
1004
+ };
1005
+ let path: String = serde_json::from_str(path.trim()).map_err(|error| {
1006
+ NaomeError::new(format!(
1007
+ "Packaged harness integrity path is invalid: {error}"
1008
+ ))
1009
+ })?;
1010
+ let hash: String = serde_json::from_str(hash.trim()).map_err(|error| {
1011
+ NaomeError::new(format!(
1012
+ "Packaged harness integrity hash is invalid: {error}"
1013
+ ))
1014
+ })?;
1015
+ integrity.insert(path, hash);
1016
+ }
1017
+
1018
+ if integrity.is_empty() {
1019
+ return Err(NaomeError::new(
1020
+ "Packaged harness integrity block is empty.",
1021
+ ));
1022
+ }
1023
+
1024
+ Ok(integrity)
1025
+ }
1026
+
1027
+ fn run_template_task_state_check(root: &Path) -> Result<(), NaomeError> {
1028
+ let template_root = template_root(root);
1029
+ let report = validate_task_state(
1030
+ &template_root,
1031
+ TaskStateOptions {
1032
+ mode: TaskStateMode::State,
1033
+ harness_health: Some(HarnessHealthOptions {
1034
+ expected_integrity: packaged_harness_integrity()?,
1035
+ allow_missing_archive: true,
1036
+ ..HarnessHealthOptions::default()
1037
+ }),
1038
+ },
1039
+ )?;
1040
+ if report.errors.is_empty() {
844
1041
  Ok(())
845
1042
  } else {
846
- Err(NaomeError::new(command_output(&output)))
1043
+ Err(NaomeError::new(report.errors.join("\n")))
1044
+ }
1045
+ }
1046
+
1047
+ fn run_template_verification_contract_check(root: &Path) -> Result<(), NaomeError> {
1048
+ let errors = validate_verification_contract(&template_root(root))?;
1049
+ if errors.is_empty() {
1050
+ Ok(())
1051
+ } else {
1052
+ Err(NaomeError::new(errors.join("\n")))
1053
+ }
1054
+ }
1055
+
1056
+ fn run_context_budget_check(root: &Path) -> Result<(), NaomeError> {
1057
+ let template_root = template_root(root);
1058
+ let mut context_files = vec![
1059
+ template_root.join("AGENTS.md"),
1060
+ template_root.join(".naomeignore"),
1061
+ ];
1062
+ context_files.extend(markdown_files(&template_root.join("docs").join("naome"))?);
1063
+ context_files.sort();
1064
+
1065
+ let mut errors = Vec::new();
1066
+ for path in context_files {
1067
+ let content = fs::read_to_string(&path)?;
1068
+ let line_count = count_lines(&content);
1069
+ if line_count > 200 {
1070
+ errors.push(format!(
1071
+ "{}: {line_count} lines",
1072
+ display_repo_path(root, &path)
1073
+ ));
1074
+ }
1075
+ }
1076
+
1077
+ if errors.is_empty() {
1078
+ Ok(())
1079
+ } else {
1080
+ Err(NaomeError::new(format!(
1081
+ "NAOME context budget exceeded. Limit: 200 lines per file.\n{}",
1082
+ errors.join("\n")
1083
+ )))
847
1084
  }
848
1085
  }
849
1086
 
1087
+ fn template_root(root: &Path) -> PathBuf {
1088
+ root.join("packages")
1089
+ .join("naome")
1090
+ .join("templates")
1091
+ .join("naome-root")
1092
+ }
1093
+
1094
+ fn markdown_files(dir: &Path) -> Result<Vec<PathBuf>, NaomeError> {
1095
+ let mut files = Vec::new();
1096
+ for entry in fs::read_dir(dir)? {
1097
+ let entry = entry?;
1098
+ let path = entry.path();
1099
+ if path.is_dir() {
1100
+ files.extend(markdown_files(&path)?);
1101
+ } else if path.is_file() && path.extension().is_some_and(|extension| extension == "md") {
1102
+ files.push(path);
1103
+ }
1104
+ }
1105
+ Ok(files)
1106
+ }
1107
+
1108
+ fn count_lines(content: &str) -> usize {
1109
+ if content.is_empty() {
1110
+ 0
1111
+ } else if content.ends_with('\n') {
1112
+ content.split('\n').count() - 1
1113
+ } else {
1114
+ content.split('\n').count()
1115
+ }
1116
+ }
1117
+
1118
+ fn display_repo_path(root: &Path, path: &Path) -> String {
1119
+ path.strip_prefix(root)
1120
+ .unwrap_or(path)
1121
+ .to_string_lossy()
1122
+ .to_string()
1123
+ }
1124
+
850
1125
  fn git_commit(root: &Path, message: &str) -> Result<(), NaomeError> {
851
1126
  let output = Command::new("git")
852
1127
  .args(["commit", "-m", message])
@@ -0,0 +1,63 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::completion::validate_complete_task;
8
+ use super::human_review_state::validate_human_review_state;
9
+ use super::progress::{checked_status, validate_clean_git_diff};
10
+ use super::shape::{
11
+ format_blocker, validate_active_task, validate_active_task_references, validate_blocker,
12
+ validate_idle_state,
13
+ };
14
+ pub(super) fn validate_admission(
15
+ task_state: &Value,
16
+ root: &Path,
17
+ errors: &mut Vec<String>,
18
+ ) -> Result<(), NaomeError> {
19
+ let status = checked_status(task_state, root, errors)?;
20
+ match status {
21
+ "idle" => validate_idle_state(task_state, errors),
22
+ "complete" => {
23
+ validate_active_task(task_state.get("activeTask"), errors);
24
+ validate_active_task_references(
25
+ task_state.get("activeTask"),
26
+ root,
27
+ errors,
28
+ Some(status),
29
+ )?;
30
+ validate_complete_task(
31
+ task_state.get("activeTask"),
32
+ task_state.get("blocker"),
33
+ root,
34
+ errors,
35
+ &mut Vec::new(),
36
+ )?;
37
+ }
38
+ "needs_human_review" => {
39
+ let start = errors.len();
40
+ validate_human_review_state(task_state, root, errors)?;
41
+ if errors.len() > start {
42
+ errors.push("Task admission is blocked because needs_human_review state is invalid; fix blocker paths and proof evidence before asking for a human decision.".to_string());
43
+ } else {
44
+ errors.push(format_blocker(
45
+ "Task admission is blocked",
46
+ task_state.get("blocker"),
47
+ ));
48
+ }
49
+ }
50
+ "blocked" => {
51
+ validate_blocker(task_state.get("blocker"), errors);
52
+ errors.push(format_blocker(
53
+ "Task admission is blocked",
54
+ task_state.get("blocker"),
55
+ ));
56
+ }
57
+ other => errors.push(format!(
58
+ "Task admission is blocked because task state is {other}."
59
+ )),
60
+ }
61
+
62
+ validate_clean_git_diff(task_state, root, errors)
63
+ }
@@ -0,0 +1,72 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::git_io::git_commit_exists;
8
+ use super::util::{is_iso_datetime, require_string, require_string_array_allow_empty};
9
+
10
+ pub(super) fn validate_admission_proof(
11
+ admission: Option<&Value>,
12
+ root: &Path,
13
+ errors: &mut Vec<String>,
14
+ ) -> Result<(), NaomeError> {
15
+ let Some(object) = admission.and_then(Value::as_object) else {
16
+ errors.push(
17
+ "activeTask.admission must be an object recorded from a passed admission check."
18
+ .to_string(),
19
+ );
20
+ return Ok(());
21
+ };
22
+ let prefix = "activeTask.admission";
23
+
24
+ require_string(object.get("command"), &format!("{prefix}.command"), errors);
25
+ require_string(object.get("cwd"), &format!("{prefix}.cwd"), errors);
26
+ require_string_array_allow_empty(
27
+ object.get("changedPaths"),
28
+ &format!("{prefix}.changedPaths"),
29
+ errors,
30
+ );
31
+ require_string(object.get("gitHead"), &format!("{prefix}.gitHead"), errors);
32
+
33
+ if object.get("command").and_then(Value::as_str)
34
+ != Some("node .naome/bin/check-task-state.js --admission")
35
+ {
36
+ errors.push(format!(
37
+ "{prefix}.command must be node .naome/bin/check-task-state.js --admission."
38
+ ));
39
+ }
40
+
41
+ if object.get("cwd").and_then(Value::as_str) != Some(".") {
42
+ errors.push(format!("{prefix}.cwd must be \".\"."));
43
+ }
44
+
45
+ match object.get("exitCode").and_then(Value::as_i64) {
46
+ Some(0) => {}
47
+ Some(_) => errors.push(format!("{prefix}.exitCode must be 0.")),
48
+ None => errors.push(format!("{prefix}.exitCode must be an integer.")),
49
+ }
50
+
51
+ if !object
52
+ .get("checkedAt")
53
+ .and_then(Value::as_str)
54
+ .is_some_and(is_iso_datetime)
55
+ {
56
+ errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
57
+ }
58
+
59
+ if let Some(changed_paths) = object.get("changedPaths").and_then(Value::as_array) {
60
+ if !changed_paths.is_empty() {
61
+ errors.push(format!("{prefix}.changedPaths must be empty because task admission requires a clean git diff."));
62
+ }
63
+ }
64
+
65
+ if let Some(git_head) = object.get("gitHead").and_then(Value::as_str) {
66
+ if !git_head.trim().is_empty() && !git_commit_exists(root, git_head)? {
67
+ errors.push(format!("{prefix}.gitHead must be an existing git commit."));
68
+ }
69
+ }
70
+
71
+ Ok(())
72
+ }
@@ -0,0 +1,130 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::completion::{
8
+ validate_admission, validate_commit_gate, validate_complete_task, validate_progress,
9
+ };
10
+ use super::diff::validate_human_review_blocker_paths;
11
+ use super::proof::validate_proof_evidence_covers_changed_paths;
12
+ use super::reconcile::validate_push_gate;
13
+ use super::shape::{
14
+ format_blocker, validate_active_task, validate_active_task_references, validate_blocker,
15
+ validate_idle_state, validate_pending_upgrade, validate_task_state_shape,
16
+ };
17
+ use super::task_diff_api::validate_harness_health_gate;
18
+ use super::types::*;
19
+ use super::util::read_json;
20
+ pub fn validate_task_state(
21
+ root: &Path,
22
+ options: TaskStateOptions,
23
+ ) -> Result<TaskStateReport, NaomeError> {
24
+ let mut report = TaskStateReport {
25
+ errors: Vec::new(),
26
+ notices: Vec::new(),
27
+ };
28
+ let Some(task_state) = read_json(root, ".naome/task-state.json", &mut report.errors)? else {
29
+ return Ok(report);
30
+ };
31
+
32
+ validate_task_state_shape(&task_state, &mut report.errors);
33
+ let status = task_state
34
+ .get("status")
35
+ .and_then(Value::as_str)
36
+ .unwrap_or("invalid");
37
+ if !ALLOWED_STATUS.contains(&status) {
38
+ return Ok(report);
39
+ }
40
+
41
+ validate_harness_health_gate(root, &options, &mut report.errors)?;
42
+ if !report.errors.is_empty() {
43
+ return Ok(report);
44
+ }
45
+
46
+ if validate_requested_mode(&task_state, root, options.mode, &mut report)? {
47
+ return Ok(report);
48
+ }
49
+
50
+ if status == "idle" {
51
+ validate_idle_state(&task_state, &mut report.errors);
52
+ return Ok(report);
53
+ }
54
+
55
+ let active_error_start = report.errors.len();
56
+ validate_active_task(task_state.get("activeTask"), &mut report.errors);
57
+ validate_pending_upgrade(&task_state, root, &mut report.errors)?;
58
+ validate_active_task_references(
59
+ task_state.get("activeTask"),
60
+ root,
61
+ &mut report.errors,
62
+ Some(status),
63
+ )?;
64
+
65
+ if status == "needs_human_review" {
66
+ validate_blocker(task_state.get("blocker"), &mut report.errors);
67
+ validate_human_review_blocker_paths(
68
+ task_state.get("activeTask"),
69
+ task_state.get("blocker"),
70
+ root,
71
+ &mut report.errors,
72
+ )?;
73
+ validate_proof_evidence_covers_changed_paths(
74
+ task_state.get("activeTask"),
75
+ root,
76
+ &mut report.errors,
77
+ )?;
78
+ if report.errors.len() > active_error_start {
79
+ report.errors.push("needs_human_review task state is invalid; fix blocker paths and proof evidence before asking for a human decision.".to_string());
80
+ } else {
81
+ report.errors.push(format_blocker(
82
+ "Human review required",
83
+ task_state.get("blocker"),
84
+ ));
85
+ }
86
+ return Ok(report);
87
+ }
88
+
89
+ if status == "blocked" {
90
+ validate_blocker(task_state.get("blocker"), &mut report.errors);
91
+ report
92
+ .errors
93
+ .push(format_blocker("Task is blocked", task_state.get("blocker")));
94
+ return Ok(report);
95
+ }
96
+
97
+ if BLOCKING_STATUS.contains(&status) {
98
+ report.errors.push(format!(
99
+ "Task is still {status}; new work must wait until the active task is complete or resolved."
100
+ ));
101
+ return Ok(report);
102
+ }
103
+
104
+ validate_complete_task(
105
+ task_state.get("activeTask"),
106
+ task_state.get("blocker"),
107
+ root,
108
+ &mut report.errors,
109
+ &mut report.notices,
110
+ )?;
111
+ Ok(report)
112
+ }
113
+
114
+ fn validate_requested_mode(
115
+ task_state: &Value,
116
+ root: &Path,
117
+ mode: TaskStateMode,
118
+ report: &mut TaskStateReport,
119
+ ) -> Result<bool, NaomeError> {
120
+ match mode {
121
+ TaskStateMode::Admission => validate_admission(task_state, root, &mut report.errors)?,
122
+ TaskStateMode::Progress => validate_progress(task_state, root, &mut report.errors)?,
123
+ TaskStateMode::CommitGate => {
124
+ validate_commit_gate(task_state, root, &mut report.errors, &mut report.notices)?;
125
+ }
126
+ TaskStateMode::PushGate => validate_push_gate(task_state, &mut report.errors),
127
+ TaskStateMode::State => return Ok(false),
128
+ }
129
+ Ok(true)
130
+ }