@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,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
+ }
@@ -0,0 +1,126 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ pub(super) use super::admission_proof::validate_admission_proof;
9
+ use super::git_io::read_git_head;
10
+ use super::proof::validate_proof_result_entries;
11
+ use super::util::{joined_strings, read_json};
12
+ pub(super) fn validate_pending_upgrade(
13
+ _task_state: &Value,
14
+ root: &Path,
15
+ errors: &mut Vec<String>,
16
+ ) -> Result<(), NaomeError> {
17
+ if !root.join(".naome/upgrade-state.json").exists() {
18
+ return Ok(());
19
+ }
20
+
21
+ let Some(upgrade_state) = read_json(root, ".naome/upgrade-state.json", errors)? else {
22
+ return Ok(());
23
+ };
24
+
25
+ if upgrade_state.get("status").and_then(Value::as_str) == Some("needs_agent_upgrade") {
26
+ let pending = joined_strings(upgrade_state.get("pending"), "unknown");
27
+ errors.push(format!(
28
+ "NAOME upgrade is pending. Finish docs/naome/upgrade.md before feature work. Pending: {pending}"
29
+ ));
30
+ }
31
+
32
+ Ok(())
33
+ }
34
+
35
+ pub(super) fn validate_active_task_references(
36
+ active_task: Option<&Value>,
37
+ root: &Path,
38
+ errors: &mut Vec<String>,
39
+ status: Option<&str>,
40
+ ) -> Result<(), NaomeError> {
41
+ let Some(active_task) = active_task else {
42
+ return Ok(());
43
+ };
44
+
45
+ validate_admission_proof(active_task.get("admission"), root, errors)?;
46
+ validate_external_git_reconciliation(active_task, status, root, errors)?;
47
+ let check_ids = read_verification_check_ids(root, errors)?;
48
+ validate_required_check_ids(active_task, &check_ids, errors);
49
+ validate_proof_result_entries(active_task, &check_ids, root, errors)?;
50
+ Ok(())
51
+ }
52
+
53
+ pub(super) fn validate_external_git_reconciliation(
54
+ active_task: &Value,
55
+ status: Option<&str>,
56
+ root: &Path,
57
+ errors: &mut Vec<String>,
58
+ ) -> Result<(), NaomeError> {
59
+ if status == Some("complete") {
60
+ return Ok(());
61
+ }
62
+
63
+ let Some(admission_head) = active_task
64
+ .get("admission")
65
+ .and_then(|admission| admission.get("gitHead"))
66
+ .and_then(Value::as_str)
67
+ .filter(|head| !head.trim().is_empty())
68
+ else {
69
+ return Ok(());
70
+ };
71
+
72
+ let Some(current_head) = read_git_head(root)? else {
73
+ return Ok(());
74
+ };
75
+
76
+ if current_head != admission_head {
77
+ errors.push(format!("Task git HEAD changed after admission from {admission_head} to {current_head}. Reconcile external git work before continuing. Human options: mark_task_complete_from_git, reopen_task_revision, recover_current_diff, cancel_task_state."));
78
+ }
79
+
80
+ Ok(())
81
+ }
82
+
83
+ pub(super) fn read_verification_check_ids(
84
+ root: &Path,
85
+ errors: &mut Vec<String>,
86
+ ) -> Result<HashSet<String>, NaomeError> {
87
+ let mut check_ids = HashSet::new();
88
+ let Some(verification) = read_json(root, ".naome/verification.json", errors)? else {
89
+ return Ok(check_ids);
90
+ };
91
+
92
+ if let Some(checks) = verification.get("checks").and_then(Value::as_array) {
93
+ for check in checks {
94
+ if let Some(id) = check.get("id").and_then(Value::as_str) {
95
+ if !id.trim().is_empty() {
96
+ check_ids.insert(id.to_string());
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ Ok(check_ids)
103
+ }
104
+
105
+ pub(super) fn validate_required_check_ids(
106
+ active_task: &Value,
107
+ check_ids: &HashSet<String>,
108
+ errors: &mut Vec<String>,
109
+ ) {
110
+ let Some(required_check_ids) = active_task
111
+ .get("requiredCheckIds")
112
+ .and_then(Value::as_array)
113
+ else {
114
+ return;
115
+ };
116
+
117
+ for check_id in required_check_ids {
118
+ if let Some(check_id) = check_id.as_str().filter(|value| !value.trim().is_empty()) {
119
+ if !check_ids.contains(check_id) {
120
+ errors.push(format!(
121
+ "activeTask.requiredCheckIds unknown check id: {check_id}"
122
+ ));
123
+ }
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,87 @@
1
+ use crate::harness_health::HarnessHealthOptions;
2
+
3
+ pub(super) const CONTROL_STATE_PATH: &str = ".naome/task-state.json";
4
+ pub(super) const ALLOWED_STATUS: &[&str] = &[
5
+ "idle",
6
+ "planning",
7
+ "implementing",
8
+ "revising",
9
+ "verifying",
10
+ "needs_human_review",
11
+ "blocked",
12
+ "complete",
13
+ ];
14
+ pub(super) const BLOCKING_STATUS: &[&str] = &[
15
+ "planning",
16
+ "implementing",
17
+ "revising",
18
+ "verifying",
19
+ "needs_human_review",
20
+ "blocked",
21
+ ];
22
+ pub(super) const ALLOWED_EVIDENCE_STATUS: &[&str] = &["added", "modified", "deleted", "renamed"];
23
+
24
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
25
+ pub enum TaskStateMode {
26
+ State,
27
+ Admission,
28
+ Progress,
29
+ CommitGate,
30
+ PushGate,
31
+ }
32
+
33
+ #[derive(Debug, Clone)]
34
+ pub struct TaskStateOptions {
35
+ pub mode: TaskStateMode,
36
+ pub harness_health: Option<HarnessHealthOptions>,
37
+ }
38
+
39
+ impl Default for TaskStateOptions {
40
+ fn default() -> Self {
41
+ Self {
42
+ mode: TaskStateMode::State,
43
+ harness_health: None,
44
+ }
45
+ }
46
+ }
47
+
48
+ #[derive(Debug, Clone, PartialEq, Eq)]
49
+ pub struct TaskStateReport {
50
+ pub errors: Vec<String>,
51
+ pub notices: Vec<String>,
52
+ }
53
+
54
+ #[derive(Debug, Clone)]
55
+ pub(super) struct ChangedEntry {
56
+ pub(super) path: String,
57
+ pub(super) status: String,
58
+ }
59
+
60
+ #[derive(Debug, Clone, PartialEq, Eq)]
61
+ pub struct CompletedTaskHarnessRefreshDiff {
62
+ pub harness_paths: Vec<String>,
63
+ pub task_paths: Vec<String>,
64
+ }
65
+
66
+ #[derive(Debug, Clone, PartialEq, Eq)]
67
+ pub struct HarnessRefreshWithUnrelatedDiff {
68
+ pub harness_paths: Vec<String>,
69
+ pub unrelated_paths: Vec<String>,
70
+ }
71
+
72
+ #[derive(Debug, Clone, PartialEq, Eq)]
73
+ pub struct HarnessRefreshDiff {
74
+ pub harness_paths: Vec<String>,
75
+ pub unrelated_paths: Vec<String>,
76
+ }
77
+
78
+ #[derive(Debug, Clone, PartialEq, Eq)]
79
+ pub struct CompletedTaskCommitDiff {
80
+ pub task_paths: Vec<String>,
81
+ pub unrelated_paths: Vec<String>,
82
+ }
83
+
84
+ pub(super) struct TaskDiff {
85
+ pub(super) diff_paths: Vec<String>,
86
+ pub(super) outside_paths: Vec<String>,
87
+ }
@@ -0,0 +1,137 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ pub(super) fn matches_any_pattern(path: &str, patterns: &[String]) -> bool {
9
+ crate::paths::matches_any(path, patterns)
10
+ }
11
+ pub(super) fn read_json(
12
+ root: &Path,
13
+ relative_path: &str,
14
+ errors: &mut Vec<String>,
15
+ ) -> Result<Option<Value>, NaomeError> {
16
+ let json_path = root.join(relative_path);
17
+ if !json_path.is_file() {
18
+ errors.push(format!("{relative_path} is missing."));
19
+ return Ok(None);
20
+ }
21
+
22
+ let content = fs::read_to_string(json_path)?;
23
+ match serde_json::from_str::<Value>(&content) {
24
+ Ok(parsed) => Ok(Some(parsed)),
25
+ Err(error) => {
26
+ errors.push(format!("{relative_path} is not valid JSON: {error}"));
27
+ Ok(None)
28
+ }
29
+ }
30
+ }
31
+
32
+ pub(super) fn require_string(value: Option<&Value>, field_name: &str, errors: &mut Vec<String>) {
33
+ if !value
34
+ .and_then(Value::as_str)
35
+ .is_some_and(is_non_empty_string)
36
+ {
37
+ errors.push(format!("{field_name} must be a non-empty string."));
38
+ }
39
+ }
40
+
41
+ pub(super) fn require_string_array(
42
+ value: Option<&Value>,
43
+ field_name: &str,
44
+ errors: &mut Vec<String>,
45
+ ) {
46
+ let Some(values) = value.and_then(Value::as_array) else {
47
+ errors.push(format!("{field_name} must be a non-empty string array."));
48
+ return;
49
+ };
50
+
51
+ if values.is_empty()
52
+ || values
53
+ .iter()
54
+ .any(|entry| !entry.as_str().is_some_and(is_non_empty_string))
55
+ {
56
+ errors.push(format!("{field_name} must be a non-empty string array."));
57
+ }
58
+ }
59
+
60
+ pub(super) fn require_string_array_allow_empty(
61
+ value: Option<&Value>,
62
+ field_name: &str,
63
+ errors: &mut Vec<String>,
64
+ ) {
65
+ let Some(values) = value.and_then(Value::as_array) else {
66
+ errors.push(format!("{field_name} must be a string array."));
67
+ return;
68
+ };
69
+
70
+ if values
71
+ .iter()
72
+ .any(|entry| !entry.as_str().is_some_and(is_non_empty_string))
73
+ {
74
+ errors.push(format!("{field_name} must be a string array."));
75
+ }
76
+ }
77
+
78
+ pub(super) fn string_array(value: Option<&Value>) -> Option<Vec<String>> {
79
+ value.and_then(Value::as_array).and_then(|values| {
80
+ values
81
+ .iter()
82
+ .map(|entry| {
83
+ entry
84
+ .as_str()
85
+ .filter(|value| is_non_empty_string(value))
86
+ .map(ToString::to_string)
87
+ })
88
+ .collect()
89
+ })
90
+ }
91
+
92
+ pub(super) fn joined_strings(value: Option<&Value>, fallback: &str) -> String {
93
+ string_array(value)
94
+ .filter(|values| !values.is_empty())
95
+ .map(|values| values.join(", "))
96
+ .unwrap_or_else(|| fallback.to_string())
97
+ }
98
+
99
+ pub(super) fn is_non_empty_string(value: &str) -> bool {
100
+ !value.trim().is_empty()
101
+ }
102
+
103
+ pub(super) fn is_id(value: &str) -> bool {
104
+ let mut chars = value.chars();
105
+ let Some(first) = chars.next() else {
106
+ return false;
107
+ };
108
+ (first.is_ascii_lowercase() || first.is_ascii_digit())
109
+ && value
110
+ .chars()
111
+ .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
112
+ }
113
+
114
+ pub(super) fn is_iso_datetime(value: &str) -> bool {
115
+ let bytes = value.as_bytes();
116
+ bytes.len() == 24
117
+ && bytes[4] == b'-'
118
+ && bytes[7] == b'-'
119
+ && bytes[10] == b'T'
120
+ && bytes[13] == b':'
121
+ && bytes[16] == b':'
122
+ && bytes[19] == b'.'
123
+ && bytes[23] == b'Z'
124
+ && bytes
125
+ .iter()
126
+ .enumerate()
127
+ .filter(|(index, _)| ![4, 7, 10, 13, 16, 19, 23].contains(index))
128
+ .all(|(_, byte)| byte.is_ascii_digit())
129
+ }
130
+
131
+ pub(super) fn normalize_path(value: impl AsRef<str>) -> String {
132
+ value
133
+ .as_ref()
134
+ .replace('\\', "/")
135
+ .trim_start_matches("./")
136
+ .to_string()
137
+ }