@lamentis/naome 1.2.0 → 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 (113) hide show
  1. package/Cargo.lock +2 -2
  2. package/bin/naome-node.js +2 -1579
  3. package/bin/naome.js +19 -5
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/dispatcher.rs +2 -1
  6. package/crates/naome-cli/src/main.rs +3 -0
  7. package/crates/naome-cli/src/quality_commands.rs +90 -2
  8. package/crates/naome-core/Cargo.toml +1 -1
  9. package/crates/naome-core/src/decision/checks.rs +64 -0
  10. package/crates/naome-core/src/decision/idle.rs +67 -0
  11. package/crates/naome-core/src/decision/json.rs +36 -0
  12. package/crates/naome-core/src/decision/states.rs +165 -0
  13. package/crates/naome-core/src/decision.rs +131 -353
  14. package/crates/naome-core/src/install_plan.rs +2 -0
  15. package/crates/naome-core/src/lib.rs +5 -3
  16. package/crates/naome-core/src/paths.rs +3 -1
  17. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  18. package/crates/naome-core/src/quality/adapters.rs +20 -67
  19. package/crates/naome-core/src/quality/cleanup.rs +13 -1
  20. package/crates/naome-core/src/quality/config.rs +8 -15
  21. package/crates/naome-core/src/quality/config_support.rs +24 -0
  22. package/crates/naome-core/src/quality/mod.rs +18 -0
  23. package/crates/naome-core/src/quality/scanner.rs +20 -8
  24. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  25. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  26. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  27. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  28. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  29. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  30. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  31. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  32. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  33. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  34. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  35. package/crates/naome-core/src/quality/types.rs +3 -0
  36. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  37. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  38. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  39. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  40. package/crates/naome-core/src/route/context.rs +180 -0
  41. package/crates/naome-core/src/route/execution.rs +96 -0
  42. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  43. package/crates/naome-core/src/route/execution_support.rs +57 -0
  44. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  45. package/crates/naome-core/src/route/git_ops.rs +72 -0
  46. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  47. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  48. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  49. package/crates/naome-core/src/route/worktree.rs +75 -0
  50. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  51. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  52. package/crates/naome-core/src/route.rs +44 -1217
  53. package/crates/naome-core/src/verification.rs +1 -0
  54. package/crates/naome-core/tests/decision.rs +24 -118
  55. package/crates/naome-core/tests/harness_health.rs +2 -0
  56. package/crates/naome-core/tests/quality.rs +12 -118
  57. package/crates/naome-core/tests/quality_structure.rs +116 -0
  58. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  59. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  60. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  61. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  62. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  63. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  64. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  65. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  66. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  67. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  68. package/crates/naome-core/tests/route.rs +1 -1376
  69. package/crates/naome-core/tests/route_baseline.rs +86 -0
  70. package/crates/naome-core/tests/route_completion.rs +141 -0
  71. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  72. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  73. package/crates/naome-core/tests/route_worktree.rs +54 -0
  74. package/crates/naome-core/tests/task_state.rs +60 -432
  75. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  76. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  77. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  78. package/crates/naome-core/tests/verification.rs +4 -45
  79. package/crates/naome-core/tests/verification_contract.rs +22 -78
  80. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  81. package/installer/agents.js +90 -0
  82. package/installer/context.js +67 -0
  83. package/installer/filesystem.js +166 -0
  84. package/installer/flows.js +84 -0
  85. package/installer/git-boundary.js +170 -0
  86. package/installer/git-hook-content.js +36 -0
  87. package/installer/git-hooks.js +134 -0
  88. package/installer/git-local.js +2 -0
  89. package/installer/git-shared.js +35 -0
  90. package/installer/harness-file-ops.js +140 -0
  91. package/installer/harness-files.js +56 -0
  92. package/installer/harness-verification.js +123 -0
  93. package/installer/install-plan.js +66 -0
  94. package/installer/main.js +25 -0
  95. package/installer/manifest-state.js +167 -0
  96. package/installer/native-build.js +24 -0
  97. package/installer/native-format.js +6 -0
  98. package/installer/native.js +162 -0
  99. package/installer/output.js +131 -0
  100. package/installer/version.js +32 -0
  101. package/native/darwin-arm64/naome +0 -0
  102. package/native/linux-x64/naome +0 -0
  103. package/package.json +2 -1
  104. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  105. package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
  106. package/templates/naome-root/.naome/bin/naome.js +25 -21
  107. package/templates/naome-root/.naome/manifest.json +4 -2
  108. package/templates/naome-root/.naome/repository-structure.json +90 -0
  109. package/templates/naome-root/.naome/verification.json +1 -0
  110. package/templates/naome-root/docs/naome/index.md +4 -3
  111. package/templates/naome-root/docs/naome/repository-quality.md +3 -0
  112. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  113. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -1,14 +1,18 @@
