@lamentis/naome 1.3.0 → 1.3.2

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 (149) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +11 -2
  3. package/bin/naome.js +62 -24
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/context_commands.rs +47 -0
  6. package/crates/naome-cli/src/dispatcher.rs +6 -0
  7. package/crates/naome-cli/src/main.rs +43 -6
  8. package/crates/naome-cli/src/quality_commands.rs +31 -46
  9. package/crates/naome-cli/src/quality_output.rs +34 -0
  10. package/crates/naome-cli/src/quality_reconcile_command.rs +45 -0
  11. package/crates/naome-cli/src/repository_model_commands.rs +84 -0
  12. package/crates/naome-cli/src/task_commands.rs +62 -0
  13. package/crates/naome-cli/src/workflow_commands.rs +100 -3
  14. package/crates/naome-core/Cargo.toml +1 -1
  15. package/crates/naome-core/src/context/helpers.rs +75 -0
  16. package/crates/naome-core/src/context/select.rs +134 -0
  17. package/crates/naome-core/src/context/types.rs +43 -0
  18. package/crates/naome-core/src/context.rs +6 -0
  19. package/crates/naome-core/src/decision/states.rs +1 -1
  20. package/crates/naome-core/src/decision.rs +4 -1
  21. package/crates/naome-core/src/install_plan.rs +18 -0
  22. package/crates/naome-core/src/intent/resolver_catalog/active.rs +38 -0
  23. package/crates/naome-core/src/intent/resolver_catalog/baseline.rs +44 -0
  24. package/crates/naome-core/src/intent/resolver_catalog/completed.rs +56 -0
  25. package/crates/naome-core/src/intent/resolver_catalog/dirty.rs +32 -0
  26. package/crates/naome-core/src/intent/resolver_catalog/ready.rs +32 -0
  27. package/crates/naome-core/src/intent/resolver_catalog/system.rs +20 -0
  28. package/crates/naome-core/src/intent/resolver_catalog.rs +12 -166
  29. package/crates/naome-core/src/journal.rs +2 -7
  30. package/crates/naome-core/src/lib.rs +33 -10
  31. package/crates/naome-core/src/quality/adapter_ios.rs +131 -0
  32. package/crates/naome-core/src/quality/adapter_support.rs +67 -0
  33. package/crates/naome-core/src/quality/adapters.rs +81 -18
  34. package/crates/naome-core/src/quality/cache.rs +7 -9
  35. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +4 -7
  36. package/crates/naome-core/src/quality/config.rs +21 -3
  37. package/crates/naome-core/src/quality/mod.rs +138 -7
  38. package/crates/naome-core/src/quality/reconcile.rs +138 -0
  39. package/crates/naome-core/src/quality/reconcile_anchors.rs +64 -0
  40. package/crates/naome-core/src/quality/scanner/analysis.rs +20 -5
  41. package/crates/naome-core/src/quality/scanner.rs +62 -17
  42. package/crates/naome-core/src/quality/semantic/checks.rs +17 -0
  43. package/crates/naome-core/src/quality/semantic/route.rs +1 -1
  44. package/crates/naome-core/src/quality/structure/adapter_ios.rs +149 -0
  45. package/crates/naome-core/src/quality/structure/adapters.rs +60 -42
  46. package/crates/naome-core/src/quality/structure/checks/directory.rs +6 -4
  47. package/crates/naome-core/src/quality/structure/classify/roles.rs +51 -5
  48. package/crates/naome-core/src/quality/structure/config.rs +24 -3
  49. package/crates/naome-core/src/quality/structure/mod.rs +3 -0
  50. package/crates/naome-core/src/quality/types.rs +20 -1
  51. package/crates/naome-core/src/repository_model/detect.rs +188 -0
  52. package/crates/naome-core/src/repository_model/explain.rs +121 -0
  53. package/crates/naome-core/src/repository_model/path_scan.rs +67 -0
  54. package/crates/naome-core/src/repository_model/path_support.rs +59 -0
  55. package/crates/naome-core/src/repository_model/types.rs +152 -0
  56. package/crates/naome-core/src/repository_model/world.rs +48 -0
  57. package/crates/naome-core/src/repository_model/world_adapters.rs +145 -0
  58. package/crates/naome-core/src/repository_model/world_path_facts.rs +55 -0
  59. package/crates/naome-core/src/repository_model/world_paths.rs +168 -0
  60. package/crates/naome-core/src/repository_model.rs +164 -0
  61. package/crates/naome-core/src/route/builtin_checks.rs +40 -1
  62. package/crates/naome-core/src/task_ledger/import.rs +142 -0
  63. package/crates/naome-core/src/task_ledger/model.rs +13 -0
  64. package/crates/naome-core/src/task_ledger/proof_record.rs +52 -0
  65. package/crates/naome-core/src/task_ledger/read.rs +118 -0
  66. package/crates/naome-core/src/task_ledger/render.rs +55 -0
  67. package/crates/naome-core/src/task_ledger/write.rs +38 -0
  68. package/crates/naome-core/src/task_ledger.rs +48 -0
  69. package/crates/naome-core/src/task_state/api.rs +4 -2
  70. package/crates/naome-core/src/task_state/completed_refresh.rs +5 -16
  71. package/crates/naome-core/src/task_state/diff.rs +2 -2
  72. package/crates/naome-core/src/task_state/evidence.rs +8 -3
  73. package/crates/naome-core/src/task_state/mod.rs +1 -1
  74. package/crates/naome-core/src/task_state/progress.rs +13 -0
  75. package/crates/naome-core/src/task_state/proof_model.rs +8 -8
  76. package/crates/naome-core/src/task_state/repair.rs +2 -2
  77. package/crates/naome-core/src/task_state/task_diff_api.rs +9 -18
  78. package/crates/naome-core/src/task_state/types.rs +24 -0
  79. package/crates/naome-core/src/verification.rs +29 -18
  80. package/crates/naome-core/src/workflow/agent/capability.rs +194 -0
  81. package/crates/naome-core/src/workflow/agent/context_delta.rs +42 -0
  82. package/crates/naome-core/src/workflow/agent/decision.rs +32 -0
  83. package/crates/naome-core/src/workflow/agent/execution.rs +80 -0
  84. package/crates/naome-core/src/workflow/agent/proof.rs +24 -0
  85. package/crates/naome-core/src/workflow/agent/support.rs +58 -0
  86. package/crates/naome-core/src/workflow/agent/watchdog.rs +47 -0
  87. package/crates/naome-core/src/workflow/agent.rs +34 -0
  88. package/crates/naome-core/src/workflow/agent_types.rs +105 -0
  89. package/crates/naome-core/src/workflow/doctor.rs +39 -0
  90. package/crates/naome-core/src/workflow/mod.rs +11 -0
  91. package/crates/naome-core/src/workflow/output.rs +8 -2
  92. package/crates/naome-core/src/workflow/phase_inference.rs +1 -1
  93. package/crates/naome-core/tests/context.rs +99 -0
  94. package/crates/naome-core/tests/harness_health.rs +33 -40
  95. package/crates/naome-core/tests/install_plan.rs +12 -0
  96. package/crates/naome-core/tests/quality.rs +178 -2
  97. package/crates/naome-core/tests/quality_performance.rs +39 -2
  98. package/crates/naome-core/tests/quality_structure_adapters.rs +39 -0
  99. package/crates/naome-core/tests/repo_support/mod.rs +7 -1
  100. package/crates/naome-core/tests/repo_support/verification_values.rs +148 -1
  101. package/crates/naome-core/tests/repository_model.rs +281 -0
  102. package/crates/naome-core/tests/route_user_diff.rs +49 -1
  103. package/crates/naome-core/tests/semantic_legacy.rs +72 -38
  104. package/crates/naome-core/tests/task_ledger.rs +328 -0
  105. package/crates/naome-core/tests/task_state.rs +34 -14
  106. package/crates/naome-core/tests/task_state_support/mod.rs +2 -1
  107. package/crates/naome-core/tests/task_state_support/states.rs +28 -11
  108. package/crates/naome-core/tests/verification.rs +14 -39
  109. package/crates/naome-core/tests/verification_contract.rs +6 -52
  110. package/crates/naome-core/tests/workflow_agent.rs +233 -0
  111. package/crates/naome-core/tests/workflow_agent_support/mod.rs +159 -0
  112. package/crates/naome-core/tests/workflow_doctor.rs +21 -0
  113. package/crates/naome-core/tests/workflow_integrity.rs +2 -20
  114. package/crates/naome-core/tests/workflow_support/mod.rs +59 -20
  115. package/installer/codex-hooks.js +121 -0
  116. package/installer/context.js +10 -0
  117. package/installer/filesystem.js +4 -0
  118. package/installer/flows.js +8 -4
  119. package/installer/harness-files.js +6 -0
  120. package/installer/install-plan.js +4 -0
  121. package/installer/main.js +1 -1
  122. package/installer/native.js +1 -1
  123. package/native/darwin-arm64/naome +0 -0
  124. package/native/linux-x64/naome +0 -0
  125. package/package.json +1 -1
  126. package/templates/naome-root/.codex/config.toml +2 -0
  127. package/templates/naome-root/.codex/hooks.json +70 -0
  128. package/templates/naome-root/.naome/bin/check-harness-health.js +8 -6
  129. package/templates/naome-root/.naome/bin/check-task-state.js +12 -7
  130. package/templates/naome-root/.naome/bin/codex-hook-io.js +122 -0
  131. package/templates/naome-root/.naome/bin/codex-hook-policy.js +180 -0
  132. package/templates/naome-root/.naome/bin/codex-hook-runtime.js +174 -0
  133. package/templates/naome-root/.naome/bin/codex-hook.js +6 -0
  134. package/templates/naome-root/.naome/bin/naome.js +35 -4
  135. package/templates/naome-root/.naome/manifest.json +12 -6
  136. package/templates/naome-root/.naome/repository-model.json +6 -0
  137. package/templates/naome-root/.naome/repository-quality.json +3 -1
  138. package/templates/naome-root/.naome/verification.json +15 -1
  139. package/templates/naome-root/AGENTS.md +38 -83
  140. package/templates/naome-root/docs/naome/agent-workflow.md +54 -18
  141. package/templates/naome-root/docs/naome/codex-hooks.md +82 -0
  142. package/templates/naome-root/docs/naome/context-economy.md +73 -0
  143. package/templates/naome-root/docs/naome/first-run.md +25 -14
  144. package/templates/naome-root/docs/naome/index.md +18 -10
  145. package/templates/naome-root/docs/naome/repository-model.md +92 -0
  146. package/templates/naome-root/docs/naome/repository-quality.md +47 -7
  147. package/templates/naome-root/docs/naome/repository-structure.md +10 -3
  148. package/templates/naome-root/docs/naome/task-ledger.md +71 -0
  149. package/templates/naome-root/docs/naome/testing.md +16 -3
