@lamentis/naome 1.0.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 (49) hide show
  1. package/Cargo.lock +199 -0
  2. package/Cargo.toml +11 -0
  3. package/LICENSE +21 -0
  4. package/README.md +6 -0
  5. package/bin/naome-node.js +1424 -0
  6. package/bin/naome.js +129 -0
  7. package/crates/naome-cli/Cargo.toml +14 -0
  8. package/crates/naome-cli/src/main.rs +341 -0
  9. package/crates/naome-core/Cargo.toml +11 -0
  10. package/crates/naome-core/src/decision.rs +432 -0
  11. package/crates/naome-core/src/git.rs +70 -0
  12. package/crates/naome-core/src/harness_health.rs +557 -0
  13. package/crates/naome-core/src/install_plan.rs +82 -0
  14. package/crates/naome-core/src/lib.rs +17 -0
  15. package/crates/naome-core/src/models.rs +99 -0
  16. package/crates/naome-core/src/paths.rs +72 -0
  17. package/crates/naome-core/src/task_state.rs +1859 -0
  18. package/crates/naome-core/src/verification.rs +217 -0
  19. package/crates/naome-core/src/verification_contract.rs +406 -0
  20. package/crates/naome-core/tests/decision.rs +297 -0
  21. package/crates/naome-core/tests/harness_health.rs +232 -0
  22. package/crates/naome-core/tests/install_plan.rs +35 -0
  23. package/crates/naome-core/tests/task_state.rs +588 -0
  24. package/crates/naome-core/tests/verification.rs +165 -0
  25. package/crates/naome-core/tests/verification_contract.rs +181 -0
  26. package/native/darwin-arm64/naome +0 -0
  27. package/package.json +44 -0
  28. package/templates/naome-root/.naome/bin/check-harness-health.js +163 -0
  29. package/templates/naome-root/.naome/bin/check-task-state.js +180 -0
  30. package/templates/naome-root/.naome/bin/naome.js +306 -0
  31. package/templates/naome-root/.naome/init-state.json +13 -0
  32. package/templates/naome-root/.naome/manifest.json +45 -0
  33. package/templates/naome-root/.naome/package.json +3 -0
  34. package/templates/naome-root/.naome/task-contract.schema.json +174 -0
  35. package/templates/naome-root/.naome/task-state.json +8 -0
  36. package/templates/naome-root/.naome/upgrade-state.json +7 -0
  37. package/templates/naome-root/.naome/verification.json +45 -0
  38. package/templates/naome-root/.naomeignore +4 -0
  39. package/templates/naome-root/AGENTS.md +77 -0
  40. package/templates/naome-root/docs/naome/agent-workflow.md +82 -0
  41. package/templates/naome-root/docs/naome/architecture.md +37 -0
  42. package/templates/naome-root/docs/naome/decisions.md +18 -0
  43. package/templates/naome-root/docs/naome/execution.md +192 -0
  44. package/templates/naome-root/docs/naome/first-run.md +135 -0
  45. package/templates/naome-root/docs/naome/index.md +67 -0
  46. package/templates/naome-root/docs/naome/repo-profile.md +51 -0
  47. package/templates/naome-root/docs/naome/security.md +60 -0
  48. package/templates/naome-root/docs/naome/testing.md +51 -0
  49. package/templates/naome-root/docs/naome/upgrade.md +20 -0