1
- use std::ffi::OsString;
2
- use std::fs;
3
1
  use std::path::Path;
4
- use std::process::Command;
5
-
6
- use serde_json::Value;
7
2
 
8
3
  use crate::git;
9
4
  use crate::models::{CheckDecision, Decision, NaomeError, TaskDecision};
10
- use crate::paths;
11
- use crate::task_state::harness_refresh_diff;
5
+
6
+ mod checks;
7
+ mod idle;
8
+ mod json;
9
+ mod states;
10
+
11
+ use self::checks::{extract_known_actions, run_node_check};
12
+ use self::json::{json_bool, json_string, read_json};
13
+ use self::states::{
14
+ active_task_decision, classify_idle_diff, completed_task_decision, task_decision,
15
+ };
12
16
 
13
17
  #[derive(Debug, Clone, Copy)]
14
18
  pub struct EvaluationOptions {
@@ -31,175 +35,29 @@ impl EvaluationOptions {
31
35
 
32
36
  pub fn evaluate_decision(root: &Path, options: EvaluationOptions) -> Result<Decision, NaomeError> {
33
37
  let changed_paths = git::changed_paths(root)?;
34
-
35
- let harness_health = if options.run_external_checks {
36
- Some(run_node_check(
37
- root,
38
- ".naome/bin/check-harness-health.js",
39
- &[],
40
- )?)
41
- } else {
42
- None
43
- };
44
-
45
- if let Some(check) = &harness_health {
46
- if !check.ok {
47
- let mut decision = Decision::new(
48
- "harness_unhealthy",
49
- true,
50
- "NAOME harness health failed.",
51
- vec!["repair_harness", "review_harness_health"],
52
- "Repair the machine-owned harness before feature work.",
53
- );
54
- decision.changed_paths = changed_paths;
55
- decision.harness_health = harness_health;
56
- decision.required_context = vec![
57
- ".naomeignore".to_string(),
58
- "docs/naome/index.md".to_string(),
59
- ".naome/manifest.json".to_string(),
60
- ];
61
- return Ok(decision);
62
- }
63
- }
64
-
65
- let init_state = read_json(root, ".naome/init-state.json")?;
66
- let upgrade_state = read_json(root, ".naome/upgrade-state.json")?;
67
- let task_state = read_json(root, ".naome/task-state.json")?;
68
-
69
- if json_string(&upgrade_state, "status").as_deref() == Some("needs_agent_upgrade") {
70
- let mut decision = Decision::new(
71
- "upgrade_required",
72
- true,
73
- "NAOME upgrade requires agent follow-up.",
74
- vec!["run_upgrade_protocol"],
75
- "Run the NAOME upgrade protocol before feature work.",
76
- );
77
- decision.changed_paths = changed_paths;
78
- decision.harness_health = harness_health;
79
- decision.required_context = vec![
80
- "docs/naome/upgrade.md".to_string(),
81
- ".naome/upgrade-state.json".to_string(),
82
- ];
38
+ let harness_health = run_harness_health(root, options)?;
39
+ if let Some(decision) = harness_health_decision(&changed_paths, &harness_health) {
83
40
  return Ok(decision);
84
41
  }
85
42
 
86
- if json_bool(&init_state, "initialized") != Some(true) {
87
- let mut decision = Decision::new(
88
- "first_run_required",
89
- true,
90
- "NAOME first-run intake has not been completed.",
91
- vec!["run_first_run_protocol"],
92
- "Run the NAOME first-run protocol before feature work.",
93
- );
94
- decision.changed_paths = changed_paths;
95
- decision.harness_health = harness_health;
96
- decision.required_context = vec![
97
- "docs/naome/first-run.md".to_string(),
98
- ".naome/init-state.json".to_string(),
99
- ];
43
+ let task_state = read_json(root, ".naome/task-state.json")?;
44
+ if let Some(decision) = setup_block_decision(root, &changed_paths, &harness_health)? {
100
45
  return Ok(decision);
101
46
  }
102
47
 
103
48
  let task_status = json_string(&task_state, "status").unwrap_or_else(|| "invalid".to_string());
104
49
  let task = task_decision(&task_state, &task_status);
105
-
106
50
  if task_status == "complete" {
107
- let mut decision = if changed_paths.is_empty() {
108
- Decision::new(
109
- "ready_for_task",
110
- false,
111
- "The last completed NAOME task has no open diff.",
112
- vec!["create_task"],
113
- "Task admission is clear; create the next task before feature work.",
114
- )
115
- } else if has_completed_task_owned_paths(task.as_ref(), &changed_paths) {
116
- Decision::new(
117
- "completed_task_unbaselined",
118
- true,
119
- "A completed NAOME task is verified and waiting for the next routing decision.",
120
- vec![
121
- "commit_task_baseline",
122
- "review_task_diff",
123
- "request_task_changes",
124
- "cancel_task_changes",
125
- ],
126
- "Run NAOME intent for the next natural-language request; deterministic policy can baseline a valid completed task automatically.",
127
- )
128
- } else if harness_refresh_diff(root)?.is_some_and(|diff| diff.unrelated_paths.is_empty()) {
129
- Decision::new(
130
- "harness_repair_unbaselined",
131
- true,
132
- "Machine-owned NAOME harness refresh files changed outside an active task.",
133
- vec![
134
- "commit_upgrade_baseline",
135
- "review_diff_first",
136
- "cancel_upgrade_baseline",
137
- ],
138
- "Run NAOME intent for the next natural-language request; deterministic policy can baseline a pure harness refresh automatically.",
139
- )
140
- } else {
141
- Decision::new(
142
- "dirty_unowned_diff",
143
- true,
144
- "The completed NAOME task has been baselined, but unrelated user changes remain.",
145
- vec!["review_unowned_diff"],
146
- "Review the unrelated diff, or route a new task so NAOME can isolate task work without touching it.",
147
- )
148
- };
149
- decision.changed_paths = changed_paths;
51
+ let mut decision = completed_task_decision(root, changed_paths, task.as_ref())?;
150
52
  decision.harness_health = harness_health;
151
53
  decision.task = task;
152
- decision.required_context = vec![
153
- "docs/naome/execution.md".to_string(),
154
- ".naome/task-state.json".to_string(),
155
- "docs/naome/agent-workflow.md".to_string(),
156
- "docs/naome/testing.md".to_string(),
157
- ];
158
54
  return Ok(decision);
159
55
  }
160
56
 
161
57
  if task_status != "idle" {
162
- let allowed_paths = task
163
- .as_ref()
164
- .map(|task| task.allowed_paths.clone())
165
- .unwrap_or_default();
166
- let out_of_scope: Vec<String> = changed_paths
167
- .iter()
168
- .filter(|path| {
169
- !is_control_state_path(path) && !paths::matches_any(path, &allowed_paths)
170
- })
171
- .cloned()
172
- .collect();
173
-
174
- let mut decision = if out_of_scope.is_empty() {
175
- Decision::new(
176
- "active_task_in_progress",
177
- false,
178
- "A NAOME task is active and the current diff is inside its declared scope.",
179
- vec!["continue_task", "request_task_changes", "complete_task"],
180
- "Continue the active task and keep proof current.",
181
- )
182
- } else {
183
- Decision::new(
184
- "active_task_blocked",
185
- true,
186
- "A NAOME task is active, but the current diff includes paths outside its declared scope.",
187
- vec!["revise_task_scope", "revert_out_of_scope_diff", "request_human_review"],
188
- "Resolve out-of-scope changes before completing this task.",
189
- )
190
- };
191
- decision.changed_paths = if out_of_scope.is_empty() {
192
- changed_paths
193
- } else {
194
- out_of_scope
195
- };
58
+ let mut decision = active_task_decision(changed_paths, task.as_ref());
196
59
  decision.harness_health = harness_health;
197
60
  decision.task = task;
198
- decision.required_context = vec![
199
- "docs/naome/execution.md".to_string(),
200
- ".naome/task-state.json".to_string(),
201
- "docs/naome/testing.md".to_string(),
202
- ];
203
61
  return Ok(decision);
204
62
  }
205
63
 
@@ -210,15 +68,91 @@ pub fn evaluate_decision(root: &Path, options: EvaluationOptions) -> Result<Deci
210
68
  return Ok(decision);
211
69
  }
212
70
 
213
- let task_admission = if options.run_external_checks {
214
- Some(run_node_check(
71
+ ready_or_admission_blocked_decision(root, options, harness_health, task)
72
+ }
73
+
74
+ fn run_harness_health(
75
+ root: &Path,
76
+ options: EvaluationOptions,
77
+ ) -> Result<Option<CheckDecision>, NaomeError> {
78
+ if options.run_external_checks {
79
+ Ok(Some(run_node_check(
215
80
  root,
216
- ".naome/bin/check-task-state.js",
217
- &["--admission"],
218
- )?)
81
+ ".naome/bin/check-harness-health.js",
82
+ &[],
83
+ )?))
219
84
  } else {
220
- None
221
- };
85
+ Ok(None)
86
+ }
87
+ }
88
+
89
+ fn harness_health_decision(
90
+ changed_paths: &[String],
91
+ harness_health: &Option<CheckDecision>,
92
+ ) -> Option<Decision> {
93
+ let check = harness_health.as_ref()?;
94
+ if check.ok {
95
+ return None;
96
+ }
97
+ let mut decision = Decision::new(
98
+ "harness_unhealthy",
99
+ true,
100
+ "NAOME harness health failed.",
101
+ vec!["repair_harness", "review_harness_health"],
102
+ "Repair the machine-owned harness before feature work.",
103
+ );
104
+ decision.changed_paths = changed_paths.to_vec();
105
+ decision.harness_health = harness_health.clone();
106
+ decision.required_context = vec![
107
+ ".naomeignore".to_string(),
108
+ "docs/naome/index.md".to_string(),
109
+ ".naome/manifest.json".to_string(),
110
+ ];
111
+ Some(decision)
112
+ }
113
+
114
+ fn setup_block_decision(
115
+ root: &Path,
116
+ changed_paths: &[String],
117
+ harness_health: &Option<CheckDecision>,
118
+ ) -> Result<Option<Decision>, NaomeError> {
119
+ let init_state = read_json(root, ".naome/init-state.json")?;
120
+ let upgrade_state = read_json(root, ".naome/upgrade-state.json")?;
121
+
122
+ if json_string(&upgrade_state, "status").as_deref() == Some("needs_agent_upgrade") {
123
+ return Ok(Some(static_block_decision(
124
+ "upgrade_required",
125
+ "NAOME upgrade requires agent follow-up.",
126
+ vec!["run_upgrade_protocol"],
127
+ "Run the NAOME upgrade protocol before feature work.",
128
+ &["docs/naome/upgrade.md", ".naome/upgrade-state.json"],
129
+ changed_paths,
130
+ harness_health,
131
+ )));
132
+ }
133
+
134
+ if json_bool(&init_state, "initialized") != Some(true) {
135
+ return Ok(Some(static_block_decision(
136
+ "first_run_required",
137
+ "NAOME first-run intake has not been completed.",
138
+ vec!["run_first_run_protocol"],
139
+ "Run the NAOME first-run protocol before feature work.",
140
+ &["docs/naome/first-run.md", ".naome/init-state.json"],
141
+ changed_paths,
142
+ harness_health,
143
+ )));
144
+ }
145
+
146
+ Ok(None)
147
+ }
148
+
149
+ fn ready_or_admission_blocked_decision(
150
+ root: &Path,
151
+ options: EvaluationOptions,
152
+ harness_health: Option<CheckDecision>,
153
+ task: Option<TaskDecision>,
154
+ ) -> Result<Decision, NaomeError> {
155
+ let task_admission = run_task_admission(root, options)?;
222
156
 
223
157
  if let Some(check) = &task_admission {
224
158
  if !check.ok {
@@ -259,6 +193,37 @@ pub fn evaluate_decision(root: &Path, options: EvaluationOptions) -> Result<Deci
259
193
  Ok(decision)
260
194
  }
261
195
 
196
+ fn run_task_admission(
197
+ root: &Path,
198
+ options: EvaluationOptions,
199
+ ) -> Result<Option<CheckDecision>, NaomeError> {
200
+ if options.run_external_checks {
201
+ Ok(Some(run_node_check(
202
+ root,
203
+ ".naome/bin/check-task-state.js",
204
+ &["--admission"],
205
+ )?))
206
+ } else {
207
+ Ok(None)
208
+ }
209
+ }
210
+
211
+ fn static_block_decision(
212
+ state: &str,
213
+ summary: &str,
214
+ actions: Vec<&str>,
215
+ next_action: &str,
216
+ required_context: &[&str],
217
+ changed_paths: &[String],
218
+ harness_health: &Option<CheckDecision>,
219
+ ) -> Decision {
220
+ let mut decision = Decision::new(state, true, summary, actions, next_action);
221
+ decision.changed_paths = changed_paths.to_vec();
222
+ decision.harness_health = harness_health.clone();
223
+ decision.required_context = required_context.iter().map(ToString::to_string).collect();
224
+ decision
225
+ }
226
+
262
227
  pub fn format_decision(decision: &Decision, mode: &str) -> String {
263
228
  let title = if mode == "next" {
264
229
  "NAOME next"
@@ -314,190 +279,3 @@ fn is_intent_routed_baseline_state(state: &str) -> bool {
314
279
  | "harness_repair_unbaselined"
315
280
  )
316
281
  }
317
-
318
- fn classify_idle_diff(root: &Path, changed_paths: Vec<String>) -> Result<Decision, NaomeError> {
319
- let manifest = read_json(root, ".naome/manifest.json").unwrap_or(Value::Null);
320
- let machine_owned = string_array_at(&manifest, "machineOwned");
321
- let project_owned = string_array_at(&manifest, "projectOwned");
322
-
323
- let mut known_harness_paths = machine_owned.clone();
324
- known_harness_paths.extend(project_owned);
325
-
326
- let all_machine_owned = !machine_owned.is_empty()
327
- && changed_paths
328
- .iter()
329
- .all(|path| paths::matches_any(path, &machine_owned));
330
- let all_harness_owned = !known_harness_paths.is_empty()
331
- && changed_paths
332
- .iter()
333
- .all(|path| paths::matches_any(path, &known_harness_paths));
334
-
335
- let mut decision = if harness_refresh_diff(root)?
336
- .is_some_and(|diff| diff.unrelated_paths.is_empty())
337
- {
338
- Decision::new(
339
- "harness_repair_unbaselined",
340
- true,
341
- "Machine-owned NAOME harness refresh files changed outside an active task.",
342
- vec![
343
- "commit_upgrade_baseline",
344
- "review_diff_first",
345
- "cancel_upgrade_baseline",
346
- ],
347
- "Run NAOME intent for the next natural-language request; deterministic policy can baseline a pure harness refresh automatically.",
348
- )
349
- } else if all_machine_owned {
350
- Decision::new(
351
- "harness_repair_unbaselined",
352
- true,
353
- "Machine-owned NAOME harness files changed outside an active task.",
354
- vec![
355
- "commit_upgrade_baseline",
356
- "review_diff_first",
357
- "cancel_upgrade_baseline",
358
- ],
359
- "Review and baseline the harness repair or cancel it before feature work.",
360
- )
361
- } else if all_harness_owned {
362
- Decision::new(
363
- "install_or_upgrade_unbaselined",
364
- true,
365
- "NAOME setup or upgrade files changed outside an active task.",
366
- vec![
367
- "commit_upgrade_baseline",
368
- "review_diff_first",
369
- "cancel_upgrade_baseline",
370
- ],
371
- "Resolve the setup or upgrade diff before feature work.",
372
- )
373
- } else {
374
- Decision::new(
375
- "dirty_unowned_diff",
376
- true,
377
- "The repository has changes not owned by an active NAOME task.",
378
- vec!["review_unowned_diff"],
379
- "Review the unowned diff, or route a new task so NAOME can isolate task work without touching it.",
380
- )
381
- };
382
-
383
- decision.changed_paths = changed_paths;
384
- decision.required_context = vec![
385
- "docs/naome/execution.md".to_string(),
386
- ".naome/task-state.json".to_string(),
387
- ];
388
- Ok(decision)
389
- }
390
-
391
- fn task_decision(task_state: &Value, status: &str) -> Option<TaskDecision> {
392
- let active_task = task_state.get("activeTask")?;
393
- if active_task.is_null() {
394
- return None;
395
- }
396
-
397
- Some(TaskDecision {
398
- id: json_string(active_task, "id"),
399
- status: status.to_string(),
400
- request: json_string(active_task, "request"),
401
- allowed_paths: string_array_at(active_task, "allowedPaths"),
402
- required_check_ids: string_array_at(active_task, "requiredCheckIds"),
403
- })
404
- }
405
-
406
- fn is_control_state_path(path: &str) -> bool {
407
- path == ".naome/task-state.json"
408
- }
409
-
410
- fn has_completed_task_owned_paths(task: Option<&TaskDecision>, changed_paths: &[String]) -> bool {
411
- let Some(task) = task else {
412
- return false;
413
- };
414
-
415
- changed_paths
416
- .iter()
417
- .any(|path| is_control_state_path(path) || paths::matches_any(path, &task.allowed_paths))
418
- }
419
-
420
- fn run_node_check(root: &Path, script: &str, args: &[&str]) -> Result<CheckDecision, NaomeError> {
421
- let mut command_args = vec![script.to_string()];
422
- command_args.extend(args.iter().map(ToString::to_string));
423
- let node_bin = std::env::var_os("NAOME_NODE_BIN").unwrap_or_else(|| OsString::from("node"));
424
- let output = Command::new(&node_bin)
425
- .args(&command_args)
426
- .current_dir(root)
427
- .output()?;
428
- let mut combined = String::new();
429
- combined.push_str(&String::from_utf8_lossy(&output.stdout));
430
- combined.push_str(&String::from_utf8_lossy(&output.stderr));
431
-
432
- Ok(CheckDecision {
433
- command: format!(
434
- "{} {}{}",
435
- node_bin.to_string_lossy(),
436
- script,
437
- if args.is_empty() {
438
- String::new()
439
- } else {
440
- format!(" {}", args.join(" "))
441
- }
442
- ),
443
- exit_code: output.status.code(),
444
- ok: output.status.success(),
445
- output: combined.trim().to_string(),
446
- })
447
- }
448
-
449
- fn read_json(root: &Path, relative_path: &str) -> Result<Value, NaomeError> {
450
- let content = fs::read_to_string(root.join(relative_path))?;
451
- Ok(serde_json::from_str(&content)?)
452
- }
453
-
454
- fn json_bool(value: &Value, key: &str) -> Option<bool> {
455
- value.get(key).and_then(Value::as_bool)
456
- }
457
-
458
- fn json_string(value: &Value, key: &str) -> Option<String> {
459
- value
460
- .get(key)
461
- .and_then(Value::as_str)
462
- .map(ToString::to_string)
463
- }
464
-
465
- fn string_array_at(value: &Value, key: &str) -> Vec<String> {
466
- value
467
- .get(key)
468
- .and_then(Value::as_array)
469
- .map(|values| {
470
- values
471
- .iter()
472
- .filter_map(Value::as_str)
473
- .map(ToString::to_string)
474
- .collect()
475
- })
476
- .unwrap_or_default()
477
- }
478
-
479
- fn extract_known_actions(output: &str) -> Vec<&'static str> {
480
- const KNOWN: [&str; 11] = [
481
- "commit_task_baseline",
482
- "review_task_diff",
483
- "request_task_changes",
484
- "cancel_task_changes",
485
- "commit_upgrade_baseline",
486
- "review_diff_first",
487
- "cancel_upgrade_baseline",
488
- "run_first_run_protocol",
489
- "run_upgrade_protocol",
490
- "review_unowned_diff",
491
- "create_task",
492
- ];
493
-
494
- let actions: Vec<&'static str> = KNOWN
495
- .into_iter()
496
- .filter(|action| output.contains(action))
497
- .collect();
498
- if actions.is_empty() {
499
- vec!["review_task_admission"]
500
- } else {
501
- actions
502
- }
503
- }
@@ -22,11 +22,13 @@ pub const PROJECT_OWNED_PATHS: &[&str] = &[
22
22
  ".naome/upgrade-state.json",
23
23
  ".naome/verification.json",
24
24
  ".naome/repository-quality.json",
25
+ ".naome/repository-structure.json",
25
26
  ".naome/repository-quality-baseline.json",
26
27
  "docs/naome/architecture.md",
27
28
  "docs/naome/decisions.md",
28
29
  "docs/naome/repo-profile.md",
29
30
  "docs/naome/repository-quality.md",
31
+ "docs/naome/repository-structure.md",
30
32
  "docs/naome/security.md",
31
33
  "docs/naome/testing.md",
32
34
  ];
@@ -16,13 +16,15 @@ mod workflow;
16
16
  pub use decision::{evaluate_decision, format_decision, EvaluationOptions};
17
17
  pub use harness_health::{validate_harness_health, HarnessHealthOptions};
18
18
  pub use install_plan::{install_plan, InstallPlan};
19
+ pub use install_plan::{MACHINE_OWNED_PATHS, PROJECT_OWNED_PATHS};
19
20
  pub use intent::{evaluate_intent, format_intent, IntentDecision, PromptEvidence};
20
21
  pub use journal::{append_task_journal, TaskJournalEntry};
21
22
  pub use models::{Decision, NaomeError};
22
23
  pub use quality::{
23
- check_repository_quality, init_repository_quality, plan_quality_cleanup, route_quality_cleanup,
24
- QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitResult, QualityMode,
25
- QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
24
+ check_repository_quality, explain_repository_structure, init_repository_quality,
25
+ plan_quality_cleanup, route_quality_cleanup, QualityCleanupPlan, QualityCleanupRoute,
26
+ QualityCleanupTask, QualityInitResult, QualityMode, QualityReport, QualitySummary,
27
+ QualityViolation, RepositoryQualityConfig, RepositoryStructureConfig, StructurePathExplanation,
26
28
  };
27
29
  pub use route::{evaluate_route, explain_route, ExplainDecision, RouteDecision, RouteOptions};
28
30
  pub use task_state::{
@@ -13,7 +13,9 @@ fn matches_pattern(path: &str, pattern: &str) -> bool {
13
13
  }
14
14
 
15
15
  if let Some(prefix) = normalized_pattern.strip_suffix("/**") {
16
- return normalized_path == prefix || normalized_path.starts_with(&format!("{prefix}/"));
16
+ if !prefix.contains('*') {
17
+ return normalized_path == prefix || normalized_path.starts_with(&format!("{prefix}/"));
18
+ }
17
19
  }
18
20
 
19
21
  if !normalized_pattern.contains('*') {
@@ -0,0 +1,89 @@
1
+ use std::collections::HashSet;
2
+
3
+ use crate::models::NaomeError;
4
+
5
+ pub(crate) struct RepoSignals<'a> {
6
+ paths: &'a [String],
7
+ }
8
+
9
+ impl<'a> RepoSignals<'a> {
10
+ pub(crate) fn new(paths: &'a [String]) -> Self {
11
+ Self { paths }
12
+ }
13
+
14
+ pub(crate) fn has_manifest(&self, expected: &str) -> bool {
15
+ let nested_suffix = format!("/{expected}");
16
+ self.paths
17
+ .iter()
18
+ .any(|path| path == expected || path.ends_with(&nested_suffix))
19
+ }
20
+
21
+ pub(crate) fn has_extension(&self, extensions: &[&str]) -> bool {
22
+ self.paths
23
+ .iter()
24
+ .any(|path| extensions.iter().any(|extension| path.ends_with(extension)))
25
+ }
26
+ }
27
+
28
+ pub(crate) trait AdapterDescriptor {
29
+ fn id(&self) -> &'static str;
30
+ fn detects(&self, signals: &RepoSignals<'_>) -> bool;
31
+ }
32
+
33
+ pub(crate) fn detected_ids<T: AdapterDescriptor>(paths: &[String], registry: &[T]) -> Vec<String> {
34
+ let signals = RepoSignals::new(paths);
35
+ registry
36
+ .iter()
37
+ .filter(|adapter| adapter.detects(&signals))
38
+ .map(|adapter| adapter.id().to_string())
39
+ .collect()
40
+ }
41
+
42
+ pub(crate) fn find_adapter_by_id<'a, T: AdapterDescriptor>(
43
+ registry: &'a [T],
44
+ id: &str,
45
+ config_path: &str,
46
+ ) -> Result<&'a T, NaomeError> {
47
+ registry
48
+ .iter()
49
+ .find(|adapter| adapter.id() == id)
50
+ .ok_or_else(|| {
51
+ NaomeError::new(format!(
52
+ "{config_path} enabledAdapters contains unknown adapter '{id}'."
53
+ ))
54
+ })
55
+ }
56
+
57
+ pub(crate) fn validate_ids<T: AdapterDescriptor>(
58
+ ids: &[String],
59
+ registry: &[T],
60
+ config_path: &str,
61
+ ) -> Result<(), NaomeError> {
62
+ let mut seen = HashSet::new();
63
+ for adapter_id in ids {
64
+ if !seen.insert(adapter_id) {
65
+ return Err(NaomeError::new(format!(
66
+ "{config_path} enabledAdapters contains duplicate adapter '{adapter_id}'."
67
+ )));
68
+ }
69
+ find_adapter_by_id(registry, adapter_id, config_path)?;
70
+ }
71
+ Ok(())
72
+ }
73
+
74
+ pub(crate) fn extend_unique(target: &mut Vec<String>, values: &[&str]) {
75
+ for value in values {
76
+ if !target.iter().any(|existing| existing == value) {
77
+ target.push((*value).to_string());
78
+ }
79
+ }
80
+ }
81
+
82
+ pub(crate) fn detects_rust_project(signals: &RepoSignals<'_>) -> bool {
83
+ signals.has_manifest("Cargo.toml") || signals.has_extension(&[".rs"])
84
+ }
85
+
86
+ pub(crate) fn detects_javascript_typescript_project(signals: &RepoSignals<'_>) -> bool {
87
+ signals.has_manifest("package.json")
88
+ || signals.has_extension(&[".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"])
89
+ }