@@ -0,0 +1,233 @@
1
+ mod workflow_agent_support;
2
+
3
+ use naome_core::{
4
+ agent_run_plan, context_delta_report, decision_gate, edit_session_watchdog,
5
+ proof_plan_for_task, repository_capability_graph, summarize_command_output, CommandCheckResult,
6
+ };
7
+
8
+ use workflow_agent_support::Repo;
9
+
10
+ #[test]
11
+ fn execution_plan_ledger_orders_read_edit_and_check_steps() {
12
+ let repo = Repo::new("execution-plan");
13
+ repo.write_harness_state("implementing", &["src/lib.rs"], &["unit"]);
14
+ repo.write_verification(&[("unit", "cargo test", "fast")]);
15
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 41 }\n");
16
+ repo.init_git();
17
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 42 }\n");
18
+
19
+ let plan = agent_run_plan(repo.path()).unwrap();
20
+
21
+ assert_eq!(plan.schema, "naome.agent-run-plan.v1");
22
+ assert_eq!(plan.execution_plan.task_state, "implementing");
23
+ assert!(plan.execution_plan.steps.iter().any(|step| {
24
+ step.id == "read-required-context"
25
+ && step.reason_codes.contains(&"token-economy".to_string())
26
+ }));
27
+ assert!(plan.execution_plan.steps.iter().any(|step| {
28
+ step.id == "run-path-quality" && step.paths == vec!["src/lib.rs".to_string()]
29
+ }));
30
+ assert!(plan.execution_plan.steps.iter().all(|step| {
31
+ !step
32
+ .commands
33
+ .iter()
34
+ .any(|command| command.contains(".naome/task-state.json"))
35
+ }));
36
+ assert!(plan
37
+ .execution_plan
38
+ .steps
39
+ .iter()
40
+ .any(|step| step.id == "run-progress-gate"));
41
+ }
42
+
43
+ #[test]
44
+ fn context_delta_marks_reusable_unread_and_stale_context() {
45
+ let repo = Repo::new("context-delta");
46
+ repo.write_harness_state("implementing", &["src/lib.rs"], &["unit"]);
47
+ repo.write_verification(&[("unit", "cargo test", "fast")]);
48
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 41 }\n");
49
+ repo.init_git();
50
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 42 }\n");
51
+
52
+ let delta = context_delta_report(
53
+ repo.path(),
54
+ &[
55
+ "docs/naome/testing.md".to_string(),
56
+ "src/lib.rs".to_string(),
57
+ ],
58
+ )
59
+ .unwrap();
60
+
61
+ assert_eq!(delta.schema, "naome.context-delta.v1");
62
+ assert!(delta
63
+ .reusable_context
64
+ .contains(&"docs/naome/testing.md".to_string()));
65
+ assert!(delta.stale_context.contains(&"src/lib.rs".to_string()));
66
+ assert!(delta
67
+ .unread_required_context
68
+ .contains(&".naome/task-state.json".to_string()));
69
+ }
70
+
71
+ #[test]
72
+ fn proof_planner_recommends_minimal_missing_checks_in_phase_order() {
73
+ let repo = Repo::new("proof-plan");
74
+ repo.write_harness_state("implementing", &["src/lib.rs"], &["shape", "unit", "pack"]);
75
+ repo.write_phased_verification();
76
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 41 }\n");
77
+ repo.init_git();
78
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 42 }\n");
79
+
80
+ let plan = proof_plan_for_task(
81
+ repo.path(),
82
+ &[CommandCheckResult {
83
+ check_id: "shape".to_string(),
84
+ exit_code: 0,
85
+ }],
86
+ )
87
+ .unwrap();
88
+
89
+ assert_eq!(plan.schema, "naome.proof-plan.v1");
90
+ assert_eq!(plan.recommended_check_ids, vec!["unit".to_string()]);
91
+ assert!(plan.withheld_check_ids.contains(&"pack".to_string()));
92
+ assert_eq!(plan.evidence_paths, vec!["src/lib.rs".to_string()]);
93
+ }
94
+
95
+ #[test]
96
+ fn proof_planner_recommends_semantic_changed_gate_with_repository_quality() {
97
+ let repo = Repo::new("proof-plan-semantic");
98
+ repo.write_harness_state(
99
+ "implementing",
100
+ &["src/lib.rs"],
101
+ &[
102
+ "repository-quality-check",
103
+ "repository-semantic-check",
104
+ "package",
105
+ ],
106
+ );
107
+ repo.write_verification(&[
108
+ (
109
+ "repository-quality-check",
110
+ "node .naome/bin/naome.js quality check --changed",
111
+ "fast",
112
+ ),
113
+ (
114
+ "repository-semantic-check",
115
+ "node .naome/bin/naome.js semantic check --changed",
116
+ "fast",
117
+ ),
118
+ ("package", "npm run pack:dry-run", "slow"),
119
+ ]);
120
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 41 }\n");
121
+ repo.init_git();
122
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 42 }\n");
123
+
124
+ let plan = proof_plan_for_task(repo.path(), &[]).unwrap();
125
+
126
+ assert_eq!(
127
+ plan.recommended_check_ids,
128
+ vec![
129
+ "repository-quality-check".to_string(),
130
+ "repository-semantic-check".to_string()
131
+ ]
132
+ );
133
+ assert!(plan.withheld_check_ids.contains(&"package".to_string()));
134
+ }
135
+
136
+ #[test]
137
+ fn output_digest_keeps_relevant_lines_paths_and_artifacts() {
138
+ let output = [
139
+ "running 2 tests",
140
+ "error: failed at src/lib.rs:4",
141
+ "full log written to target/test-output/fail.log",
142
+ "noise line",
143
+ ]
144
+ .join("\n");
145
+
146
+ let digest = summarize_command_output("cargo test", ".", 101, &output, 3);
147
+
148
+ assert!(digest.truncated);
149
+ assert!(digest
150
+ .relevant_lines
151
+ .iter()
152
+ .any(|line| line.contains("error")));
153
+ assert!(digest.affected_paths.contains(&"src/lib.rs".to_string()));
154
+ assert!(digest
155
+ .artifacts
156
+ .contains(&"target/test-output/fail.log".to_string()));
157
+ }
158
+
159
+ #[test]
160
+ fn capability_graph_maps_stack_roots_and_checks() {
161
+ let repo = Repo::new("capability-graph");
162
+ repo.write_harness_state("idle", &[], &[]);
163
+ repo.write_verification(&[("unit", "cargo test", "fast")]);
164
+ repo.write_file(
165
+ "Cargo.toml",
166
+ "[package]\nname = \"demo\"\nversion = \"0.1.0\"\n",
167
+ );
168
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 42 }\n");
169
+
170
+ let graph = repository_capability_graph(repo.path()).unwrap();
171
+
172
+ assert_eq!(graph.schema, "naome.capability-graph.v1");
173
+ assert!(graph
174
+ .capabilities
175
+ .iter()
176
+ .any(|capability| capability.id == "language:rust"));
177
+ assert!(graph
178
+ .capabilities
179
+ .iter()
180
+ .any(|capability| capability.id == "build:cargo"));
181
+ assert!(graph
182
+ .verification_edges
183
+ .iter()
184
+ .any(|edge| edge.check_id == "unit"));
185
+ }
186
+
187
+ #[test]
188
+ fn edit_watchdog_flags_scope_drift_and_recommends_early_gates() {
189
+ let repo = Repo::new("edit-watchdog");
190
+ repo.write_harness_state("implementing", &["src/lib.rs"], &["unit"]);
191
+ repo.write_verification(&[("unit", "cargo test", "fast")]);
192
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 42 }\n");
193
+ repo.write_file("README.md", "# Demo\n");
194
+
195
+ let watchdog = edit_session_watchdog(
196
+ repo.path(),
197
+ &["src/lib.rs".to_string(), "README.md".to_string()],
198
+ )
199
+ .unwrap();
200
+
201
+ assert_eq!(watchdog.schema, "naome.edit-session-watchdog.v1");
202
+ assert!(watchdog
203
+ .next_commands
204
+ .contains(&"node .naome/bin/naome.js quality check --path src/lib.rs".to_string()));
205
+ assert!(watchdog.findings.iter().any(|finding| {
206
+ finding.check_id == "scope-drift" && finding.paths == vec!["README.md".to_string()]
207
+ }));
208
+ }
209
+
210
+ #[test]
211
+ fn decision_gate_classifies_continue_vs_human_review() {
212
+ let repo = Repo::new("decision-gate");
213
+ repo.write_harness_state("implementing", &["src/lib.rs"], &["unit"]);
214
+ repo.write_verification(&[("unit", "cargo test", "fast")]);
215
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 41 }\n");
216
+ repo.init_git();
217
+ repo.write_file("src/lib.rs", "pub fn answer() -> i32 { 42 }\n");
218
+
219
+ let gate = decision_gate(repo.path()).unwrap();
220
+
221
+ assert_eq!(gate.schema, "naome.decision-gate.v1");
222
+ assert_eq!(gate.action, "continue");
223
+ assert!(gate
224
+ .reason_codes
225
+ .contains(&"active_task_in_progress".to_string()));
226
+
227
+ repo.write_file("README.md", "# Drift\n");
228
+ let drift_gate = decision_gate(repo.path()).unwrap();
229
+ assert_eq!(drift_gate.action, "ask_human");
230
+ assert!(drift_gate
231
+ .reason_codes
232
+ .contains(&"scope_review_required".to_string()));
233
+ }
@@ -0,0 +1,159 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+ use std::sync::atomic::{AtomicU64, Ordering};
5
+ use std::time::{SystemTime, UNIX_EPOCH};
6
+
7
+ use serde_json::json;
8
+
9
+ pub struct Repo {
10
+ root: PathBuf,
11
+ }
12
+
13
+ impl Repo {
14
+ pub fn new(name: &str) -> Self {
15
+ let nonce = SystemTime::now()
16
+ .duration_since(UNIX_EPOCH)
17
+ .unwrap()
18
+ .as_nanos();
19
+ let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
20
+ let root =
21
+ std::env::temp_dir().join(format!("naome-workflow-agent-{name}-{nonce}-{counter}"));
22
+ fs::create_dir_all(root.join(".naome")).unwrap();
23
+ Self { root }
24
+ }
25
+
26
+ pub fn path(&self) -> &Path {
27
+ &self.root
28
+ }
29
+
30
+ pub fn write_harness_state(&self, status: &str, allowed_paths: &[&str], checks: &[&str]) {
31
+ self.write_file(".naomeignore", ".naome/archive/\n");
32
+ self.write_json(".naome/init-state.json", json!({ "initialized": true }));
33
+ self.write_json(".naome/upgrade-state.json", json!({ "status": "complete" }));
34
+ self.write_json(
35
+ ".naome/task-state.json",
36
+ task_state(status, allowed_paths, checks),
37
+ );
38
+ self.write_file("docs/naome/testing.md", "# NAOME Testing\n");
39
+ }
40
+
41
+ pub fn write_verification(&self, checks: &[(&str, &str, &str)]) {
42
+ self.write_json(
43
+ ".naome/verification.json",
44
+ json!({
45
+ "schema": "naome.verification.v1",
46
+ "version": 1,
47
+ "status": "ready",
48
+ "checks": checks.iter().map(|(id, command, cost)| json!({
49
+ "id": id,
50
+ "command": command,
51
+ "cwd": ".",
52
+ "purpose": "test check",
53
+ "cost": cost,
54
+ "source": "test",
55
+ "evidence": ["src/lib.rs"],
56
+ "lastVerified": null
57
+ })).collect::<Vec<_>>(),
58
+ "changeTypes": [],
59
+ "releaseGates": []
60
+ }),
61
+ );
62
+ }
63
+
64
+ pub fn write_phased_verification(&self) {
65
+ self.write_verification(&[
66
+ (
67
+ "shape",
68
+ "node .naome/bin/check-task-state.js --progress",
69
+ "fast",
70
+ ),
71
+ ("unit", "cargo test", "fast"),
72
+ ("pack", "npm run pack:dry-run", "slow"),
73
+ ]);
74
+ let mut verification: serde_json::Value = serde_json::from_str(
75
+ &fs::read_to_string(self.root.join(".naome/verification.json")).unwrap(),
76
+ )
77
+ .unwrap();
78
+ verification["phases"] = json!([
79
+ { "id": "shape", "order": 1, "checkIds": ["shape"] },
80
+ { "id": "focused-tests", "order": 2, "checkIds": ["unit"] },
81
+ { "id": "package-release", "order": 3, "checkIds": ["pack"] }
82
+ ]);
83
+ self.write_json(".naome/verification.json", verification);
84
+ }
85
+
86
+ pub fn init_git(&self) {
87
+ self.git(&["init"]);
88
+ self.git(&["config", "user.email", "naome@example.com"]);
89
+ self.git(&["config", "user.name", "NAOME Test"]);
90
+ self.git(&["add", "."]);
91
+ self.git(&["commit", "-m", "baseline"]);
92
+ }
93
+
94
+ pub fn write_file(&self, relative_path: &str, content: &str) {
95
+ let path = self.root.join(relative_path);
96
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
97
+ fs::write(path, content).unwrap();
98
+ }
99
+
100
+ fn git(&self, args: &[&str]) {
101
+ let output = Command::new("git")
102
+ .args(args)
103
+ .current_dir(&self.root)
104
+ .output()
105
+ .unwrap();
106
+ assert!(
107
+ output.status.success(),
108
+ "git {:?} failed\nstdout:\n{}\nstderr:\n{}",
109
+ args,
110
+ String::from_utf8_lossy(&output.stdout),
111
+ String::from_utf8_lossy(&output.stderr)
112
+ );
113
+ }
114
+
115
+ fn write_json(&self, relative_path: &str, value: serde_json::Value) {
116
+ self.write_file(
117
+ relative_path,
118
+ &format!("{}\n", serde_json::to_string_pretty(&value).unwrap()),
119
+ );
120
+ }
121
+ }
122
+
123
+ fn task_state(status: &str, allowed_paths: &[&str], checks: &[&str]) -> serde_json::Value {
124
+ json!({
125
+ "schema": "naome.task-state.v1",
126
+ "version": 1,
127
+ "status": status,
128
+ "activeTask": {
129
+ "id": "demo-task",
130
+ "request": "Update demo.",
131
+ "userPrompt": {
132
+ "receivedAt": "2026-05-08T00:00:00.000Z",
133
+ "text": "Update demo."
134
+ },
135
+ "admission": {
136
+ "command": "node .naome/bin/check-task-state.js --admission",
137
+ "cwd": ".",
138
+ "exitCode": 0,
139
+ "checkedAt": "2026-05-08T00:00:00.000Z",
140
+ "gitHead": "",
141
+ "changedPaths": []
142
+ },
143
+ "allowedPaths": allowed_paths,
144
+ "declaredChangeTypes": ["harness-core"],
145
+ "requiredCheckIds": checks,
146
+ "proofResults": [],
147
+ "revisions": [],
148
+ "humanReview": {
149
+ "required": false,
150
+ "approved": false,
151
+ "reason": null
152
+ }
153
+ },
154
+ "blocker": null,
155
+ "updatedAt": "2026-05-08T00:00:00.000Z"
156
+ })
157
+ }
158
+
159
+ static COUNTER: AtomicU64 = AtomicU64::new(0);
@@ -22,3 +22,24 @@ fn doctor_report_explains_active_task_and_policy_files() {
22
22
  .next_action
23
23
  .contains("Finish or resolve the active task"));
