@lamentis/naome 1.1.2 → 1.2.1

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 (204) 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 +2 -1579
  6. package/bin/naome.js +68 -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 +37 -0
  11. package/crates/naome-cli/src/install_bridge.rs +83 -0
  12. package/crates/naome-cli/src/main.rs +60 -341
  13. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  14. package/crates/naome-cli/src/quality_commands.rs +229 -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/decision/checks.rs +64 -0
  19. package/crates/naome-core/src/decision/idle.rs +67 -0
  20. package/crates/naome-core/src/decision/json.rs +36 -0
  21. package/crates/naome-core/src/decision/states.rs +165 -0
  22. package/crates/naome-core/src/decision.rs +131 -353
  23. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  24. package/crates/naome-core/src/harness_health.rs +14 -126
  25. package/crates/naome-core/src/install_plan.rs +5 -0
  26. package/crates/naome-core/src/intent/classifier.rs +171 -0
  27. package/crates/naome-core/src/intent/envelope.rs +108 -0
  28. package/crates/naome-core/src/intent/legacy.rs +138 -0
  29. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  30. package/crates/naome-core/src/intent/model.rs +71 -0
  31. package/crates/naome-core/src/intent/patterns.rs +170 -0
  32. package/crates/naome-core/src/intent/resolver.rs +162 -0
  33. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  34. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  35. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  36. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  37. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  38. package/crates/naome-core/src/intent/risk.rs +40 -0
  39. package/crates/naome-core/src/intent/segment.rs +170 -0
  40. package/crates/naome-core/src/intent.rs +64 -879
  41. package/crates/naome-core/src/journal.rs +9 -20
  42. package/crates/naome-core/src/lib.rs +15 -0
  43. package/crates/naome-core/src/paths.rs +3 -1
  44. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  45. package/crates/naome-core/src/quality/adapters.rs +131 -0
  46. package/crates/naome-core/src/quality/baseline.rs +75 -0
  47. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  48. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  49. package/crates/naome-core/src/quality/checks.rs +228 -0
  50. package/crates/naome-core/src/quality/cleanup.rs +84 -0
  51. package/crates/naome-core/src/quality/config.rs +102 -0
  52. package/crates/naome-core/src/quality/config_support.rs +24 -0
  53. package/crates/naome-core/src/quality/mod.rs +108 -0
  54. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  55. package/crates/naome-core/src/quality/scanner.rs +379 -0
  56. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  57. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  58. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  59. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  60. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  61. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  62. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  63. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  64. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  65. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  66. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  67. package/crates/naome-core/src/quality/types.rs +292 -0
  68. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  69. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  70. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  71. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  72. package/crates/naome-core/src/route/context.rs +180 -0
  73. package/crates/naome-core/src/route/execution.rs +96 -0
  74. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  75. package/crates/naome-core/src/route/execution_support.rs +57 -0
  76. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  77. package/crates/naome-core/src/route/git_ops.rs +72 -0
  78. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  79. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  80. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  81. package/crates/naome-core/src/route/worktree.rs +75 -0
  82. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  83. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  84. package/crates/naome-core/src/route.rs +44 -1155
  85. package/crates/naome-core/src/task_state/admission.rs +63 -0
  86. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  87. package/crates/naome-core/src/task_state/api.rs +130 -0
  88. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  89. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  90. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  91. package/crates/naome-core/src/task_state/completion.rs +72 -0
  92. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  93. package/crates/naome-core/src/task_state/diff.rs +95 -0
  94. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  95. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  96. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  97. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  98. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  99. package/crates/naome-core/src/task_state/mod.rs +38 -0
  100. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  101. package/crates/naome-core/src/task_state/progress.rs +123 -0
  102. package/crates/naome-core/src/task_state/proof.rs +139 -0
  103. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  104. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  105. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  106. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  107. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  108. package/crates/naome-core/src/task_state/repair.rs +168 -0
  109. package/crates/naome-core/src/task_state/shape.rs +117 -0
  110. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  111. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  112. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  113. package/crates/naome-core/src/task_state/types.rs +87 -0
  114. package/crates/naome-core/src/task_state/util.rs +137 -0
  115. package/crates/naome-core/src/verification/render.rs +122 -0
  116. package/crates/naome-core/src/verification.rs +177 -58
  117. package/crates/naome-core/src/verification_contract.rs +49 -21
  118. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  119. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  120. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  121. package/crates/naome-core/src/workflow/mod.rs +18 -0
  122. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  123. package/crates/naome-core/src/workflow/output.rs +111 -0
  124. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  125. package/crates/naome-core/src/workflow/phases.rs +169 -0
  126. package/crates/naome-core/src/workflow/policy.rs +156 -0
  127. package/crates/naome-core/src/workflow/processes.rs +91 -0
  128. package/crates/naome-core/src/workflow/types.rs +42 -0
  129. package/crates/naome-core/tests/decision.rs +24 -118
  130. package/crates/naome-core/tests/harness_health.rs +5 -0
  131. package/crates/naome-core/tests/intent.rs +97 -792
  132. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  133. package/crates/naome-core/tests/intent_v2.rs +90 -0
  134. package/crates/naome-core/tests/quality.rs +319 -0
  135. package/crates/naome-core/tests/quality_structure.rs +116 -0
  136. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  137. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  138. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  139. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  140. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  141. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  142. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  143. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  144. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  145. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  146. package/crates/naome-core/tests/route.rs +1 -1476
  147. package/crates/naome-core/tests/route_baseline.rs +86 -0
  148. package/crates/naome-core/tests/route_completion.rs +141 -0
  149. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  150. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  151. package/crates/naome-core/tests/route_worktree.rs +54 -0
  152. package/crates/naome-core/tests/task_state.rs +60 -429
  153. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  154. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  155. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  156. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  157. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  158. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  159. package/crates/naome-core/tests/verification.rs +4 -45
  160. package/crates/naome-core/tests/verification_contract.rs +22 -78
  161. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  162. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  163. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  164. package/installer/agents.js +90 -0
  165. package/installer/context.js +67 -0
  166. package/installer/filesystem.js +166 -0
  167. package/installer/flows.js +84 -0
  168. package/installer/git-boundary.js +170 -0
  169. package/installer/git-hook-content.js +36 -0
  170. package/installer/git-hooks.js +134 -0
  171. package/installer/git-local.js +2 -0
  172. package/installer/git-shared.js +35 -0
  173. package/installer/harness-file-ops.js +140 -0
  174. package/installer/harness-files.js +56 -0
  175. package/installer/harness-verification.js +123 -0
  176. package/installer/install-plan.js +66 -0
  177. package/installer/main.js +25 -0
  178. package/installer/manifest-state.js +167 -0
  179. package/installer/native-build.js +24 -0
  180. package/installer/native-format.js +6 -0
  181. package/installer/native.js +162 -0
  182. package/installer/output.js +131 -0
  183. package/installer/version.js +32 -0
  184. package/native/darwin-arm64/naome +0 -0
  185. package/native/linux-x64/naome +0 -0
  186. package/package.json +3 -2
  187. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  188. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  189. package/templates/naome-root/.naome/bin/naome.js +51 -76
  190. package/templates/naome-root/.naome/manifest.json +22 -18
  191. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  192. package/templates/naome-root/.naome/repository-quality.json +24 -0
  193. package/templates/naome-root/.naome/repository-structure.json +90 -0
  194. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  195. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  196. package/templates/naome-root/.naome/verification.json +38 -0
  197. package/templates/naome-root/AGENTS.md +3 -0
  198. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  199. package/templates/naome-root/docs/naome/execution.md +25 -21
  200. package/templates/naome-root/docs/naome/index.md +5 -3
  201. package/templates/naome-root/docs/naome/repository-quality.md +46 -0
  202. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  203. package/templates/naome-root/docs/naome/testing.md +13 -0
  204. package/crates/naome-core/src/task_state.rs +0 -2210
