@lamentis/naome 1.4.1 → 1.4.3

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 (52) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +17 -122
  3. package/crates/naome-cli/Cargo.toml +1 -1
  4. package/crates/naome-cli/src/main.rs +14 -5
  5. package/crates/naome-cli/src/task_commands/agent_snapshot.rs +173 -0
  6. package/crates/naome-cli/src/task_commands/can_edit.rs +64 -0
  7. package/crates/naome-cli/src/task_commands/check_run/output.rs +34 -0
  8. package/crates/naome-cli/src/task_commands/check_run/receipts.rs +163 -0
  9. package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
  10. package/crates/naome-cli/src/task_commands/check_run.rs +196 -0
  11. package/crates/naome-cli/src/task_commands/commit_preflight.rs +89 -0
  12. package/crates/naome-cli/src/task_commands/common.rs +39 -1
  13. package/crates/naome-cli/src/task_commands/compact_proof.rs +69 -0
  14. package/crates/naome-cli/src/task_commands/complete.rs +43 -0
  15. package/crates/naome-cli/src/task_commands/loop_control.rs +73 -0
  16. package/crates/naome-cli/src/task_commands/path_policy.rs +57 -0
  17. package/crates/naome-cli/src/task_commands/planner/checks.rs +166 -0
  18. package/crates/naome-cli/src/task_commands/planner/impact.rs +35 -0
  19. package/crates/naome-cli/src/task_commands/planner/mod.rs +24 -0
  20. package/crates/naome-cli/src/task_commands/preflight.rs +208 -0
  21. package/crates/naome-cli/src/task_commands/readiness.rs +14 -10
  22. package/crates/naome-cli/src/task_commands/record.rs +176 -37
  23. package/crates/naome-cli/src/task_commands/repair.rs +58 -11
  24. package/crates/naome-cli/src/task_commands/scope_suggestions.rs +109 -0
  25. package/crates/naome-cli/src/task_commands.rs +26 -3
  26. package/crates/naome-cli/tests/task_cli_agent_controls.rs +9 -16
  27. package/crates/naome-cli/tests/task_cli_fast_flow.rs +290 -0
  28. package/crates/naome-cli/tests/task_cli_loop.rs +383 -0
  29. package/crates/naome-cli/tests/task_cli_loop_edit.rs +144 -0
  30. package/crates/naome-cli/tests/task_cli_support/mod.rs +28 -0
  31. package/crates/naome-core/Cargo.toml +1 -1
  32. package/crates/naome-core/src/lib.rs +7 -7
  33. package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
  34. package/crates/naome-core/src/task_state/mod.rs +2 -0
  35. package/crates/naome-core/src/task_state/status/control/repair.rs +2 -2
  36. package/crates/naome-core/src/task_state/status/model.rs +2 -0
  37. package/crates/naome-core/src/task_state/status/proof.rs +59 -9
  38. package/crates/naome-core/src/task_state/status/proof_read.rs +14 -0
  39. package/crates/naome-core/src/task_state/status/report_context.rs +23 -1
  40. package/crates/naome-core/src/task_state/status/transition.rs +29 -1
  41. package/crates/naome-core/tests/task_status.rs +122 -0
  42. package/installer/context.js +1 -1
  43. package/installer/harness-verification.js +2 -6
  44. package/installer/manifest-state.js +2 -2
  45. package/installer/native.js +3 -31
  46. package/native/darwin-arm64/naome +0 -0
  47. package/native/linux-x64/naome +0 -0
  48. package/package.json +1 -1
  49. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  50. package/templates/naome-root/.naome/bin/check-task-state.js +4 -39
  51. package/templates/naome-root/.naome/bin/naome.js +2 -30
  52. package/templates/naome-root/.naome/manifest.json +2 -2