@@ -0,0 +1,432 @@
1
+ use std::fs;
2
+ use std::ffi::OsString;
3
+ use std::path::Path;
4
+ use std::process::Command;
5
+
6
+ use serde_json::Value;
7
+
8
+ use crate::git;
9
+ use crate::models::{CheckDecision, Decision, NaomeError, TaskDecision};
10
+ use crate::paths;
11
+
12
+ #[derive(Debug, Clone, Copy)]
13
+ pub struct EvaluationOptions {
14
+ pub run_external_checks: bool,
15
+ }
16
+
17
+ impl EvaluationOptions {
18
+ pub fn online() -> Self {
19
+ Self {
20
+ run_external_checks: true,
21
+ }
22
+ }
23
+
24
+ pub fn offline() -> Self {
25
+ Self {
26
+ run_external_checks: false,
27
+ }
28
+ }
29
+ }
30
+
31
+ pub fn evaluate_decision(root: &Path, options: EvaluationOptions) -> Result<Decision, NaomeError> {
32
+ let changed_paths = git::changed_paths(root)?;
33
+
34
+ let harness_health = if options.run_external_checks {
35
+ Some(run_node_check(
36
+ root,
37
+ ".naome/bin/check-harness-health.js",
38
+ &[],
39
+ )?)
40
+ } else {
41
+ None
42
+ };
43
+
44
+ if let Some(check) = &harness_health {
45
+ if !check.ok {
46
+ let mut decision = Decision::new(
47
+ "harness_unhealthy",
48
+ true,
49
+ "NAOME harness health failed.",
50
+ vec!["repair_harness", "review_harness_health"],
51
+ "Repair the machine-owned harness before feature work.",
52
+ );
53
+ decision.changed_paths = changed_paths;
54
+ decision.harness_health = harness_health;
55
+ decision.required_context = vec![
56
+ ".naomeignore".to_string(),
57
+ "docs/naome/index.md".to_string(),
58
+ ".naome/manifest.json".to_string(),
59
+ ];
60
+ return Ok(decision);
61
+ }
62
+ }
63
+
64
+ let init_state = read_json(root, ".naome/init-state.json")?;
65
+ let upgrade_state = read_json(root, ".naome/upgrade-state.json")?;
66
+ let task_state = read_json(root, ".naome/task-state.json")?;
67
+
68
+ if json_string(&upgrade_state, "status").as_deref() == Some("needs_agent_upgrade") {
69
+ let mut decision = Decision::new(
70
+ "upgrade_required",
71
+ true,
72
+ "NAOME upgrade requires agent follow-up.",
73
+ vec!["run_upgrade_protocol"],
74
+ "Run the NAOME upgrade protocol before feature work.",
75
+ );
76
+ decision.changed_paths = changed_paths;
77
+ decision.harness_health = harness_health;
78
+ decision.required_context = vec![
79
+ "docs/naome/upgrade.md".to_string(),
80
+ ".naome/upgrade-state.json".to_string(),
81
+ ];
82
+ return Ok(decision);
83
+ }
84
+
85
+ if json_bool(&init_state, "initialized") != Some(true) {
86
+ let mut decision = Decision::new(
87
+ "first_run_required",
88
+ true,
89
+ "NAOME first-run intake has not been completed.",
90
+ vec!["run_first_run_protocol"],
91
+ "Run the NAOME first-run protocol before feature work.",
92
+ );
93
+ decision.changed_paths = changed_paths;
94
+ decision.harness_health = harness_health;
95
+ decision.required_context = vec![
96
+ "docs/naome/first-run.md".to_string(),
97
+ ".naome/init-state.json".to_string(),
98
+ ];
99
+ return Ok(decision);
100
+ }
101
+
102
+ let task_status = json_string(&task_state, "status").unwrap_or_else(|| "invalid".to_string());
103
+ let task = task_decision(&task_state, &task_status);
104
+
105
+ if task_status == "complete" {
106
+ let mut decision = if changed_paths.is_empty() {
107
+ Decision::new(
108
+ "ready_for_task",
109
+ false,
110
+ "The last completed NAOME task has no open diff.",
111
+ vec!["create_task"],
112
+ "Task admission is clear; create the next task before feature work.",
113
+ )
114
+ } else {
115
+ Decision::new(
116
+ "completed_task_unbaselined",
117
+ true,
118
+ "A completed NAOME task still has an unbaselined diff.",
119
+ vec![
120
+ "commit_task_baseline",
121
+ "review_task_diff",
122
+ "request_task_changes",
123
+ "cancel_task_changes",
124
+ ],
125
+ "Choose how to resolve the completed task diff before accepting new feature work.",
126
+ )
127
+ };
128
+ decision.changed_paths = changed_paths;
129
+ decision.harness_health = harness_health;
130
+ decision.task = task;
131
+ decision.required_context = vec![
132
+ "docs/naome/execution.md".to_string(),
133
+ ".naome/task-state.json".to_string(),
134
+ ];
135
+ return Ok(decision);
136
+ }
137
+
138
+ if task_status != "idle" {
139
+ let allowed_paths = task
140
+ .as_ref()
141
+ .map(|task| task.allowed_paths.clone())
142
+ .unwrap_or_default();
143
+ let out_of_scope: Vec<String> = changed_paths
144
+ .iter()
145
+ .filter(|path| {
146
+ !is_control_state_path(path) && !paths::matches_any(path, &allowed_paths)
147
+ })
148
+ .cloned()
149
+ .collect();
150
+
151
+ let mut decision = if out_of_scope.is_empty() {
152
+ Decision::new(
153
+ "active_task_in_progress",
154
+ false,
155
+ "A NAOME task is active and the current diff is inside its declared scope.",
156
+ vec!["continue_task", "request_task_changes", "complete_task"],
157
+ "Continue the active task and keep proof current.",
158
+ )
159
+ } else {
160
+ Decision::new(
161
+ "active_task_blocked",
162
+ true,
163
+ "A NAOME task is active, but the current diff includes paths outside its declared scope.",
164
+ vec!["revise_task_scope", "revert_out_of_scope_diff", "request_human_review"],
165
+ "Resolve out-of-scope changes before completing this task.",
166
+ )
167
+ };
168
+ decision.changed_paths = if out_of_scope.is_empty() {
169
+ changed_paths
170
+ } else {
171
+ out_of_scope
172
+ };
173
+ decision.harness_health = harness_health;
174
+ decision.task = task;
175
+ decision.required_context = vec![
176
+ "docs/naome/execution.md".to_string(),
177
+ ".naome/task-state.json".to_string(),
178
+ ];
179
+ return Ok(decision);
180
+ }
181
+
182
+ if !changed_paths.is_empty() {
183
+ let mut decision = classify_idle_diff(root, changed_paths)?;
184
+ decision.harness_health = harness_health;
185
+ decision.task = task;
186
+ return Ok(decision);
187
+ }
188
+
189
+ let task_admission = if options.run_external_checks {
190
+ Some(run_node_check(
191
+ root,
192
+ ".naome/bin/check-task-state.js",
193
+ &["--admission"],
194
+ )?)
195
+ } else {
196
+ None
197
+ };
198
+
199
+ if let Some(check) = &task_admission {
200
+ if !check.ok {
201
+ let mut decision = Decision::new(
202
+ "task_admission_blocked",
203
+ true,
204
+ "NAOME task admission is blocked.",
205
+ extract_known_actions(&check.output),
206
+ "Follow the task admission options before accepting new feature work.",
207
+ );
208
+ decision.harness_health = harness_health;
209
+ decision.task_admission = task_admission;
210
+ decision.task = task;
211
+ decision.required_context = vec![
212
+ "docs/naome/execution.md".to_string(),
213
+ ".naome/task-state.json".to_string(),
214
+ ];
215
+ return Ok(decision);
216
+ }
217
+ }
218
+
219
+ let mut decision = Decision::new(
220
+ "ready_for_task",
221
+ false,
222
+ "NAOME is ready for a new task.",
223
+ vec!["create_task"],
224
+ "Create or update a NAOME task before feature work.",
225
+ );
226
+ decision.harness_health = harness_health;
227
+ decision.task_admission = task_admission;
228
+ decision.task = task;
229
+ Ok(decision)
230
+ }
231
+
232
+ pub fn format_decision(decision: &Decision, mode: &str) -> String {
233
+ let title = if mode == "next" {
234
+ "NAOME next"
235
+ } else {
236
+ "NAOME status"
237
+ };
238
+ let mut lines = vec![
239
+ format!("{title}: {}", decision.state),
240
+ format!("Blocked: {}", decision.blocked),
241
+ format!("Summary: {}", decision.summary),
242
+ format!("Next action: {}", decision.next_action),
243
+ ];
244
+
245
+ if !decision.allowed_actions.is_empty() {
246
+ lines.push(format!(
247
+ "Allowed actions: {}",
248
+ decision.allowed_actions.join(", ")
249
+ ));
250
+ }
251
+
252
+ if !decision.changed_paths.is_empty() {
253
+ lines.push(format!(
254
+ "Changed paths: {}",
255
+ decision.changed_paths.join(", ")
256
+ ));
257
+ }
258
+
259
+ if !decision.required_context.is_empty() {
260
+ lines.push(format!(
261
+ "Required context: {}",
262
+ decision.required_context.join(", ")
263
+ ));
264
+ }
265
+
266
+ lines.push(String::new());
267
+ lines.join("\n")
268
+ }
269
+
270
+ fn classify_idle_diff(root: &Path, changed_paths: Vec<String>) -> Result<Decision, NaomeError> {
271
+ let manifest = read_json(root, ".naome/manifest.json").unwrap_or(Value::Null);
272
+ let machine_owned = string_array_at(&manifest, "machineOwned");
273
+ let project_owned = string_array_at(&manifest, "projectOwned");
274
+
275
+ let mut known_harness_paths = machine_owned.clone();
276
+ known_harness_paths.extend(project_owned);
277
+
278
+ let all_machine_owned = !machine_owned.is_empty()
279
+ && changed_paths
280
+ .iter()
281
+ .all(|path| paths::matches_any(path, &machine_owned));
282
+ let all_harness_owned = !known_harness_paths.is_empty()
283
+ && changed_paths
284
+ .iter()
285
+ .all(|path| paths::matches_any(path, &known_harness_paths));
286
+
287
+ let mut decision = if all_machine_owned {
288
+ Decision::new(
289
+ "harness_repair_unbaselined",
290
+ true,
291
+ "Machine-owned NAOME harness files changed outside an active task.",
292
+ vec![
293
+ "commit_upgrade_baseline",
294
+ "review_diff_first",
295
+ "cancel_upgrade_baseline",
296
+ ],
297
+ "Review and baseline the harness repair or cancel it before feature work.",
298
+ )
299
+ } else if all_harness_owned {
300
+ Decision::new(
301
+ "install_or_upgrade_unbaselined",
302
+ true,
303
+ "NAOME setup or upgrade files changed outside an active task.",
304
+ vec![
305
+ "commit_upgrade_baseline",
306
+ "review_diff_first",
307
+ "cancel_upgrade_baseline",
308
+ ],
309
+ "Resolve the setup or upgrade diff before feature work.",
310
+ )
311
+ } else {
312
+ Decision::new(
313
+ "dirty_unowned_diff",
314
+ true,
315
+ "The repository has changes not owned by an active NAOME task.",
316
+ vec!["review_unowned_diff", "clear_or_commit_unowned_diff"],
317
+ "Review, commit, or clear the unowned diff before accepting new feature work.",
318
+ )
319
+ };
320
+
321
+ decision.changed_paths = changed_paths;
322
+ decision.required_context = vec![
323
+ "docs/naome/execution.md".to_string(),
324
+ ".naome/task-state.json".to_string(),
325
+ ];
326
+ Ok(decision)
327
+ }
328
+
329
+ fn task_decision(task_state: &Value, status: &str) -> Option<TaskDecision> {
330
+ let active_task = task_state.get("activeTask")?;
331
+ if active_task.is_null() {
332
+ return None;
333
+ }
334
+
335
+ Some(TaskDecision {
336
+ id: json_string(active_task, "id"),
337
+ status: status.to_string(),
338
+ request: json_string(active_task, "request"),
339
+ allowed_paths: string_array_at(active_task, "allowedPaths"),
340
+ required_check_ids: string_array_at(active_task, "requiredCheckIds"),
341
+ })
342
+ }
343
+
344
+ fn is_control_state_path(path: &str) -> bool {
345
+ path == ".naome/task-state.json"
346
+ }
347
+
348
+ fn run_node_check(root: &Path, script: &str, args: &[&str]) -> Result<CheckDecision, NaomeError> {
349
+ let mut command_args = vec![script.to_string()];
350
+ command_args.extend(args.iter().map(ToString::to_string));
351
+ let node_bin = std::env::var_os("NAOME_NODE_BIN").unwrap_or_else(|| OsString::from("node"));
352
+ let output = Command::new(&node_bin)
353
+ .args(&command_args)
354
+ .current_dir(root)
355
+ .output()?;
356
+ let mut combined = String::new();
357
+ combined.push_str(&String::from_utf8_lossy(&output.stdout));
358
+ combined.push_str(&String::from_utf8_lossy(&output.stderr));
359
+
360
+ Ok(CheckDecision {
361
+ command: format!(
362
+ "{} {}{}",
363
+ node_bin.to_string_lossy(),
364
+ script,
365
+ if args.is_empty() {
366
+ String::new()
367
+ } else {
368
+ format!(" {}", args.join(" "))
369
+ }
370
+ ),
371
+ exit_code: output.status.code(),
372
+ ok: output.status.success(),
373
+ output: combined.trim().to_string(),
374
+ })
375
+ }
376
+
377
+ fn read_json(root: &Path, relative_path: &str) -> Result<Value, NaomeError> {
378
+ let content = fs::read_to_string(root.join(relative_path))?;
379
+ Ok(serde_json::from_str(&content)?)
380
+ }
381
+
382
+ fn json_bool(value: &Value, key: &str) -> Option<bool> {
383
+ value.get(key).and_then(Value::as_bool)
384
+ }
385
+
386
+ fn json_string(value: &Value, key: &str) -> Option<String> {
387
+ value
388
+ .get(key)
389
+ .and_then(Value::as_str)
390
+ .map(ToString::to_string)
391
+ }
392
+
393
+ fn string_array_at(value: &Value, key: &str) -> Vec<String> {
394
+ value
395
+ .get(key)
396
+ .and_then(Value::as_array)
397
+ .map(|values| {
398
+ values
399
+ .iter()
400
+ .filter_map(Value::as_str)
401
+ .map(ToString::to_string)
402
+ .collect()
403
+ })
404
+ .unwrap_or_default()
405
+ }
406
+
407
+ fn extract_known_actions(output: &str) -> Vec<&'static str> {
408
+ const KNOWN: [&str; 12] = [
409
+ "commit_task_baseline",
410
+ "review_task_diff",
411
+ "request_task_changes",
412
+ "cancel_task_changes",
413
+ "commit_upgrade_baseline",
414
+ "review_diff_first",
415
+ "cancel_upgrade_baseline",
416
+ "run_first_run_protocol",
417
+ "run_upgrade_protocol",
418
+ "review_unowned_diff",
419
+ "clear_or_commit_unowned_diff",
420
+ "create_task",
421
+ ];
422
+
423
+ let actions: Vec<&'static str> = KNOWN
424
+ .into_iter()
425
+ .filter(|action| output.contains(action))
426
+ .collect();
427
+ if actions.is_empty() {
428
+ vec!["review_task_admission"]
429
+ } else {
430
+ actions
431
+ }
432
+ }
@@ -0,0 +1,70 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+ use std::process::Command;
4
+
5
+ use crate::models::NaomeError;
6
+ use crate::paths;
7
+
8
+ pub fn changed_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
9
+ let ignored_patterns = read_naomeignore_patterns(root);
10
+ let output = Command::new("git")
11
+ .args(["status", "--porcelain=v1", "-z", "-uall"])
12
+ .current_dir(root)
13
+ .output()?;
14
+
15
+ if !output.status.success() {
16
+ return Ok(Vec::new());
17
+ }
18
+
19
+ let mut paths = Vec::new();
20
+ let mut entries = output
21
+ .stdout
22
+ .split(|byte| *byte == 0)
23
+ .filter(|entry| !entry.is_empty());
24
+ while let Some(entry) = entries.next() {
25
+ if entry.len() < 4 {
26
+ continue;
27
+ }
28
+
29
+ let status = String::from_utf8_lossy(&entry[0..2]);
30
+ let path_bytes = &entry[3..];
31
+ if path_bytes.is_empty() {
32
+ continue;
33
+ }
34
+
35
+ let path = String::from_utf8_lossy(path_bytes).replace('\\', "/");
36
+ if !paths::matches_any(&path, &ignored_patterns) {
37
+ paths.push(path);
38
+ }
39
+
40
+ if status.contains('R') || status.contains('C') {
41
+ let _ = entries.next();
42
+ }
43
+ }
44
+
45
+ paths.sort();
46
+ paths.dedup();
47
+ Ok(paths)
48
+ }
49
+
50
+ fn read_naomeignore_patterns(root: &Path) -> Vec<String> {
51
+ let Ok(content) = fs::read_to_string(root.join(".naomeignore")) else {
52
+ return Vec::new();
53
+ };
54
+
55
+ content
56
+ .lines()
57
+ .map(str::trim)
58
+ .filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
59
+ .map(normalize_ignore_pattern)
60
+ .collect()
61
+ }
62
+
63
+ fn normalize_ignore_pattern(pattern: &str) -> String {
64
+ let normalized = pattern.trim_start_matches("./").replace('\\', "/");
65
+ if normalized.ends_with('/') {
66
+ format!("{normalized}**")
67
+ } else {
68
+ normalized
69
+ }
70
+ }