@lamentis/naome 1.1.2 → 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 (125) 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.js +54 -16
  6. package/crates/naome-cli/Cargo.toml +1 -1
  7. package/crates/naome-cli/src/check_commands.rs +135 -0
  8. package/crates/naome-cli/src/cli_args.rs +5 -0
  9. package/crates/naome-cli/src/dispatcher.rs +36 -0
  10. package/crates/naome-cli/src/install_bridge.rs +83 -0
  11. package/crates/naome-cli/src/main.rs +57 -341
  12. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  13. package/crates/naome-cli/src/quality_commands.rs +141 -0
  14. package/crates/naome-cli/src/simple_commands.rs +53 -0
  15. package/crates/naome-cli/src/workflow_commands.rs +153 -0
  16. package/crates/naome-core/Cargo.toml +1 -1
  17. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  18. package/crates/naome-core/src/harness_health.rs +14 -126
  19. package/crates/naome-core/src/install_plan.rs +3 -0
  20. package/crates/naome-core/src/intent/classifier.rs +171 -0
  21. package/crates/naome-core/src/intent/envelope.rs +108 -0
  22. package/crates/naome-core/src/intent/legacy.rs +138 -0
  23. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  24. package/crates/naome-core/src/intent/model.rs +71 -0
  25. package/crates/naome-core/src/intent/patterns.rs +170 -0
  26. package/crates/naome-core/src/intent/resolver.rs +162 -0
  27. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  28. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  29. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  30. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  31. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  32. package/crates/naome-core/src/intent/risk.rs +40 -0
  33. package/crates/naome-core/src/intent/segment.rs +170 -0
  34. package/crates/naome-core/src/intent.rs +64 -879
  35. package/crates/naome-core/src/journal.rs +9 -20
  36. package/crates/naome-core/src/lib.rs +13 -0
  37. package/crates/naome-core/src/quality/adapters.rs +178 -0
  38. package/crates/naome-core/src/quality/baseline.rs +75 -0
  39. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  40. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  41. package/crates/naome-core/src/quality/checks.rs +228 -0
  42. package/crates/naome-core/src/quality/cleanup.rs +72 -0
  43. package/crates/naome-core/src/quality/config.rs +109 -0
  44. package/crates/naome-core/src/quality/mod.rs +90 -0
  45. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  46. package/crates/naome-core/src/quality/scanner.rs +367 -0
  47. package/crates/naome-core/src/quality/types.rs +289 -0
  48. package/crates/naome-core/src/route.rs +62 -0
  49. package/crates/naome-core/src/task_state/admission.rs +63 -0
  50. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  51. package/crates/naome-core/src/task_state/api.rs +130 -0
  52. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  53. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  54. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  55. package/crates/naome-core/src/task_state/completion.rs +72 -0
  56. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  57. package/crates/naome-core/src/task_state/diff.rs +95 -0
  58. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  59. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  60. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  61. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  62. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  63. package/crates/naome-core/src/task_state/mod.rs +38 -0
  64. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  65. package/crates/naome-core/src/task_state/progress.rs +123 -0
  66. package/crates/naome-core/src/task_state/proof.rs +139 -0
  67. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  68. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  69. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  70. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  71. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  72. package/crates/naome-core/src/task_state/repair.rs +168 -0
  73. package/crates/naome-core/src/task_state/shape.rs +117 -0
  74. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  75. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  76. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  77. package/crates/naome-core/src/task_state/types.rs +87 -0
  78. package/crates/naome-core/src/task_state/util.rs +137 -0
  79. package/crates/naome-core/src/verification/render.rs +122 -0
  80. package/crates/naome-core/src/verification.rs +176 -58
  81. package/crates/naome-core/src/verification_contract.rs +49 -21
  82. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  83. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  84. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  85. package/crates/naome-core/src/workflow/mod.rs +18 -0
  86. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  87. package/crates/naome-core/src/workflow/output.rs +111 -0
  88. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  89. package/crates/naome-core/src/workflow/phases.rs +169 -0
  90. package/crates/naome-core/src/workflow/policy.rs +156 -0
  91. package/crates/naome-core/src/workflow/processes.rs +91 -0
  92. package/crates/naome-core/src/workflow/types.rs +42 -0
  93. package/crates/naome-core/tests/harness_health.rs +3 -0
  94. package/crates/naome-core/tests/intent.rs +97 -792
  95. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  96. package/crates/naome-core/tests/intent_v2.rs +90 -0
  97. package/crates/naome-core/tests/quality.rs +425 -0
  98. package/crates/naome-core/tests/route.rs +88 -188
  99. package/crates/naome-core/tests/task_state.rs +3 -0
  100. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  101. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  102. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  103. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  104. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  105. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  106. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  107. package/native/darwin-arm64/naome +0 -0
  108. package/native/linux-x64/naome +0 -0
  109. package/package.json +2 -2
  110. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  111. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  112. package/templates/naome-root/.naome/bin/naome.js +34 -63
  113. package/templates/naome-root/.naome/manifest.json +20 -18
  114. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  115. package/templates/naome-root/.naome/repository-quality.json +24 -0
  116. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  117. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  118. package/templates/naome-root/.naome/verification.json +37 -0
  119. package/templates/naome-root/AGENTS.md +3 -0
  120. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  121. package/templates/naome-root/docs/naome/execution.md +25 -21
  122. package/templates/naome-root/docs/naome/index.md +4 -3
  123. package/templates/naome-root/docs/naome/repository-quality.md +43 -0
  124. package/templates/naome-root/docs/naome/testing.md +12 -0
  125. 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
+ }