24
24
  }
25
+
26
+ #[test]
27
+ fn doctor_report_marks_stale_repository_model() {
28
+ let repo = WorkflowFixture::new("doctor-repository-model-stale");
29
+ repo.init_git();
30
+ repo.write(".naome/repository-quality.json", "{}\n");
31
+ repo.write(".naome/repository-structure.json", "{}\n");
32
+ repo.write(".naome/repository-model.json", "{\"schema\":\"naome.repository-model.v1\",\"version\":1,\"status\":\"ready\",\"facts\":[]}\n");
33
+ repo.write("Package.swift", "// swift-tools-version: 5.9\n");
34
+
35
+ let report = doctor_report(repo.path()).unwrap();
36
+
37
+ assert!(!report.ok);
38
+ assert_eq!(report.repository_model.config_present, true);
39
+ assert_eq!(report.repository_model.status, "stale");
40
+ assert!(report
41
+ .repository_model
42
+ .messages
43
+ .iter()
44
+ .any(|message| message.contains("naome repo model --write")));
45
+ }
@@ -5,8 +5,7 @@ use std::fs;
5
5
  use naome_core::{
6
6
  refresh_integrity, summarize_command_output, verification_phase_plan, CommandCheckResult,
7
7
  };
8
- use serde_json::json;
9
- use workflow_support::{check, phase, WorkflowFixture};
8
+ use workflow_support::{phased_verification_fixture, WorkflowFixture};
10
9
 