@@ -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
+ }
@@ -0,0 +1,170 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::harness_health::validate_harness_health;
7
+ use crate::models::NaomeError;
8
+
9
+ use super::completion::validate_complete_task_against_entries;
10
+ use super::git_io::{read_git_changed_entries, read_git_changed_paths};
11
+ use super::reconcile::{
12
+ is_packaged_machine_owned_path, is_repair_archive_path, is_safe_harness_refresh_path,
13
+ };
14
+ use super::shape::{
15
+ read_verification_check_ids, validate_active_task, validate_active_task_references,
16
+ validate_pending_upgrade, validate_required_check_ids, validate_task_state_shape,
17
+ };
18
+ use super::types::{
19
+ CompletedTaskCommitDiff, HarnessRefreshDiff, HarnessRefreshWithUnrelatedDiff, TaskStateOptions,
20
+ CONTROL_STATE_PATH,
21
+ };
22
+ use super::util::{matches_any_pattern, read_json, string_array};
23
+
24
+ pub fn completed_task_commit_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
25
+ Ok(completed_task_commit_diff(root)?
26
+ .map(|diff| diff.task_paths)
27
+ .unwrap_or_default())
28
+ }
29
+
30
+ pub fn completed_task_commit_diff(
31
+ root: &Path,
32
+ ) -> Result<Option<CompletedTaskCommitDiff>, NaomeError> {
33
+ let mut read_errors = Vec::new();
34
+ let Some(task_state) = read_json(root, ".naome/task-state.json", &mut read_errors)? else {
35
+ return Ok(None);
36
+ };
37
+ if !read_errors.is_empty()
38
+ || task_state.get("status").and_then(Value::as_str) != Some("complete")
39
+ {
40
+ return Ok(None);
41
+ }
42
+ let Some(active_task) = task_state.get("activeTask") else {
43
+ return Ok(None);
44
+ };
45
+
46
+ let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
47
+ let mut task_entries = Vec::new();
48
+ let mut unrelated_paths = Vec::new();
49
+ for entry in read_git_changed_entries(root)? {
50
+ if entry.path == CONTROL_STATE_PATH || matches_any_pattern(&entry.path, &allowed_paths) {
51
+ task_entries.push(entry);
52
+ } else {
53
+ unrelated_paths.push(entry.path);
54
+ }
55
+ }
56
+
57
+ if task_entries.is_empty() {
58
+ return Ok(None);
59
+ }
60
+
61
+ let mut errors = Vec::new();
62
+ validate_task_state_shape(&task_state, &mut errors);
63
+ validate_active_task(Some(active_task), &mut errors);
64
+ validate_pending_upgrade(&task_state, root, &mut errors)?;
65
+ validate_active_task_references(Some(active_task), root, &mut errors, Some("complete"))?;
66
+ if !task_state.get("blocker").is_some_and(Value::is_null) {
67
+ errors.push("complete task state must have blocker set to null.".to_string());
68
+ }
69
+ let check_ids = read_verification_check_ids(root, &mut errors)?;
70
+ validate_required_check_ids(active_task, &check_ids, &mut errors);
71
+ validate_complete_task_against_entries(
72
+ active_task,
73
+ root,
74
+ &check_ids,
75
+ &task_entries,
76
+ &mut errors,
77
+ )?;
78
+ if !errors.is_empty() {
79
+ return Ok(None);
80
+ }
81
+
82
+ let mut task_paths: Vec<String> = task_entries
83
+ .into_iter()
84
+ .map(|entry| entry.path)
85
+ .collect::<HashSet<_>>()
86
+ .into_iter()
87
+ .collect();
88
+ task_paths.sort();
89
+ unrelated_paths.sort();
90
+
91
+ Ok(Some(CompletedTaskCommitDiff {
92
+ task_paths,
93
+ unrelated_paths,
94
+ }))
95
+ }
96
+
97
+ pub fn harness_refresh_diff(root: &Path) -> Result<Option<HarnessRefreshDiff>, NaomeError> {
98
+ let changed_paths = read_git_changed_paths(root)?;
99
+ let has_repair_signal = changed_paths
100
+ .iter()
101
+ .any(|path| is_packaged_machine_owned_path(path) || is_repair_archive_path(path));
102
+ if !has_repair_signal {
103
+ return Ok(None);
104
+ }
105
+
106
+ let mut harness_paths = Vec::new();
107
+ let mut unrelated_paths = Vec::new();
108
+
109
+ for path in changed_paths {
110
+ if is_safe_harness_refresh_path(&path) {
111
+ harness_paths.push(path);
112
+ } else {
113
+ unrelated_paths.push(path);
114
+ }
115
+ }
116
+
117
+ if harness_paths.is_empty() {
118
+ return Ok(None);
119
+ }
120
+
121
+ harness_paths.sort();
122
+ unrelated_paths.sort();
123
+
124
+ Ok(Some(HarnessRefreshDiff {
125
+ harness_paths,
126
+ unrelated_paths,
127
+ }))
128
+ }
129
+
130
+ pub fn harness_refresh_with_unrelated_diff(
131
+ root: &Path,
132
+ ) -> Result<Option<HarnessRefreshWithUnrelatedDiff>, NaomeError> {
133
+ let Some(diff) = harness_refresh_diff(root)? else {
134
+ return Ok(None);
135
+ };
136
+ if diff.unrelated_paths.is_empty() {
137
+ return Ok(None);
138
+ }
139
+
140
+ Ok(Some(HarnessRefreshWithUnrelatedDiff {
141
+ harness_paths: diff.harness_paths,
142
+ unrelated_paths: diff.unrelated_paths,
143
+ }))
144
+ }
145
+
146
+ pub(super) fn validate_harness_health_gate(
147
+ root: &Path,
148
+ options: &TaskStateOptions,
149
+ errors: &mut Vec<String>,
150
+ ) -> Result<(), NaomeError> {
151
+ let Some(health_options) = options.harness_health.clone() else {
152
+ return Ok(());
153
+ };
154
+
155
+ let health_errors = validate_harness_health(root, health_options)?;
156
+ if health_errors.is_empty() {
157
+ return Ok(());
158
+ }
159
+
160
+ errors.push(
161
+ "Harness health failed; normal NAOME task work is repair-only until machine-owned harness files are healthy. Human options: repair_harness, review_harness_health."
162
+ .to_string(),
163
+ );
164
+ errors.extend(
165
+ health_errors
166
+ .into_iter()
167
+ .map(|error| format!("Harness health: {error}")),
168
+ );
169
+ Ok(())
170
+ }
@@ -0,0 +1,131 @@
1
+ use serde_json::Value;
2
+
3
+ use super::util::{
4
+ is_iso_datetime, is_non_empty_string, require_string, require_string_array,
5
+ require_string_array_allow_empty, string_array,
6
+ };
7
+ pub(super) fn validate_revisions(revisions: Option<&Value>, errors: &mut Vec<String>) {
8
+ let Some(revisions) = revisions else {
9
+ return;
10
+ };
11
+ let Some(revisions) = revisions.as_array() else {
12
+ errors.push("activeTask.revisions must be an array when present.".to_string());
13
+ return;
14
+ };
15
+
16
+ for (index, revision) in revisions.iter().enumerate() {
17
+ let prefix = format!("activeTask.revisions[{index}]");
18
+ let Some(object) = revision.as_object() else {
19
+ errors.push(format!("{prefix} must be an object."));
20
+ continue;
21
+ };
22
+
23
+ require_string(object.get("request"), &format!("{prefix}.request"), errors);
24
+ validate_prompt_record(
25
+ object.get("userPrompt"),
26
+ &format!("{prefix}.userPrompt"),
27
+ errors,
28
+ );
29
+
30
+ if !object
31
+ .get("requestedAt")
32
+ .and_then(Value::as_str)
33
+ .is_some_and(is_iso_datetime)
34
+ {
35
+ errors.push(format!("{prefix}.requestedAt must be an ISO timestamp."));
36
+ }
37
+
38
+ if let Some(proof_stale) = object.get("proofStale") {
39
+ if !proof_stale.is_boolean() {
40
+ errors.push(format!("{prefix}.proofStale must be boolean when present."));
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ pub(super) fn validate_prompt_record(
47
+ prompt_record: Option<&Value>,
48
+ field_name: &str,
49
+ errors: &mut Vec<String>,
50
+ ) {
51
+ let Some(object) = prompt_record.and_then(Value::as_object) else {
52
+ errors.push(format!(
53
+ "{field_name} must be an object with receivedAt and text."
54
+ ));
55
+ return;
56
+ };
57
+
58
+ if !object
59
+ .get("receivedAt")
60
+ .and_then(Value::as_str)
61
+ .is_some_and(is_iso_datetime)
62
+ {
63
+ errors.push(format!("{field_name}.receivedAt must be an ISO timestamp."));
64
+ }
65
+
66
+ require_string(object.get("text"), &format!("{field_name}.text"), errors);
67
+ }
68
+
69
+ pub(super) fn validate_human_review(human_review: Option<&Value>, errors: &mut Vec<String>) {
70
+ let Some(object) = human_review.and_then(Value::as_object) else {
71
+ errors.push("activeTask.humanReview must be an object.".to_string());
72
+ return;
73
+ };
74
+
75
+ if !object.get("required").is_some_and(Value::is_boolean) {
76
+ errors.push("activeTask.humanReview.required must be boolean.".to_string());
77
+ }
78
+
79
+ if !object.get("approved").is_some_and(Value::is_boolean) {
80
+ errors.push("activeTask.humanReview.approved must be boolean.".to_string());
81
+ }
82
+
83
+ if let Some(reason) = object.get("reason") {
84
+ if !reason.is_null() && !reason.as_str().is_some_and(is_non_empty_string) {
85
+ errors.push("activeTask.humanReview.reason must be a string or null.".to_string());
86
+ }
87
+ }
88
+ }
89
+
90
+ pub(super) fn validate_blocker(blocker: Option<&Value>, errors: &mut Vec<String>) {
91
+ let Some(object) = blocker.and_then(Value::as_object) else {
92
+ errors.push(
93
+ "blocker must be an object when task state is blocked or needs human review."
94
+ .to_string(),
95
+ );
96
+ return;
97
+ };
98
+
99
+ require_string(object.get("type"), "blocker.type", errors);
100
+ require_string(object.get("message"), "blocker.message", errors);
101
+ require_string_array_allow_empty(object.get("paths"), "blocker.paths", errors);
102
+ require_string_array(object.get("humanOptions"), "blocker.humanOptions", errors);
103
+ }
104
+
105
+ pub(super) fn format_blocker(prefix: &str, blocker: Option<&Value>) -> String {
106
+ let Some(object) = blocker.and_then(Value::as_object) else {
107
+ return format!("{prefix}.");
108
+ };
109
+
110
+ let mut parts = vec![format!(
111
+ "{prefix}: {}",
112
+ object
113
+ .get("message")
114
+ .and_then(Value::as_str)
115
+ .unwrap_or("No message recorded.")
116
+ )];
117
+
118
+ if let Some(paths) = string_array(object.get("paths")) {
119
+ if !paths.is_empty() {
120
+ parts.push(format!("Paths: {}", paths.join(", ")));
121
+ }
122
+ }
123
+
124
+ if let Some(options) = string_array(object.get("humanOptions")) {
125
+ if !options.is_empty() {
126
+ parts.push(format!("Human options: {}", options.join(", ")));
127
+ }
128
+ }
129
+
130
+ parts.join(" ")
131
+ }