@@ -0,0 +1,57 @@
1
+ use std::path::Path;
2
+
3
+ use naome_core::{naomeignore_patterns, path_matches_any};
4
+ use serde_json::json;
5
+
6
+ pub(super) fn normalize_requested_path(raw_path: &str) -> (String, Option<String>) {
7
+ if raw_path.contains('\\') {
8
+ return (
9
+ raw_path.to_string(),
10
+ Some("Backslash path separators are not allowed.".to_string()),
11
+ );
12
+ }
13
+ let path = Path::new(raw_path);
14
+ if path.is_absolute() {
15
+ return (
16
+ raw_path.to_string(),
17
+ Some("Absolute paths are not allowed.".to_string()),
18
+ );
19
+ }
20
+ if raw_path.split('/').any(|part| part == "..") {
21
+ return (
22
+ raw_path.to_string(),
23
+ Some("Parent traversal is not allowed.".to_string()),
24
+ );
25
+ }
26
+ let normalized = raw_path
27
+ .split('/')
28
+ .filter(|part| !part.is_empty() && *part != ".")
29
+ .collect::<Vec<_>>()
30
+ .join("/");
31
+ if normalized.is_empty() {
32
+ return (
33
+ raw_path.to_string(),
34
+ Some("Path must not be empty.".to_string()),
35
+ );
36
+ }
37
+ (normalized, None)
38
+ }
39
+
40
+ pub(super) fn is_control_path(path: &str) -> bool {
41
+ path == ".naome" || path.starts_with(".naome/") || path == ".naomeignore"
42
+ }
43
+
44
+ pub(super) fn is_ignored(root: &Path, path: &str) -> bool {
45
+ path_matches_any(path, &naomeignore_patterns(root))
46
+ }
47
+
48
+ pub(super) fn edit_finding(id: &str, message: &str, path: &str) -> serde_json::Value {
49
+ json!({
50
+ "id": id,
51
+ "severity": "error",
52
+ "message": message,
53
+ "path": path,
54
+ "suggestedFix": "Use task request-scope for legitimate scope changes or choose an allowed path.",
55
+ "agentInstruction": "Do not edit this path in the current task."
56
+ })
57
+ }
@@ -0,0 +1,166 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+ use std::fs;
3
+ use std::path::Path;
4
+
5
+ use naome_core::TaskProofStatus;
6
+ use serde_json::{json, Value};
7
+
8
+ use crate::task_commands::check_run;
9
+
10
+ use super::impact::impact_kind;
11
+
12
+ #[derive(Debug, Clone, PartialEq, Eq)]
13
+ pub(super) struct VerificationCheck {
14
+ id: String,
15
+ command: String,
16
+ cwd: String,
17
+ }
18
+
19
+ pub(in crate::task_commands) fn changed_paths(root: &Path) -> Vec<String> {
20
+ check_run::changed_paths(root).unwrap_or_default()
21
+ }
22
+
23
+ fn read_verification_checks(root: &Path) -> Vec<VerificationCheck> {
24
+ let Ok(content) = fs::read_to_string(root.join(".naome/verification.json")) else {
25
+ return Vec::new();
26
+ };
27
+ let Ok(value) = serde_json::from_str::<Value>(&content) else {
28
+ return Vec::new();
29
+ };
30
+ let mut checks = value
31
+ .get("checks")
32
+ .and_then(Value::as_array)
33
+ .into_iter()
34
+ .flatten()
35
+ .filter_map(|check| {
36
+ Some(VerificationCheck {
37
+ id: check.get("id")?.as_str()?.to_string(),
38
+ command: check.get("command")?.as_str()?.to_string(),
39
+ cwd: check.get("cwd")?.as_str()?.to_string(),
40
+ })
41
+ })
42
+ .collect::<Vec<_>>();
43
+ checks.sort_by(|left, right| left.id.cmp(&right.id));
44
+ checks
45
+ }
46
+
47
+ pub(in crate::task_commands) fn planned_commands(
48
+ root: &Path,
49
+ paths: &[String],
50
+ proof: Option<&TaskProofStatus>,
51
+ ) -> Vec<Value> {
52
+ let mut selected = BTreeMap::<String, (VerificationCheck, BTreeSet<String>, String)>::new();
53
+ for check in read_verification_checks(root) {
54
+ let reasons = check_reasons(&check, paths, proof);
55
+ if reasons.is_empty() {
56
+ continue;
57
+ }
58
+ let proof_reason = proof_reason(&reasons).to_string();
59
+ selected.insert(check.id.clone(), (check, reasons, proof_reason));
60
+ }
61
+ selected
62
+ .into_values()
63
+ .map(|(check, reasons, proof_reason)| command_value(check, reasons, proof_reason, paths))
64
+ .collect()
65
+ }
66
+
67
+ fn check_reasons(
68
+ check: &VerificationCheck,
69
+ paths: &[String],
70
+ proof: Option<&TaskProofStatus>,
71
+ ) -> BTreeSet<String> {
72
+ let mut reasons = BTreeSet::new();
73
+ if let Some(proof) = proof {
74
+ if proof.missing_checks.contains(&check.id) {
75
+ reasons.insert("missing-proof".to_string());
76
+ }
77
+ if proof.stale_checks.contains(&check.id) {
78
+ reasons.insert("stale-proof".to_string());
79
+ }
80
+ }
81
+ for path in paths {
82
+ if check_is_impacted(check, path) {
83
+ reasons.insert(impact_kind(path).to_string());
84
+ }
85
+ }
86
+ reasons
87
+ }
88
+
89
+ fn command_value(
90
+ check: VerificationCheck,
91
+ reasons: BTreeSet<String>,
92
+ proof_reason: String,
93
+ paths: &[String],
94
+ ) -> Value {
95
+ let selection_reason = reasons.into_iter().collect::<Vec<_>>().join(",");
96
+ json!({
97
+ "checkId": check.id,
98
+ "command": check.command,
99
+ "cwd": check.cwd,
100
+ "reason": selection_reason,
101
+ "selectionReason": selection_reason,
102
+ "impactedPaths": paths,
103
+ "proofReason": proof_reason,
104
+ "safeToExecute": safe_to_execute(&check, paths)
105
+ })
106
+ }
107
+
108
+ fn proof_reason(reasons: &BTreeSet<String>) -> &'static str {
109
+ if reasons.contains("missing-proof") {
110
+ "missing"
111
+ } else if reasons.contains("stale-proof") {
112
+ "stale"
113
+ } else if reasons.contains("final_only") || reasons.contains("release_only") {
114
+ "final"
115
+ } else {
116
+ "impacted"
117
+ }
118
+ }
119
+
120
+ fn check_is_impacted(check: &VerificationCheck, path: &str) -> bool {
121
+ let command = check.command.as_str();
122
+ match impact_kind(path) {
123
+ "changed_task_state" => {
124
+ command.contains("check-task-state") || command.contains("test:task-state")
125
+ }
126
+ "changed_architecture_config" => command.contains("arch validate"),
127
+ "changed_package_metadata" => {
128
+ command.contains("pack:dry-run") || command.contains("test:decision-engine")
129
+ }
130
+ "changed_native_binary" | "changed_templates" => {
131
+ command.contains("check-harness-health") || command.contains("pack:dry-run")
132
+ }
133
+ "changed_docs" => command.contains("quality check") || command.contains("semantic check"),
134
+ "changed_tests" => {
135
+ command.contains("quality check")
136
+ || command.contains("semantic check")
137
+ || command.contains("test:")
138
+ }
139
+ _ => {
140
+ command == "git diff --check"
141
+ || command.contains("quality check")
142
+ || command.contains("semantic check")
143
+ || command.contains("arch validate")
144
+ }
145
+ }
146
+ }
147
+
148
+ fn safe_to_execute(check: &VerificationCheck, paths: &[String]) -> bool {
149
+ if check.cwd != "." {
150
+ return false;
151
+ }
152
+ match check.command.as_str() {
153
+ "git diff --check"
154
+ | "node .naome/bin/check-harness-health.js"
155
+ | "node .naome/bin/check-task-state.js"
156
+ | "node .naome/bin/naome.js quality check --changed"
157
+ | "node .naome/bin/naome.js semantic check --changed"
158
+ | "node .naome/bin/naome.js arch validate --changed-only" => true,
159
+ "npm run check:task-state" | "npm run test:task-state" | "npm run test:decision-engine" => {
160
+ !paths
161
+ .iter()
162
+ .any(|path| path == "package.json" || path == "packages/naome/package.json")
163
+ }
164
+ _ => false,
165
+ }
166
+ }
@@ -0,0 +1,35 @@
1
+ pub(in crate::task_commands) fn impact_kind(path: &str) -> &'static str {
2
+ if path == ".naome/task-state.json" {
3
+ "changed_task_state"
4
+ } else if path == "naome.arch.yaml" || path.ends_with("/naome.arch.yaml") {
5
+ "changed_architecture_config"
6
+ } else if path == "package.json"
7
+ || path.ends_with("/package.json")
8
+ || path.ends_with("Cargo.toml")
9
+ || path.ends_with("Cargo.lock")
10
+ {
11
+ "changed_package_metadata"
12
+ } else if path.contains("/native/") || path.ends_with(".node") || path.ends_with(".so") {
13
+ "changed_native_binary"
14
+ } else if path.contains("/templates/") || path.starts_with("templates/") {
15
+ "changed_templates"
16
+ } else if path.ends_with(".md") || path.starts_with("docs/") {
17
+ "changed_docs"
18
+ } else if path.contains("test") || path.contains("spec") {
19
+ "changed_tests"
20
+ } else {
21
+ "changed_source"
22
+ }
23
+ }
24
+
25
+ pub(in crate::task_commands) fn path_risk(path: &str) -> &'static str {
26
+ match impact_kind(path) {
27
+ "changed_package_metadata"
28
+ | "changed_native_binary"
29
+ | "changed_templates"
30
+ | "changed_task_state"
31
+ | "changed_architecture_config" => "high",
32
+ "changed_source" | "changed_tests" => "medium",
33
+ _ => "low",
34
+ }
35
+ }
@@ -0,0 +1,24 @@
1
+ use serde_json::Value;
2
+
3
+ mod checks;
4
+ mod impact;
5
+
6
+ pub(super) use checks::{changed_paths, planned_commands};
7
+ pub(super) use impact::path_risk;
8
+
9
+ pub(super) fn split_safe_commands(commands: &[Value]) -> (Vec<Value>, Vec<Value>) {
10
+ let mut safe = Vec::new();
11
+ let mut deferred = Vec::new();
12
+ for command in commands {
13
+ if command
14
+ .get("safeToExecute")
15
+ .and_then(Value::as_bool)
16
+ .unwrap_or(false)
17
+ {
18
+ safe.push(command.clone());
19
+ } else {
20
+ deferred.push(command.clone());
21
+ }
22
+ }
23
+ (safe, deferred)
24
+ }
@@ -0,0 +1,208 @@
1
+ use std::path::Path;
2
+
3
+ use naome_core::{path_matches_any, task_status_report, TaskStatusReportV1};
4
+ use serde_json::{json, Value};
5
+
6
+ use super::common::{agent_session, print_json_with_session, repeated_values};
7
+ use super::path_policy::{is_control_path, is_ignored, normalize_requested_path};
8
+ use super::planner;
9
+
10
+ pub(super) fn preflight(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
11
+ let session = agent_session(args)?;
12
+ let paths = requested_paths(root, args);
13
+ let status = task_status_report(root)?;
14
+ let mut findings = target_findings(selector_supplied(args));
15
+ let mut must_not_edit = Vec::new();
16
+ let mut normalized_paths = Vec::new();
17
+ let path_reports = path_reports(
18
+ root,
19
+ &paths,
20
+ &status,
21
+ &mut findings,
22
+ &mut must_not_edit,
23
+ &mut normalized_paths,
24
+ );
25
+ normalized_paths.sort();
26
+ normalized_paths.dedup();
27
+ must_not_edit.sort();
28
+ must_not_edit.dedup();
29
+ let recommended = planner::planned_commands(root, &normalized_paths, Some(&status.proof));
30
+ let (safe, _deferred) = planner::split_safe_commands(&recommended);
31
+ let blocked = !findings.is_empty() || !must_not_edit.is_empty();
32
+ print_json_with_session(
33
+ json!({
34
+ "schema": "naome.task.preflight.v1",
35
+ "paths": path_reports,
36
+ "recommendedCommands": recommended,
37
+ "safeCommands": safe,
38
+ "mustNotEdit": must_not_edit,
39
+ "expectedProofPathSet": normalized_paths,
40
+ "findings": findings,
41
+ "nextAction": {
42
+ "type": if blocked { "blocked" } else { "edit" },
43
+ "reason": if blocked { "Preflight is blocked until every requested path is explicit and editable." } else { "All requested paths are editable." },
44
+ "paths": must_not_edit,
45
+ "commands": [],
46
+ "checkIds": [],
47
+ "safeToExecute": !blocked,
48
+ "requiresUserApproval": blocked
49
+ }
50
+ }),
51
+ session.as_deref(),
52
+ )
53
+ }
54
+
55
+ fn requested_paths(root: &Path, args: &[String]) -> Vec<String> {
56
+ let mut paths = if args.iter().any(|arg| arg == "--from-changed") {
57
+ planner::changed_paths(root)
58
+ } else {
59
+ repeated_values(args, "--path")
60
+ };
61
+ paths.sort();
62
+ paths.dedup();
63
+ paths
64
+ }
65
+
66
+ fn selector_supplied(args: &[String]) -> bool {
67
+ args.iter()
68
+ .any(|arg| arg == "--from-changed" || arg == "--path")
69
+ }
70
+
71
+ fn target_findings(selector_supplied: bool) -> Vec<Value> {
72
+ if !selector_supplied {
73
+ vec![finding(
74
+ "task.preflight.missing_target_paths",
75
+ "Preflight requires --from-changed or at least one --path.",
76
+ "",
77
+ )]
78
+ } else {
79
+ Vec::new()
80
+ }
81
+ }
82
+
83
+ fn path_reports(
84
+ root: &Path,
85
+ paths: &[String],
86
+ status: &TaskStatusReportV1,
87
+ findings: &mut Vec<Value>,
88
+ must_not_edit: &mut Vec<String>,
89
+ normalized_paths: &mut Vec<String>,
90
+ ) -> Vec<Value> {
91
+ paths
92
+ .iter()
93
+ .map(|raw_path| {
94
+ path_report(
95
+ root,
96
+ raw_path,
97
+ status,
98
+ findings,
99
+ must_not_edit,
100
+ normalized_paths,
101
+ )
102
+ })
103
+ .collect()
104
+ }
105
+
106
+ fn path_report(
107
+ root: &Path,
108
+ raw_path: &str,
109
+ status: &TaskStatusReportV1,
110
+ findings: &mut Vec<Value>,
111
+ must_not_edit: &mut Vec<String>,
112
+ normalized_paths: &mut Vec<String>,
113
+ ) -> Value {
114
+ let (path, path_error) = normalize_requested_path(raw_path);
115
+ let ignored = path_error.is_none() && is_ignored(root, &path);
116
+ let control = path_error.is_none() && is_control_path(&path);
117
+ let in_scope = path_error.is_none() && path_matches_any(&path, &status.scope.allowed_paths);
118
+ let editable = path_editable(status, path_error.is_none(), ignored, control, in_scope);
119
+ track_path_report(&path, editable, must_not_edit, normalized_paths);
120
+ add_path_finding(&path, path_error, ignored, control, in_scope, findings);
121
+ let commands =
122
+ planner::planned_commands(root, std::slice::from_ref(&path), Some(&status.proof));
123
+ json!({
124
+ "path": path,
125
+ "editable": editable,
126
+ "reason": if editable { "Path is editable for the active task." } else { "Path is not editable without scope or state repair." },
127
+ "ignored": ignored,
128
+ "controlPath": control,
129
+ "inScope": in_scope,
130
+ "impactedChecks": commands.iter().filter_map(|command| command.get("checkId").and_then(Value::as_str).map(ToString::to_string)).collect::<Vec<_>>(),
131
+ "requiredBeforeCommit": commands,
132
+ "risk": planner::path_risk(raw_path),
133
+ "agentInstruction": if editable { "Agent may edit this path, then rerun task agent-snapshot or task loop." } else { "Do not edit this path in the current task." }
134
+ })
135
+ }
136
+
137
+ fn path_editable(
138
+ status: &TaskStatusReportV1,
139
+ valid_path: bool,
140
+ ignored: bool,
141
+ control: bool,
142
+ in_scope: bool,
143
+ ) -> bool {
144
+ valid_path
145
+ && !ignored
146
+ && !control
147
+ && in_scope
148
+ && status.task_id.is_some()
149
+ && !matches!(
150
+ status.state.as_str(),
151
+ "idle" | "missing" | "complete" | "blocked" | "needs_human_review"
152
+ )
153
+ }
154
+
155
+ fn track_path_report(
156
+ path: &str,
157
+ editable: bool,
158
+ must_not_edit: &mut Vec<String>,
159
+ normalized_paths: &mut Vec<String>,
160
+ ) {
161
+ if editable {
162
+ normalized_paths.push(path.to_string());
163
+ } else {
164
+ must_not_edit.push(path.to_string());
165
+ }
166
+ }
167
+
168
+ fn add_path_finding(
169
+ path: &str,
170
+ path_error: Option<String>,
171
+ ignored: bool,
172
+ control: bool,
173
+ in_scope: bool,
174
+ findings: &mut Vec<Value>,
175
+ ) {
176
+ if let Some(message) = path_error {
177
+ findings.push(finding("task.preflight.unsafe_path", &message, path));
178
+ } else if ignored {
179
+ findings.push(finding(
180
+ "task.preflight.ignored_path",
181
+ "Path is ignored by .naomeignore.",
182
+ path,
183
+ ));
184
+ } else if control {
185
+ findings.push(finding(
186
+ "task.preflight.control_path",
187
+ "Path is a NAOME control path.",
188
+ path,
189
+ ));
190
+ } else if !in_scope {
191
+ findings.push(finding(
192
+ "task.preflight.out_of_scope",
193
+ "Path is outside active task scope.",
194
+ path,
195
+ ));
196
+ }
197
+ }
198
+
199
+ fn finding(id: &str, message: &str, path: &str) -> Value {
200
+ json!({
201
+ "id": id,
202
+ "severity": "error",
203
+ "message": message,
204
+ "path": path,
205
+ "suggestedFix": "Use an in-scope non-control path or request explicit scope revision.",
206
+ "agentInstruction": "Do not edit this path unless a human-approved task scope revision allows it."
207
+ })
208
+ }
@@ -4,20 +4,24 @@ use std::path::Path;
4
4
  use naome_core::{task_status_report, task_transition_readiness};