11
10
  #[test]
12
11
  fn refresh_integrity_is_idempotent_and_repairs_manifest_hash() {
@@ -29,24 +28,7 @@ fn refresh_integrity_is_idempotent_and_repairs_manifest_hash() {
29
28
  #[test]
30
29
  fn failed_early_phase_withholds_expensive_recommendations() {
31
30
  let repo = WorkflowFixture::new("verification-phases");
32
- repo.write_verification(json!({
33
- "schema": "naome.verification.v1",
34
- "version": 1,
35
- "status": "ready",
36
- "lastUpdated": "2026-05-07",
37
- "checks": [
38
- check("naome-harness-health", "node .naome/bin/check-harness-health.js", "fast"),
39
- check("repository-quality-check", "node .naome/bin/naome.js quality check --changed", "fast"),
40
- check("package-dry-run", "npm run pack:dry-run", "medium")
41
- ],
42
- "phases": [
43
- phase("shape-health", 10, ["naome-harness-health"]),
44
- phase("quality", 20, ["repository-quality-check"]),
45
- phase("package-release", 50, ["package-dry-run"])
46
- ],
47
- "changeTypes": [],
48
- "releaseGates": []
49
- }));
31
+ repo.write_verification(phased_verification_fixture());
50
32
 
51
33
  let plan = verification_phase_plan(
52
34
  repo.path(),
@@ -80,17 +80,10 @@ impl WorkflowFixture {
80
80
  checks,
81
81
  git_text(&self.root, &["rev-parse", "HEAD"]),
82
82
  );
83
- self.write(
84
- ".naome/task-state.json",
85
- &pretty(json!({
86
- "schema": "naome.task-state.v2",
87
- "version": 2,
88
- "status": status,
89
- "activeTask": active_task,
90
- "blocker": null,
91
- "updatedAt": "2026-05-07T12:00:00.000Z"
92
- })),
93
- );
83
+ let mut task_state = task_state_template_fixture();
84
+ task_state["status"] = json!(status);
85
+ task_state["activeTask"] = active_task;
86
+ self.write(".naome/task-state.json", &pretty(task_state));
94
87
  }
95
88
 
96
89
  pub fn write_verification(&self, value: serde_json::Value) {
@@ -113,15 +106,32 @@ impl Drop for WorkflowFixture {
113
106
  }
114
107
 
115
108
  pub fn default_verification() -> serde_json::Value {
116
- json!({
117
- "schema": "naome.verification.v1",
118
- "version": 1,
119
- "status": "ready",
120
- "lastUpdated": "2026-05-07",
121
- "checks": [check("diff-check", "git diff --check", "fast")],
122
- "changeTypes": [],
123
- "releaseGates": []
124
- })
109
+ let mut value = verification_template_fixture();
110
+ value["checks"] = json!([check("diff-check", "git diff --check", "fast")]);
111
+ value
112
+ }
113
+
114
+ pub fn phased_verification_fixture() -> serde_json::Value {
115
+ let mut value = verification_template_fixture();
116
+ value["checks"] = json!([
117
+ check(
118
+ "naome-harness-health",
119
+ "node .naome/bin/check-harness-health.js",
120
+ "fast"
121
+ ),
122
+ check(
123
+ "repository-quality-check",
124
+ "node .naome/bin/naome.js quality check --changed",
125
+ "fast"
126
+ ),
127
+ check("package-dry-run", "npm run pack:dry-run", "medium")
128
+ ]);
129
+ value["phases"] = json!([
130
+ phase("shape-health", 10, ["naome-harness-health"]),
131
+ phase("quality", 20, ["repository-quality-check"]),
132
+ phase("package-release", 50, ["package-dry-run"])
133
+ ]);
134
+ value
125
135
  }
126
136
 
127
137
  pub fn check(id: &str, command: &str, cost: &str) -> serde_json::Value {
@@ -159,6 +169,35 @@ fn active_task_state(
159
169
  })
160
170
  }
161
171
 
172
+ fn task_state_template_fixture() -> serde_json::Value {
173
+ serde_json::from_str(
174
+ r#"{
175
+ "schema": "naome.task-state.v2",
176
+ "version": 2,
177
+ "status": null,
178
+ "activeTask": null,
179
+ "blocker": null,
180
+ "updatedAt": "2026-05-07T12:00:00.000Z"
181
+ }"#,
182
+ )
183
+ .unwrap()
184
+ }
185
+
186
+ fn verification_template_fixture() -> serde_json::Value {
187
+ serde_json::from_str(
188
+ r#"{
189
+ "schema": "naome.verification.v1",
190
+ "version": 1,
191
+ "status": "ready",
192
+ "lastUpdated": "2026-05-07",
193
+ "checks": [],
194
+ "changeTypes": [],
195
+ "releaseGates": []
196
+ }"#,
197
+ )
198
+ .unwrap()
199
+ }
200
+
162
201
  fn admission_record(git_head: String) -> serde_json::Value {
163
202
  json!({
164
203
  "command": "node .naome/bin/check-task-state.js --admission",
@@ -0,0 +1,121 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ import { replaceHarnessFile } from "./harness-file-ops.js";
7
+ import { printSection } from "./output.js";
8
+
9
+ const HOOK_CONFIG_PATH = ".codex/hooks.json";
10
+ const HOOK_FEATURE_CONFIG_PATH = ".codex/config.toml";
11
+ const HOOK_DISPATCHER_PATH = ".naome/bin/codex-hook.js";
12
+ const HOOK_IO_PATH = ".naome/bin/codex-hook-io.js";
13
+ const HOOK_POLICY_PATH = ".naome/bin/codex-hook-policy.js";
14
+ const HOOK_RUNTIME_PATH = ".naome/bin/codex-hook-runtime.js";
15
+ const HOOK_DOC_PATH = "docs/naome/codex-hooks.md";
16
+
17
+ const CODEX_HOOK_MACHINE_PATHS = [
18
+ HOOK_FEATURE_CONFIG_PATH,
19
+ HOOK_CONFIG_PATH,
20
+ HOOK_DISPATCHER_PATH,
21
+ HOOK_IO_PATH,
22
+ HOOK_POLICY_PATH,
23
+ HOOK_RUNTIME_PATH,
24
+ HOOK_DOC_PATH,
25
+ ];
26
+ const CODEX_HOOK_EXECUTABLE_PATHS = [HOOK_DISPATCHER_PATH];
27
+
28
+ export async function resolveCodexHooksPreference(ctx) {
29
+ const override = parseCodexHooksOverride(process.env.NAOME_CODEX_HOOKS);
30
+ if (override !== null) {
31
+ setCodexHooksEnabled(ctx, override);
32
+ return;
33
+ }
34
+
35
+ if (hasManagedCodexHooks(ctx)) {
36
+ setCodexHooksEnabled(ctx, true);
37
+ return;
38
+ }
39
+
40
+ if (!process.stdin.isTTY || process.env.CI === "true") {
41
+ setCodexHooksEnabled(ctx, false);
42
+ return;
43
+ }
44
+
45
+ printSection(ctx, "Optional Codex Hooks");
46
+ console.log(`${ctx.color.dim("purpose")} earlier NAOME feedback for Codex agents`);
47
+ console.log(`${ctx.color.dim("scope ")} repo-local acceleration only; normal NAOME gates remain authoritative`);
48
+ console.log(`${ctx.color.dim("human ")} optional; declining keeps install and sync working`);
49
+ console.log("");
50
+
51
+ const rl = createInterface({ input, output });
52
+ const answer = await rl.question(`${ctx.color.yellow("?")} Install optional Codex Hooks for this repository? (y/N) `);
53
+ rl.close();
54
+
55
+ setCodexHooksEnabled(ctx, answer.trim().toLowerCase() === "y");
56
+ }
57
+
58
+ export function ensureCodexHooks(ctx, archiveDirName) {
59
+ if (!ctx.codexHooksEnabled) {
60
+ return;
61
+ }
62
+
63
+ ensureCodexHookManifestPaths(ctx);
64
+ for (const relativePath of ctx.optionalCodexHookPaths) {
65
+ replaceHarnessFile(ctx, relativePath, archiveDirName);
66
+ }
67
+ }
68
+
69
+ export function ensureCodexHookManifestPaths(ctx) {
70
+ for (const relativePath of CODEX_HOOK_MACHINE_PATHS) {
71
+ appendUnique(ctx.machineOwnedPaths, relativePath);
72
+ appendUnique(ctx.localOnlyMachineOwnedPaths, relativePath);
73
+ appendUnique(ctx.localOnlyGitExcludeEntries, relativePath);
74
+ appendUnique(ctx.localOnlyGitUntrackPaths, relativePath);
75
+ }
76
+ for (const relativePath of CODEX_HOOK_EXECUTABLE_PATHS) {
77
+ ctx.executableMachineOwnedPaths.add(relativePath);
78
+ }
79
+ }
80
+
81
+ function setCodexHooksEnabled(ctx, enabled) {
82
+ ctx.codexHooksEnabled = enabled;
83
+ if (enabled) {
84
+ ensureCodexHookManifestPaths(ctx);
85
+ }
86
+ }
87
+
88
+ function hasManagedCodexHooks(ctx) {
89
+ const hookConfigPath = join(ctx.targetRoot, HOOK_CONFIG_PATH);
90
+ if (!existsSync(hookConfigPath)) {
91
+ return false;
92
+ }
93
+
94
+ try {
95
+ const hooks = JSON.parse(readFileSync(hookConfigPath, "utf8"));
96
+ return hooks.schema === "naome.codex-hooks.v1";
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ function parseCodexHooksOverride(value) {
103
+ if (value === undefined) {
104
+ return null;
105
+ }
106
+
107
+ const normalized = value.trim().toLowerCase();
108
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) {
109
+ return true;
110
+ }
111
+ if (["0", "false", "no", "n", "off"].includes(normalized)) {
112
+ return false;
113
+ }
114
+ return null;
115
+ }
116
+
117
+ function appendUnique(values, value) {
118
+ if (!values.includes(value)) {
119
+ values.push(value);
120
+ }
121
+ }
@@ -34,6 +34,16 @@ export function createInstallerContext() {
34
34
  installPlan: null,
35
35
  machineOwnedPaths: [],
36
36
  projectOwnedPaths: [],
37
+ optionalCodexHookPaths: [
38
+ ".codex/config.toml",
39
+ ".codex/hooks.json",
40
+ ".naome/bin/codex-hook.js",
41
+ ".naome/bin/codex-hook-io.js",
42
+ ".naome/bin/codex-hook-policy.js",
43
+ ".naome/bin/codex-hook-runtime.js",
44
+ "docs/naome/codex-hooks.md",
45
+ ],
46
+ codexHooksEnabled: false,
37
47
  localOnlyMachineOwnedPaths: [],
38
48
  localOnlyGitIgnoreEntries: [],
39
49
  localOnlyGitExcludeEntries: [],