@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
package/bin/naome.js CHANGED
@@ -12,7 +12,7 @@ const packageVersion = packageMetadata.version;
12
12
  const nativeBinaryName = process.platform === "win32" ? "naome.exe" : "naome";
13
13
  const args = process.argv.slice(2);
14
14
  const [command] = args;
15
- const helpCommands = "status [--json]|next [--json]|intent --prompt-file <path> [--json]|intent --prompt <text> [--json]|route --prompt-file <path> [--execute] [--json]|route --prompt <text> [--execute] [--json]|explain --prompt-file <path> [--json]|explain --prompt <text> [--json]|install|sync [--check-update]|update [--json] [--execute]|quality init [--json]|quality check --changed [--json]|quality report [--json]|cleanup plan [--json]|cleanup route --path <path> [--json]|refresh-integrity [--json]|workflow search-profile|check-search|phases|processes|mutations [--json]|commit -m \"type(scope): message\"".split("|");
15
+ const helpCommands = "status [--json]|next [--json]|intent --prompt-file <path> [--json]|intent --prompt <text> [--json]|route --prompt-file <path> [--execute] [--json]|route --prompt <text> [--execute] [--json]|explain --prompt-file <path> [--json]|explain --prompt <text> [--json]|install|sync [--check-update]|update [--json] [--execute]|quality init [--json]|quality check --changed [--json]|quality report [--json]|structure report [--json]|structure explain --path <path> [--json]|cleanup plan [--json]|cleanup route --path <path> [--json]|refresh-integrity [--json]|workflow search-profile|check-search|phases|processes|mutations [--json]|commit -m \"type(scope): message\"".split("|");
16
16
 