5
5
  use serde_json::json;
6
6
 
7
- use super::common::print_json;
7
+ use super::common::{agent_session, print_json_with_session};
8
8
 
9
- pub(super) fn can_commit(root: &Path, _args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
9
+ pub(super) fn can_commit(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
10
+ let session = agent_session(args)?;
10
11
  let status = task_status_report(root)?;
11
12
  let transition = task_transition_readiness(root, "complete")?;
12
13
  let required = commit_requirements(&status, &transition.blocking_findings);
13
- print_json(json!({
14
- "schema": "naome.task.commit-readiness.v1",
15
- "allowed": transition.allowed && status.agent_loop.can_commit && required.is_empty(),
16
- "commitPaths": status.scope.in_scope_changed_paths,
17
- "blockingFindings": transition.blocking_findings,
18
- "requiredBeforeCommit": required,
19
- "agentLoop": status.agent_loop
20
- }))
14
+ print_json_with_session(
15
+ json!({
16
+ "schema": "naome.task.commit-readiness.v1",
17
+ "allowed": transition.allowed && status.agent_loop.can_commit && required.is_empty(),
18
+ "commitPaths": status.scope.in_scope_changed_paths,
19
+ "blockingFindings": transition.blocking_findings,
20
+ "requiredBeforeCommit": required,
21
+ "agentLoop": status.agent_loop
22
+ }),
23
+ session.as_deref(),
24
+ )
21
25
  }
22
26
 
23
27
  fn commit_requirements(