@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
@@ -0,0 +1,139 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ use super::diff::task_diff_from_entries;
9
+ pub(super) use super::evidence::{
10
+ evidence_entry_path, validate_control_state_paths, validate_control_state_patterns,
11
+ validate_evidence_array, validate_evidence_paths,
12
+ };
13
+ use super::git_io::read_git_changed_entries;
14
+ use super::proof_entry::validate_proof_result;
15
+ use super::proof_model::canonical_proofs;
16
+ use super::types::ChangedEntry;
17
+ use super::util::{is_iso_datetime, matches_any_pattern, normalize_path, string_array};
18
+ pub(super) fn validate_proof_result_entries(
19
+ active_task: &Value,
20
+ check_ids: &HashSet<String>,
21
+ root: &Path,
22
+ errors: &mut Vec<String>,
23
+ ) -> Result<(), NaomeError> {
24
+ if let Some(proofs) = active_task.get("proofResults").and_then(Value::as_array) {
25
+ for (index, proof) in proofs.iter().enumerate() {
26
+ validate_proof_result(proof, index, check_ids, root, errors, active_task)?;
27
+ }
28
+ }
29
+ for proof in canonical_proofs(active_task, root, errors)? {
30
+ if proof.command.trim().is_empty() {
31
+ errors.push(format!(
32
+ "activeTask proof command is empty: {}",
33
+ proof.check_id
34
+ ));
35
+ }
36
+ if proof.cwd.trim().is_empty() {
37
+ errors.push(format!("activeTask proof cwd is empty: {}", proof.check_id));
38
+ }
39
+ if !is_iso_datetime(&proof.checked_at) {
40
+ errors.push(format!(
41
+ "activeTask proof checkedAt must be an ISO timestamp: {}",
42
+ proof.check_id
43
+ ));
44
+ }
45
+ }
46
+
47
+ Ok(())
48
+ }
49
+
50
+ pub(super) fn validate_proof_results(
51
+ active_task: &Value,
52
+ check_ids: &HashSet<String>,
53
+ root: &Path,
54
+ errors: &mut Vec<String>,
55
+ ) -> Result<(), NaomeError> {
56
+ let Some(required_check_ids) = active_task
57
+ .get("requiredCheckIds")
58
+ .and_then(Value::as_array)
59
+ else {
60
+ return Ok(());
61
+ };
62
+ let proofs = canonical_proofs(active_task, root, errors)?;
63
+
64
+ for check_id in required_check_ids {
65
+ let Some(check_id) = check_id.as_str().filter(|value| !value.trim().is_empty()) else {
66
+ continue;
67
+ };
68
+
69
+ match proofs.iter().find(|proof| proof.check_id == check_id) {
70
+ Some(proof) if proof.exit_code == 0 => {}
71
+ Some(_) => errors.push(format!(
72
+ "activeTask.proofResults failed proof result: {check_id}"
73
+ )),
74
+ None => errors.push(format!(
75
+ "activeTask.proofResults missing proof result: {check_id}"
76
+ )),
77
+ }
78
+ }
79
+
80
+ validate_proof_result_entries(active_task, check_ids, root, errors)
81
+ }
82
+
83
+ pub(super) fn validate_proof_evidence_covers_changed_paths(
84
+ active_task: Option<&Value>,
85
+ root: &Path,
86
+ errors: &mut Vec<String>,
87
+ ) -> Result<(), NaomeError> {
88
+ let Some(active_task) = active_task else {
89
+ return Ok(());
90
+ };
91
+ let mut proof_errors = Vec::new();
92
+ let proofs = canonical_proofs(active_task, root, &mut proof_errors)?;
93
+ if proofs.is_empty() {
94
+ return Ok(());
95
+ }
96
+
97
+ let entries = read_git_changed_entries(root)?;
98
+ validate_proof_evidence_covers_changed_entries(active_task, root, &entries, errors)
99
+ }
100
+
101
+ pub(super) fn validate_proof_evidence_covers_changed_entries(
102
+ active_task: &Value,
103
+ root: &Path,
104
+ entries: &[ChangedEntry],
105
+ errors: &mut Vec<String>,
106
+ ) -> Result<(), NaomeError> {
107
+ let mut proof_errors = Vec::new();
108
+ let proofs = canonical_proofs(active_task, root, &mut proof_errors)?;
109
+ if proofs.is_empty() {
110
+ return Ok(());
111
+ }
112
+
113
+ let changed_paths = task_diff_from_entries(active_task, entries);
114
+ let evidence_paths: HashSet<String> = proofs
115
+ .iter()
116
+ .flat_map(|proof| proof.evidence.clone())
117
+ .filter_map(|entry| evidence_entry_path(&entry).map(normalize_path))
118
+ .collect();
119
+
120
+ let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
121
+ let changed_allowed_paths: Vec<String> = changed_paths
122
+ .diff_paths
123
+ .into_iter()
124
+ .filter(|path| matches_any_pattern(path, &allowed_paths))
125
+ .collect();
126
+ let missing_paths: Vec<String> = changed_allowed_paths
127
+ .into_iter()
128
+ .filter(|path| !evidence_paths.contains(path))
129
+ .collect();
130
+
131
+ if !missing_paths.is_empty() {
132
+ errors.push(format!(
133
+ "activeTask.proofResults evidence missing changed allowed paths: {}",
134
+ missing_paths.join(", ")
135
+ ));
136
+ }
137
+
138
+ Ok(())
139
+ }
@@ -0,0 +1,66 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ use super::proof::{
9
+ validate_control_state_paths, validate_evidence_array, validate_evidence_paths,
10
+ };
11
+ use super::util::{is_iso_datetime, require_string};
12
+
13
+ pub(super) fn validate_proof_result(
14
+ proof: &Value,
15
+ index: usize,
16
+ check_ids: &HashSet<String>,
17
+ root: &Path,
18
+ errors: &mut Vec<String>,
19
+ active_task: &Value,
20
+ ) -> Result<(), NaomeError> {
21
+ let prefix = format!("activeTask.proofResults[{index}]");
22
+ let Some(object) = proof.as_object() else {
23
+ errors.push(format!("{prefix} must be an object."));
24
+ return Ok(());
25
+ };
26
+
27
+ require_string(object.get("checkId"), &format!("{prefix}.checkId"), errors);
28
+ require_string(object.get("command"), &format!("{prefix}.command"), errors);
29
+ require_string(object.get("cwd"), &format!("{prefix}.cwd"), errors);
30
+ validate_evidence_array(
31
+ object.get("evidence"),
32
+ &format!("{prefix}.evidence"),
33
+ errors,
34
+ );
35
+ validate_control_state_paths(
36
+ object.get("evidence"),
37
+ &format!("{prefix}.evidence"),
38
+ errors,
39
+ );
40
+
41
+ if object.get("exitCode").and_then(Value::as_i64).is_none() {
42
+ errors.push(format!("{prefix}.exitCode must be an integer."));
43
+ }
44
+
45
+ if !object
46
+ .get("checkedAt")
47
+ .and_then(Value::as_str)
48
+ .is_some_and(is_iso_datetime)
49
+ {
50
+ errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
51
+ }
52
+
53
+ if let Some(check_id) = object.get("checkId").and_then(Value::as_str) {
54
+ if !check_id.trim().is_empty() && !check_ids.contains(check_id) {
55
+ errors.push(format!("{prefix}.checkId unknown check id: {check_id}"));
56
+ }
57
+ }
58
+
59
+ validate_evidence_paths(
60
+ object.get("evidence"),
61
+ &format!("{prefix}.evidence"),
62
+ root,
63
+ errors,
64
+ active_task,
65
+ )
66
+ }
@@ -0,0 +1,70 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::compact_proof::compact_proofs;
8
+ use super::proof_sources::{check_id_from_proof, read_verification_defaults};
9
+
10
+ #[derive(Debug, Clone)]
11
+ pub(super) struct CanonicalProof {
12
+ pub(super) check_id: String,
13
+ pub(super) command: String,
14
+ pub(super) cwd: String,
15
+ pub(super) exit_code: i64,
16
+ pub(super) checked_at: String,
17
+ pub(super) evidence: Vec<Value>,
18
+ }
19
+
20
+ #[derive(Debug, Clone)]
21
+ pub(super) struct VerificationDefaults {
22
+ pub(super) command: String,
23
+ pub(super) cwd: String,
24
+ }
25
+
26
+ pub(super) fn canonical_proofs(
27
+ active_task: &Value,
28
+ root: &Path,
29
+ errors: &mut Vec<String>,
30
+ ) -> Result<Vec<CanonicalProof>, NaomeError> {
31
+ let defaults = read_verification_defaults(root, errors)?;
32
+ let mut proofs = legacy_proofs(active_task);
33
+ proofs.extend(compact_proofs(active_task, root, errors, &defaults)?);
34
+ Ok(proofs)
35
+ }
36
+
37
+ pub(crate) fn canonical_proof_check_ids(active_task: &Value) -> Vec<String> {
38
+ let mut check_ids = Vec::new();
39
+ if let Some(proofs) = active_task.get("proofResults").and_then(Value::as_array) {
40
+ check_ids.extend(proofs.iter().filter_map(check_id_from_proof));
41
+ }
42
+ if let Some(batches) = active_task.get("proofBatches").and_then(Value::as_array) {
43
+ for batch in batches {
44
+ if let Some(proofs) = batch.get("proofs").and_then(Value::as_array) {
45
+ check_ids.extend(proofs.iter().filter_map(check_id_from_proof));
46
+ }
47
+ }
48
+ }
49
+ check_ids
50
+ }
51
+
52
+ fn legacy_proofs(active_task: &Value) -> Vec<CanonicalProof> {
53
+ active_task
54
+ .get("proofResults")
55
+ .and_then(Value::as_array)
56
+ .into_iter()
57
+ .flatten()
58
+ .filter_map(|proof| {
59
+ let evidence = proof.get("evidence")?.as_array()?.clone();
60
+ Some(CanonicalProof {
61
+ check_id: proof.get("checkId")?.as_str()?.to_string(),
62
+ command: proof.get("command")?.as_str()?.to_string(),
63
+ cwd: proof.get("cwd")?.as_str()?.to_string(),
64
+ exit_code: proof.get("exitCode")?.as_i64()?,
65
+ checked_at: proof.get("checkedAt")?.as_str()?.to_string(),
66
+ evidence,
67
+ })
68
+ })
69
+ .collect()
70
+ }
@@ -0,0 +1,76 @@
1
+ use std::collections::HashMap;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ use super::proof::{
9
+ validate_control_state_paths, validate_evidence_array, validate_evidence_paths,
10
+ };
11
+ use super::proof_model::VerificationDefaults;
12
+ use super::util::read_json;
13
+
14
+ pub(super) fn read_path_sets(
15
+ active_task: &Value,
16
+ root: &Path,
17
+ errors: &mut Vec<String>,
18
+ ) -> Result<HashMap<String, Vec<Value>>, NaomeError> {
19
+ let mut sets = HashMap::new();
20
+ let Some(path_sets) = active_task.get("proofPathSets") else {
21
+ return Ok(sets);
22
+ };
23
+ let Some(path_sets) = path_sets.as_object() else {
24
+ errors.push("activeTask.proofPathSets must be an object when present.".to_string());
25
+ return Ok(sets);
26
+ };
27
+ for (name, value) in path_sets {
28
+ let prefix = format!("activeTask.proofPathSets.{name}");
29
+ validate_evidence_array(Some(value), &prefix, errors);
30
+ validate_control_state_paths(Some(value), &prefix, errors);
31
+ validate_evidence_paths(Some(value), &prefix, root, errors, active_task)?;
32
+ if let Some(paths) = value.as_array() {
33
+ sets.insert(name.clone(), paths.clone());
34
+ }
35
+ }
36
+ Ok(sets)
37
+ }
38
+
39
+ pub(super) fn read_verification_defaults(
40
+ root: &Path,
41
+ errors: &mut Vec<String>,
42
+ ) -> Result<HashMap<String, VerificationDefaults>, NaomeError> {
43
+ let Some(verification) = read_json(root, ".naome/verification.json", errors)? else {
44
+ return Ok(HashMap::new());
45
+ };
46
+ let mut defaults = HashMap::new();
47
+ for check in verification
48
+ .get("checks")
49
+ .and_then(Value::as_array)
50
+ .into_iter()
51
+ .flatten()
52
+ {
53
+ let (Some(id), Some(command), Some(cwd)) = (
54
+ check.get("id").and_then(Value::as_str),
55
+ check.get("command").and_then(Value::as_str),
56
+ check.get("cwd").and_then(Value::as_str),
57
+ ) else {
58
+ continue;
59
+ };
60
+ defaults.insert(
61
+ id.to_string(),
62
+ VerificationDefaults {
63
+ command: command.to_string(),
64
+ cwd: cwd.to_string(),
65
+ },
66
+ );
67
+ }
68
+ Ok(defaults)
69
+ }
70
+
71
+ pub(super) fn check_id_from_proof(proof: &Value) -> Option<String> {
72
+ proof
73
+ .get("checkId")
74
+ .and_then(Value::as_str)
75
+ .map(ToString::to_string)
76
+ }
@@ -0,0 +1,49 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::repair::{is_completed_task_diff, is_harness_repair_diff, is_naome_baseline_diff};
8
+ use super::shape::validate_blocker;
9
+ use super::types::BLOCKING_STATUS;
10
+ pub(super) fn validate_push_gate(task_state: &Value, errors: &mut Vec<String>) {
11
+ let status = task_state
12
+ .get("status")
13
+ .and_then(Value::as_str)
14
+ .unwrap_or("invalid");
15
+ if !BLOCKING_STATUS.contains(&status) {
16
+ return;
17
+ }
18
+
19
+ if status == "blocked" || status == "needs_human_review" {
20
+ validate_blocker(task_state.get("blocker"), errors);
21
+ }
22
+
23
+ errors.push(format!("NAOME push gate blocked because task state is {status}. Resolve the active task before pushing. Human options: continue_current_task, request_task_changes, mark_task_blocked, cancel_task_state."));
24
+ }
25
+
26
+ pub(super) fn format_dirty_diff_admission_blocker(
27
+ task_state: &Value,
28
+ root: &Path,
29
+ changed_paths: &[String],
30
+ ) -> Result<String, NaomeError> {
31
+ let prefix = format!(
32
+ "Task admission requires a clean git diff. Changed paths: {}.",
33
+ changed_paths.join(", ")
34
+ );
35
+
36
+ if is_harness_repair_diff(root, changed_paths)? {
37
+ return Ok(format!("{prefix} These look like completed Harness Repair changes. Run NAOME intent for the next natural-language request before deciding whether to baseline, review, or cancel the repair diff."));
38
+ }
39
+
40
+ if is_completed_task_diff(task_state, changed_paths) {
41
+ return Ok(format!("{prefix} These look like completed task changes. Run NAOME intent for the next natural-language request; deterministic policy can baseline a valid completed task before creating the next task."));
42
+ }
43
+
44
+ if is_naome_baseline_diff(changed_paths) {
45
+ return Ok(format!("{prefix} These look like completed NAOME install or upgrade changes. Run NAOME intent for the next natural-language request; deterministic policy can baseline setup before creating the next task."));
46
+ }
47
+
48
+ Ok(format!("{prefix} Ask the user to choose exactly one: review_task_diff, request_task_changes, cancel_task_changes. Do not start new feature work or commit without explicit user selection."))
49
+ }
@@ -0,0 +1,7 @@
1
+ pub(super) use super::completed_refresh::add_completed_task_diff_notice;
2
+ pub(super) use super::push_gate::{format_dirty_diff_admission_blocker, validate_push_gate};
3
+ pub(super) use super::repair::{
4
+ is_deterministic_harness_refresh_diff, is_harness_repair_diff,
5
+ is_install_or_upgrade_baseline_diff, is_packaged_machine_owned_path, is_repair_archive_path,
6
+ is_safe_harness_refresh_path,
7
+ };
@@ -0,0 +1,168 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::install_plan::MACHINE_OWNED_PATHS;
7
+ use crate::models::NaomeError;
8
+
9
+ use super::types::CONTROL_STATE_PATH;
10
+ use super::util::{
11
+ is_non_empty_string, matches_any_pattern, normalize_path, read_json, string_array,
12
+ };
13
+ pub(super) fn is_harness_repair_diff(
14
+ root: &Path,
15
+ changed_paths: &[String],
16
+ ) -> Result<bool, NaomeError> {
17
+ let machine_owned_paths = read_machine_owned_paths(root)?;
18
+ if machine_owned_paths.is_empty() {
19
+ return Ok(false);
20
+ }
21
+
22
+ let has_repair_signal = changed_paths
23
+ .iter()
24
+ .any(|path| machine_owned_paths.contains(path) || is_repair_archive_path(path));
25
+ if !has_repair_signal {
26
+ return Ok(false);
27
+ }
28
+
29
+ Ok(changed_paths
30
+ .iter()
31
+ .all(|path| machine_owned_paths.contains(path) || is_repair_support_path(path)))
32
+ }
33
+
34
+ pub(super) fn is_repair_support_path(path: &str) -> bool {
35
+ path == ".naome/manifest.json"
36
+ || path == ".naome/upgrade-state.json"
37
+ || is_repair_archive_path(path)
38
+ }
39
+
40
+ pub(super) fn is_packaged_machine_owned_path(path: &str) -> bool {
41
+ MACHINE_OWNED_PATHS.iter().any(|owned| *owned == path)
42
+ }
43
+
44
+ pub(super) fn is_safe_harness_refresh_path(path: &str) -> bool {
45
+ is_packaged_machine_owned_path(path) || is_repair_support_path(path)
46
+ }
47
+
48
+ pub(super) fn is_deterministic_harness_refresh_diff(changed_paths: &[String]) -> bool {
49
+ !changed_paths.is_empty()
50
+ && changed_paths
51
+ .iter()
52
+ .any(|path| is_packaged_machine_owned_path(path) || is_repair_archive_path(path))
53
+ && changed_paths
54
+ .iter()
55
+ .all(|path| is_safe_harness_refresh_path(path))
56
+ }
57
+
58
+ pub(super) fn is_repair_archive_path(path: &str) -> bool {
59
+ path.starts_with(".naome/archive/repair-")
60
+ }
61
+
62
+ pub(super) fn is_completed_task_diff(task_state: &Value, changed_paths: &[String]) -> bool {
63
+ if task_state.get("status").and_then(Value::as_str) != Some("complete") {
64
+ return false;
65
+ }
66
+ let Some(active_task) = task_state.get("activeTask") else {
67
+ return false;
68
+ };
69
+ let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
70
+ let task_paths: Vec<&String> = changed_paths
71
+ .iter()
72
+ .filter(|path| path.as_str() != CONTROL_STATE_PATH)
73
+ .collect();
74
+ !task_paths.is_empty()
75
+ && task_paths
76
+ .iter()
77
+ .all(|path| matches_any_pattern(path, &allowed_paths))
78
+ }
79
+
80
+ pub(super) fn is_naome_baseline_diff(changed_paths: &[String]) -> bool {
81
+ changed_paths.iter().all(|path| {
82
+ path == "AGENTS.md"
83
+ || path == ".gitignore"
84
+ || path == ".naomeignore"
85
+ || path.starts_with(".naome/")
86
+ || path.starts_with("docs/naome/")
87
+ })
88
+ }
89
+
90
+ pub(super) fn is_install_or_upgrade_baseline_diff(
91
+ root: &Path,
92
+ changed_paths: &[String],
93
+ ) -> Result<bool, NaomeError> {
94
+ if !is_naome_baseline_diff(changed_paths) {
95
+ return Ok(false);
96
+ }
97
+
98
+ let has_setup_signal = changed_paths.iter().any(|path| {
99
+ matches!(
100
+ path.as_str(),
101
+ "AGENTS.md"
102
+ | ".gitignore"
103
+ | ".naomeignore"
104
+ | ".naome/init-state.json"
105
+ | ".naome/manifest.json"
106
+ | ".naome/package.json"
107
+ | ".naome/task-contract.schema.json"
108
+ | ".naome/upgrade-state.json"
109
+ )
110
+ });
111
+ if !has_setup_signal {
112
+ return Ok(false);
113
+ }
114
+
115
+ if read_init_incomplete(root)? || read_upgrade_baseline_signal(root)? {
116
+ return Ok(true);
117
+ }
118
+
119
+ Ok(changed_paths.iter().any(|path| {
120
+ matches!(
121
+ path.as_str(),
122
+ ".naome/init-state.json" | ".naome/manifest.json" | ".naome/upgrade-state.json"
123
+ )
124
+ }))
125
+ }
126
+
127
+ pub(super) fn read_init_incomplete(root: &Path) -> Result<bool, NaomeError> {
128
+ let Some(init_state) = read_json(root, ".naome/init-state.json", &mut Vec::new())? else {
129
+ return Ok(false);
130
+ };
131
+
132
+ Ok(
133
+ init_state.get("initialized").and_then(Value::as_bool) != Some(true)
134
+ || init_state.get("intakeStatus").and_then(Value::as_str) != Some("complete"),
135
+ )
136
+ }
137
+
138
+ pub(super) fn read_upgrade_baseline_signal(root: &Path) -> Result<bool, NaomeError> {
139
+ let Some(upgrade_state) = read_json(root, ".naome/upgrade-state.json", &mut Vec::new())? else {
140
+ return Ok(false);
141
+ };
142
+
143
+ Ok(upgrade_state.get("fromVersion").is_some()
144
+ || upgrade_state
145
+ .get("pending")
146
+ .and_then(Value::as_array)
147
+ .is_some_and(|pending| !pending.is_empty())
148
+ || upgrade_state
149
+ .get("completed")
150
+ .and_then(Value::as_array)
151
+ .is_some_and(|completed| !completed.is_empty()))
152
+ }
153
+
154
+ pub(super) fn read_machine_owned_paths(root: &Path) -> Result<HashSet<String>, NaomeError> {
155
+ let Some(manifest) = read_json(root, ".naome/manifest.json", &mut Vec::new())? else {
156
+ return Ok(HashSet::new());
157
+ };
158
+
159
+ Ok(manifest
160
+ .get("machineOwned")
161
+ .and_then(Value::as_array)
162
+ .into_iter()
163
+ .flatten()
164
+ .filter_map(Value::as_str)
165
+ .filter(|value| is_non_empty_string(value))
166
+ .map(normalize_path)
167
+ .collect())
168
+ }
@@ -0,0 +1,117 @@
1
+ use serde_json::Value;
2
+
3
+ use super::proof::validate_control_state_patterns;
4
+ pub(super) use super::task_records::{
5
+ format_blocker, validate_blocker, validate_human_review, validate_prompt_record,
6
+ validate_revisions,
7
+ };
8
+ pub(super) use super::task_references::{
9
+ read_verification_check_ids, validate_active_task_references, validate_pending_upgrade,
10
+ validate_required_check_ids,
11
+ };
12
+ use super::types::ALLOWED_STATUS;
13
+ use super::util::{
14
+ is_id, is_iso_datetime, require_string, require_string_array, require_string_array_allow_empty,
15
+ };
16
+ pub(super) fn validate_task_state_shape(task_state: &Value, errors: &mut Vec<String>) {
17
+ let Some(object) = task_state.as_object() else {
18
+ errors.push(".naome/task-state.json must be a JSON object.".to_string());
19
+ return;
20
+ };
21
+
22
+ match (
23
+ object.get("schema").and_then(Value::as_str),
24
+ object.get("version").and_then(Value::as_i64),
25
+ ) {
26
+ (Some("naome.task-state.v1"), Some(1)) | (Some("naome.task-state.v2"), Some(2)) => {}
27
+ _ => errors.push(
28
+ ".naome/task-state.json schema/version must be naome.task-state.v1/1 or naome.task-state.v2/2."
29
+ .to_string(),
30
+ ),
31
+ }
32
+
33
+ let status = object.get("status").and_then(Value::as_str);
34
+ if !status.is_some_and(|value| ALLOWED_STATUS.contains(&value)) {
35
+ errors.push(format!(
36
+ ".naome/task-state.json status must be one of: {}.",
37
+ ALLOWED_STATUS.join(", ")
38
+ ));
39
+ }
40
+
41
+ if let Some(updated_at) = object.get("updatedAt") {
42
+ if !updated_at.is_null() && !updated_at.as_str().is_some_and(is_iso_datetime) {
43
+ errors.push(
44
+ ".naome/task-state.json updatedAt must be an ISO timestamp or null.".to_string(),
45
+ );
46
+ }
47
+ }
48
+ }
49
+
50
+ pub(super) fn validate_idle_state(task_state: &Value, errors: &mut Vec<String>) {
51
+ if !task_state.get("activeTask").is_some_and(Value::is_null) {
52
+ errors.push("idle task state must have activeTask set to null.".to_string());
53
+ }
54
+
55
+ if !task_state.get("blocker").is_some_and(Value::is_null) {
56
+ errors.push("idle task state must have blocker set to null.".to_string());
57
+ }
58
+ }
59
+
60
+ pub(super) fn validate_active_task(active_task: Option<&Value>, errors: &mut Vec<String>) {
61
+ let Some(active_task) = active_task.and_then(Value::as_object) else {
62
+ errors.push("activeTask must be an object for active task states.".to_string());
63
+ return;
64
+ };
65
+
66
+ if !active_task
67
+ .get("id")
68
+ .and_then(Value::as_str)
69
+ .is_some_and(is_id)
70
+ {
71
+ errors.push("activeTask.id must be kebab-case lowercase.".to_string());
72
+ }
73
+
74
+ require_string(active_task.get("request"), "activeTask.request", errors);
75
+ validate_prompt_record(
76
+ active_task.get("userPrompt"),
77
+ "activeTask.userPrompt",
78
+ errors,
79
+ );
80
+ require_string_array(
81
+ active_task.get("allowedPaths"),
82
+ "activeTask.allowedPaths",
83
+ errors,
84
+ );
85
+ require_string_array(
86
+ active_task.get("declaredChangeTypes"),
87
+ "activeTask.declaredChangeTypes",
88
+ errors,
89
+ );
90
+ require_string_array_allow_empty(
91
+ active_task.get("requiredCheckIds"),
92
+ "activeTask.requiredCheckIds",
93
+ errors,
94
+ );
95
+ validate_control_state_patterns(
96
+ active_task.get("allowedPaths"),
97
+ "activeTask.allowedPaths",
98
+ errors,
99
+ );
100
+
101
+ let proof_results = active_task.get("proofResults");
102
+ let proof_batches = active_task.get("proofBatches");
103
+ if proof_results.is_some_and(|value| !value.is_array()) {
104
+ errors.push("activeTask.proofResults must be an array when present.".to_string());
105
+ }
106
+ if proof_batches.is_some_and(|value| !value.is_array()) {
107
+ errors.push("activeTask.proofBatches must be an array when present.".to_string());
108
+ }
109
+ if proof_results.is_none() && proof_batches.is_none() {
110
+ errors.push(
111
+ "activeTask.proofResults or activeTask.proofBatches must be present.".to_string(),
112
+ );
113
+ }
114
+
115
+ validate_revisions(active_task.get("revisions"), errors);
116
+ validate_human_review(active_task.get("humanReview"), errors);
117
+ }