17
17
  if (isHelpRequest(args)) {
18
18
  printHelp();
@@ -265,13 +265,19 @@ function runNativePackageCommand(args) {
265
265
 
266
266
  function ensureRepositoryQualityInitialized(nativeBinary, qualityConfigExisted) {
267
267
  const root = process.cwd();
268
- if (qualityConfigExisted || !existsSync(join(root, ".naome"))) {
268
+ if (!existsSync(join(root, ".naome"))) {
269
269
  return;
270
270
  }
271
271
 
272
- for (const path of repositoryQualityPaths(root)) {
273
- if (existsSync(path)) {
274
- unlinkSync(path);
272
+ if (qualityConfigExisted && repositoryQualitySupportFilesExist(root)) {
273
+ return;
274
+ }
275
+
276
+ if (!qualityConfigExisted) {
277
+ for (const path of repositoryQualityPaths(root)) {
278
+ if (existsSync(path)) {
279
+ unlinkSync(path);
280
+ }
275
281
  }
276
282
  }
277
283
 
@@ -303,10 +309,18 @@ function repositoryQualityConfigPath(root) {
303
309
  function repositoryQualityPaths(root) {
304
310
  return [
305
311
  repositoryQualityConfigPath(root),
312
+ join(root, ".naome", "repository-structure.json"),
306
313
  join(root, ".naome", "repository-quality-baseline.json")
307
314
  ];
308
315
  }
309
316
 
317
+ function repositoryQualitySupportFilesExist(root) {
318
+ return [
319
+ join(root, ".naome", "repository-structure.json"),
320
+ join(root, ".naome", "repository-quality-baseline.json")
321
+ ].every((path) => existsSync(path));
322
+ }
323
+
310
324
  function resolveNativePackageBinary() {
311
325
  const candidates = [
312
326
  process.env.NAOME_NATIVE_BIN && resolve(process.cwd(), process.env.NAOME_NATIVE_BIN),
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.2.0"
3
+ version = "1.2.1"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -3,7 +3,7 @@ use std::path::Path;
3
3
  use crate::check_commands::{run_harness_health, run_task_state, run_verification_contract};
4
4
  use crate::install_bridge::run_install_bridge;
5
5
  use crate::prompt_commands::{run_explain, run_intent, run_route};
6
- use crate::quality_commands::{run_cleanup_command, run_quality_command};
6
+ use crate::quality_commands::{run_cleanup_command, run_quality_command, run_structure_command};
7
7
  use crate::simple_commands::{
8
8
  print_install_plan, run_commit_paths, run_journal_task, seed_verification,
9
9
  };
@@ -20,6 +20,7 @@ pub fn dispatch_command(
20
20
  "seed-verification" => seed_verification(root)?,
21
21
  "refresh-integrity" => run_refresh_integrity(root, args)?,
22
22
  "quality" => run_quality_command(root, args)?,
23
+ "structure" => run_structure_command(root, args)?,
23
24
  "cleanup" => run_cleanup_command(root, args)?,
24
25
  "workflow" => run_workflow_command(root, args)?,
25
26
  "check-harness-health" => run_harness_health(root, args)?,
@@ -30,6 +30,8 @@ const HELP: &str = r#"Usage:
30
30
  naome quality init [--json]
31
31
  naome quality check --changed [--json]
32
32
  naome quality report [--json]
33
+ naome structure report [--json]
34
+ naome structure explain --path <path> [--json]
33
35
  naome cleanup plan [--json]
34
36
  naome cleanup route --path <path> [--json]
35
37
  naome workflow search-profile [--json]
@@ -71,6 +73,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
71
73
  && command != "refresh-integrity"
72
74
  && command != "workflow"
73
75
  && command != "quality"
76
+ && command != "structure"
74
77
  && command != "cleanup"
75
78
  && command != "install-plan"
76
79
  && command != "install"
@@ -1,8 +1,8 @@
1
1
  use std::path::Path;
2
2
 
3
3
  use naome_core::{
4
- check_repository_quality, init_repository_quality, plan_quality_cleanup, route_quality_cleanup,
5
- QualityMode,
4
+ check_repository_quality, explain_repository_structure, init_repository_quality,
5
+ plan_quality_cleanup, route_quality_cleanup, QualityMode,
6
6
  };
7
7
 
8
8
  use crate::cli_args::option_value;
@@ -44,6 +44,23 @@ pub fn run_cleanup_command(root: &Path, args: &[String]) -> Result<(), Box<dyn s
44
44
  Ok(())
45
45
  }
46
46
 
47
+ pub fn run_structure_command(
48
+ root: &Path,
49
+ args: &[String],
50
+ ) -> Result<(), Box<dyn std::error::Error>> {
51
+ let Some(subcommand) = args.get(1).map(String::as_str) else {
52
+ return Err("naome structure requires report or explain.".into());
53
+ };
54
+ let json = args.iter().any(|arg| arg == "--json");
55
+
56
+ match subcommand {
57
+ "report" => run_structure_report(root, json)?,
58
+ "explain" => run_structure_explain(root, args, json)?,
59
+ _ => return Err(format!("unknown naome structure command: {subcommand}").into()),
60
+ }
61
+ Ok(())
62
+ }
63
+
47
64
  fn run_quality_check(
48
65
  root: &Path,
49
66
  args: &[String],
@@ -132,6 +149,77 @@ fn run_cleanup_route(
132
149
  Ok(())
133
150
  }
134
151
 
152
+ fn run_structure_report(root: &Path, json: bool) -> Result<(), Box<dyn std::error::Error>> {
153
+ let mut report = check_repository_quality(root, QualityMode::Report)?;
154
+ report
155
+ .violations
156
+ .retain(|violation| is_structure_check(&violation.check_id));
157
+ report.summary.violation_count = report.violations.len();
158
+ report.summary.blocking_violation_count = report.violations.len();
159
+ report.summary.baseline_violation_count = report
160
+ .violations
161
+ .iter()
162
+ .filter(|violation| violation.baseline)
163
+ .count();
164
+ report.ok = report.violations.is_empty();
165
+ report.schema = "naome.repository-structure-report.v1".to_string();
166
+
167
+ if json {
168
+ println!("{}", serde_json::to_string_pretty(&report)?);
169
+ } else if report.violations.is_empty() {
170
+ println!("NAOME repository structure report: no debt found.");
171
+ } else {
172
+ println!(
173
+ "NAOME repository structure report: {} violation(s).",
174
+ report.violations.len()
175
+ );
176
+ for violation in report.violations.iter().take(20) {
177
+ print_quality_violation(violation);
178
+ }
179
+ }
180
+ Ok(())
181
+ }
182
+
183
+ fn run_structure_explain(
184
+ root: &Path,
185
+ args: &[String],
186
+ json: bool,
187
+ ) -> Result<(), Box<dyn std::error::Error>> {
188
+ let Some(path) = option_value(args, "--path") else {
189
+ return Err("naome structure explain requires --path <path>.".into());
190
+ };
191
+ let explanation = explain_repository_structure(root, path)?;
192
+ if json {
193
+ println!("{}", serde_json::to_string_pretty(&explanation)?);
194
+ } else {
195
+ println!(
196
+ "{} role={} layer={} directory={}",
197
+ explanation.path, explanation.role, explanation.layer, explanation.directory
198
+ );
199
+ if let Some(language) = explanation.language {
200
+ println!("language={language}");
201
+ }
202
+ if let Some(module) = explanation.module {
203
+ println!("module={module}");
204
+ }
205
+ }
206
+ Ok(())
207
+ }
208
+
209
+ fn is_structure_check(check_id: &str) -> bool {
210
+ matches!(
211
+ check_id,
212
+ "directory-role-mixing"
213
+ | "misplaced-file-role"
214
+ | "root-file-sprawl"
215
+ | "dumping-ground-directory"
216
+ | "directory-size"
217
+ | "path-depth"
218
+ | "case-collision"
219
+ | "test-source-pairing"
220
+ )
221
+ }
222
+
135
223
  fn print_quality_violation(violation: &naome_core::QualityViolation) {
136
224
  let location = violation
137
225
  .line
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.2.0"
3
+ version = "1.2.1"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -0,0 +1,64 @@
1
+ use std::ffi::OsString;
2
+ use std::path::Path;
3
+ use std::process::Command;
4
+
5
+ use crate::models::{CheckDecision, NaomeError};
6
+
7
+ pub(super) fn run_node_check(
8
+ root: &Path,
9
+ script: &str,
10
+ args: &[&str],
11
+ ) -> Result<CheckDecision, NaomeError> {
12
+ let mut command_args = vec![script.to_string()];
13
+ command_args.extend(args.iter().map(ToString::to_string));
14
+ let node_bin = std::env::var_os("NAOME_NODE_BIN").unwrap_or_else(|| OsString::from("node"));
15
+ let output = Command::new(&node_bin)
16
+ .args(&command_args)
17
+ .current_dir(root)
18
+ .output()?;
19
+ let mut combined = String::new();
20
+ combined.push_str(&String::from_utf8_lossy(&output.stdout));
21
+ combined.push_str(&String::from_utf8_lossy(&output.stderr));
22
+
23
+ Ok(CheckDecision {
24
+ command: format!(
25
+ "{} {}{}",
26
+ node_bin.to_string_lossy(),
27
+ script,
28
+ if args.is_empty() {
29
+ String::new()
30
+ } else {
31
+ format!(" {}", args.join(" "))
32
+ }
33
+ ),
34
+ exit_code: output.status.code(),
35
+ ok: output.status.success(),
36
+ output: combined.trim().to_string(),
37
+ })
38
+ }
39
+
40
+ pub(super) fn extract_known_actions(output: &str) -> Vec<&'static str> {
41
+ const KNOWN: [&str; 11] = [
42
+ "commit_task_baseline",
43
+ "review_task_diff",
44
+ "request_task_changes",
45
+ "cancel_task_changes",
46
+ "commit_upgrade_baseline",
47
+ "review_diff_first",
48
+ "cancel_upgrade_baseline",
49
+ "run_first_run_protocol",
50
+ "run_upgrade_protocol",
51
+ "review_unowned_diff",
52
+ "create_task",
53
+ ];
54
+
55
+ let actions: Vec<&'static str> = KNOWN
56
+ .into_iter()
57
+ .filter(|action| output.contains(action))
58
+ .collect();
59
+ if actions.is_empty() {
60
+ vec!["review_task_admission"]
61
+ } else {
62
+ actions
63
+ }
64
+ }
@@ -0,0 +1,67 @@
1
+ use std::path::Path;
2
+
3
+ use crate::models::{Decision, NaomeError};
4
+ use crate::paths;
5
+ use crate::task_state::harness_refresh_diff;
6
+
7
+ pub(super) fn idle_diff_decision(
8
+ root: &Path,
9
+ changed_paths: &[String],
10
+ machine_owned: &[String],
11
+ known_harness_paths: &[String],
12
+ ) -> Result<Decision, NaomeError> {
13
+ let all_machine_owned = !machine_owned.is_empty()
14
+ && changed_paths
15
+ .iter()
16
+ .all(|path| paths::matches_any(path, machine_owned));
17
+ let all_harness_owned = !known_harness_paths.is_empty()
18
+ && changed_paths
19
+ .iter()
20
+ .all(|path| paths::matches_any(path, known_harness_paths));
21
+
22
+ if harness_refresh_diff(root)?.is_some_and(|diff| diff.unrelated_paths.is_empty()) {
23
+ Ok(harness_repair_decision(
24
+ "Machine-owned NAOME harness refresh files changed outside an active task.",
25
+ "Run NAOME intent for the next natural-language request; deterministic policy can baseline a pure harness refresh automatically.",
26
+ ))
27
+ } else if all_machine_owned {
28
+ Ok(harness_repair_decision(
29
+ "Machine-owned NAOME harness files changed outside an active task.",
30
+ "Review and baseline the harness repair or cancel it before feature work.",
31
+ ))
32
+ } else if all_harness_owned {
33
+ Ok(Decision::new(
34
+ "install_or_upgrade_unbaselined",
35
+ true,
36
+ "NAOME setup or upgrade files changed outside an active task.",
37
+ vec![
38
+ "commit_upgrade_baseline",
39
+ "review_diff_first",
40
+ "cancel_upgrade_baseline",
41
+ ],
42
+ "Resolve the setup or upgrade diff before feature work.",
43
+ ))
44
+ } else {
45
+ Ok(Decision::new(
46
+ "dirty_unowned_diff",
47
+ true,
48
+ "The repository has changes not owned by an active NAOME task.",
49
+ vec!["review_unowned_diff"],
50
+ "Review the unowned diff, or route a new task so NAOME can isolate task work without touching it.",
51
+ ))
52
+ }
53
+ }
54
+
55
+ fn harness_repair_decision(user_message: &str, next_action: &str) -> Decision {
56
+ Decision::new(
57
+ "harness_repair_unbaselined",
58
+ true,
59
+ user_message,
60
+ vec![
61
+ "commit_upgrade_baseline",
62
+ "review_diff_first",
63
+ "cancel_upgrade_baseline",
64
+ ],
65
+ next_action,
66
+ )
67
+ }
@@ -0,0 +1,36 @@
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 read_json(root: &Path, relative_path: &str) -> Result<Value, NaomeError> {
9
+ let content = fs::read_to_string(root.join(relative_path))?;
10
+ Ok(serde_json::from_str(&content)?)
11
+ }
12
+
13
+ pub(super) fn json_bool(value: &Value, key: &str) -> Option<bool> {
14
+ value.get(key).and_then(Value::as_bool)
15
+ }
16
+
17
+ pub(super) fn json_string(value: &Value, key: &str) -> Option<String> {
18
+ value
19
+ .get(key)
20
+ .and_then(Value::as_str)
21
+ .map(ToString::to_string)
22
+ }
23
+
24
+ pub(super) fn string_array_at(value: &Value, key: &str) -> Vec<String> {
25
+ value
26
+ .get(key)
27
+ .and_then(Value::as_array)
28
+ .map(|values| {
29
+ values
30
+ .iter()
31
+ .filter_map(Value::as_str)
32
+ .map(ToString::to_string)
33
+ .collect()
34
+ })
35
+ .unwrap_or_default()
36
+ }
@@ -0,0 +1,165 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::{Decision, NaomeError, TaskDecision};
6
+ use crate::paths;
7
+ use crate::task_state::harness_refresh_diff;
8
+
9
+ use super::idle::idle_diff_decision;
10
+ use super::json::{json_string, read_json, string_array_at};
11
+
12
+ pub(super) fn completed_task_decision(
13
+ root: &Path,
14
+ changed_paths: Vec<String>,
15
+ task: Option<&TaskDecision>,
16
+ ) -> Result<Decision, NaomeError> {
17
+ let mut decision = if changed_paths.is_empty() {
18
+ Decision::new(
19
+ "ready_for_task",
20
+ false,
21
+ "The last completed NAOME task has no open diff.",
22
+ vec!["create_task"],
23
+ "Task admission is clear; create the next task before feature work.",
24
+ )
25
+ } else if has_completed_task_owned_paths(task, &changed_paths) {
26
+ Decision::new(
27
+ "completed_task_unbaselined",
28
+ true,
29
+ "A completed NAOME task is verified and waiting for the next routing decision.",
30
+ vec![
31
+ "commit_task_baseline",
32
+ "review_task_diff",
33
+ "request_task_changes",
34
+ "cancel_task_changes",
35
+ ],
36
+ "Run NAOME intent for the next natural-language request; deterministic policy can baseline a valid completed task automatically.",
37
+ )
38
+ } else if harness_refresh_diff(root)?.is_some_and(|diff| diff.unrelated_paths.is_empty()) {
39
+ Decision::new(
40
+ "harness_repair_unbaselined",
41
+ true,
42
+ "Machine-owned NAOME harness refresh files changed outside an active task.",
43
+ vec![
44
+ "commit_upgrade_baseline",
45
+ "review_diff_first",
46
+ "cancel_upgrade_baseline",
47
+ ],
48
+ "Run NAOME intent for the next natural-language request; deterministic policy can baseline a pure harness refresh automatically.",
49
+ )
50
+ } else {
51
+ Decision::new(
52
+ "dirty_unowned_diff",
53
+ true,
54
+ "The completed NAOME task has been baselined, but unrelated user changes remain.",
55
+ vec!["review_unowned_diff"],
56
+ "Review the unrelated diff, or route a new task so NAOME can isolate task work without touching it.",
57
+ )
58
+ };
59
+
60
+ decision.changed_paths = changed_paths;
61
+ decision.required_context = vec![
62
+ "docs/naome/execution.md".to_string(),
63
+ ".naome/task-state.json".to_string(),
64
+ "docs/naome/agent-workflow.md".to_string(),
65
+ "docs/naome/testing.md".to_string(),
66
+ ];
67
+ Ok(decision)
68
+ }
69
+
70
+ pub(super) fn active_task_decision(
71
+ changed_paths: Vec<String>,
72
+ task: Option<&TaskDecision>,
73
+ ) -> Decision {
74
+ let allowed_paths = task
75
+ .map(|task| task.allowed_paths.clone())
76
+ .unwrap_or_default();
77
+ let out_of_scope: Vec<String> = changed_paths
78
+ .iter()
79
+ .filter(|path| !is_control_state_path(path) && !paths::matches_any(path, &allowed_paths))
80
+ .cloned()
81
+ .collect();
82
+
83
+ let mut decision = if out_of_scope.is_empty() {
84
+ Decision::new(
85
+ "active_task_in_progress",
86
+ false,
87
+ "A NAOME task is active and the current diff is inside its declared scope.",
88
+ vec!["continue_task", "request_task_changes", "complete_task"],
89
+ "Continue the active task and keep proof current.",
90
+ )
91
+ } else {
92
+ Decision::new(
93
+ "active_task_blocked",
94
+ true,
95
+ "A NAOME task is active, but the current diff includes paths outside its declared scope.",
96
+ vec![
97
+ "revise_task_scope",
98
+ "revert_out_of_scope_diff",
99
+ "request_human_review",
100
+ ],
101
+ "Resolve out-of-scope changes before completing this task.",
102
+ )
103
+ };
104
+ decision.changed_paths = if out_of_scope.is_empty() {
105
+ changed_paths
106
+ } else {
107
+ out_of_scope
108
+ };
109
+ decision.required_context = vec![
110
+ "docs/naome/execution.md".to_string(),
111
+ ".naome/task-state.json".to_string(),
112
+ "docs/naome/testing.md".to_string(),
113
+ ];
114
+ decision
115
+ }
116
+
117
+ pub(super) fn classify_idle_diff(
118
+ root: &Path,
119
+ changed_paths: Vec<String>,
120
+ ) -> Result<Decision, NaomeError> {
121
+ let manifest = read_json(root, ".naome/manifest.json").unwrap_or(Value::Null);
122
+ let machine_owned = string_array_at(&manifest, "machineOwned");
123
+ let project_owned = string_array_at(&manifest, "projectOwned");
124
+
125
+ let mut known_harness_paths = machine_owned.clone();
126
+ known_harness_paths.extend(project_owned);
127
+ let mut decision =
128
+ idle_diff_decision(root, &changed_paths, &machine_owned, &known_harness_paths)?;
129
+
130
+ decision.changed_paths = changed_paths;
131
+ decision.required_context = vec![
132
+ "docs/naome/execution.md".to_string(),
133
+ ".naome/task-state.json".to_string(),
134
+ ];
135
+ Ok(decision)
136
+ }
137
+
138
+ pub(super) fn task_decision(task_state: &Value, status: &str) -> Option<TaskDecision> {
139
+ let active_task = task_state.get("activeTask")?;
140
+ if active_task.is_null() {
141
+ return None;
142
+ }
143
+
144
+ Some(TaskDecision {
145
+ id: json_string(active_task, "id"),
146
+ status: status.to_string(),
147
+ request: json_string(active_task, "request"),
148
+ allowed_paths: string_array_at(active_task, "allowedPaths"),
149
+ required_check_ids: string_array_at(active_task, "requiredCheckIds"),
150
+ })
151
+ }
152
+
153
+ fn has_completed_task_owned_paths(task: Option<&TaskDecision>, changed_paths: &[String]) -> bool {
154
+ let Some(task) = task else {
155
+ return false;
156
+ };
157
+
158
+ changed_paths
159
+ .iter()
160
+ .any(|path| is_control_state_path(path) || paths::matches_any(path, &task.allowed_paths))
161
+ }
162
+
163
+ fn is_control_state_path(path: &str) -> bool {
164
+ path == ".naome/task-state.json"
165
+ }