@lamentis/naome 1.4.0 → 1.4.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 (64) 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 +13 -0
  5. package/crates/naome-cli/src/task_commands/can_edit.rs +116 -0
  6. package/crates/naome-cli/src/task_commands/check_run/output.rs +34 -0
  7. package/crates/naome-cli/src/task_commands/check_run/receipts.rs +155 -0
  8. package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
  9. package/crates/naome-cli/src/task_commands/check_run.rs +192 -0
  10. package/crates/naome-cli/src/task_commands/common.rs +70 -0
  11. package/crates/naome-cli/src/task_commands/complete.rs +43 -0
  12. package/crates/naome-cli/src/task_commands/loop_control.rs +55 -0
  13. package/crates/naome-cli/src/task_commands/readiness.rs +44 -0
  14. package/crates/naome-cli/src/task_commands/record.rs +236 -0
  15. package/crates/naome-cli/src/task_commands/repair.rs +77 -0
  16. package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
  17. package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
  18. package/crates/naome-cli/src/task_commands.rs +80 -1
  19. package/crates/naome-cli/tests/task_cli.rs +58 -0
  20. package/crates/naome-cli/tests/task_cli_agent_controls.rs +210 -0
  21. package/crates/naome-cli/tests/task_cli_control.rs +126 -0
  22. package/crates/naome-cli/tests/task_cli_loop.rs +383 -0
  23. package/crates/naome-cli/tests/task_cli_loop_edit.rs +144 -0
  24. package/crates/naome-cli/tests/task_cli_support/mod.rs +178 -0
  25. package/crates/naome-core/Cargo.toml +1 -1
  26. package/crates/naome-core/src/lib.rs +7 -2
  27. package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
  28. package/crates/naome-core/src/task_state/mod.rs +12 -0
  29. package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
  30. package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
  31. package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
  32. package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
  33. package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
  34. package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
  35. package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
  36. package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
  37. package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
  38. package/crates/naome-core/src/task_state/status/control.rs +16 -0
  39. package/crates/naome-core/src/task_state/status/git.rs +133 -0
  40. package/crates/naome-core/src/task_state/status/model.rs +152 -0
  41. package/crates/naome-core/src/task_state/status/proof.rs +217 -0
  42. package/crates/naome-core/src/task_state/status/proof_read.rs +164 -0
  43. package/crates/naome-core/src/task_state/status/report.rs +148 -0
  44. package/crates/naome-core/src/task_state/status/report_context.rs +148 -0
  45. package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
  46. package/crates/naome-core/src/task_state/status/scope.rs +111 -0
  47. package/crates/naome-core/src/task_state/status/transition.rs +101 -0
  48. package/crates/naome-core/src/task_state/status.rs +23 -0
  49. package/crates/naome-core/src/task_state/status_output.rs +103 -0
  50. package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
  51. package/crates/naome-core/tests/task_state_support/states.rs +4 -0
  52. package/crates/naome-core/tests/task_status.rs +423 -0
  53. package/crates/naome-core/tests/task_status_git.rs +141 -0
  54. package/installer/context.js +1 -1
  55. package/installer/harness-verification.js +2 -6
  56. package/installer/manifest-state.js +2 -2
  57. package/installer/native.js +3 -31
  58. package/native/darwin-arm64/naome +0 -0
  59. package/native/linux-x64/naome +0 -0
  60. package/package.json +1 -1
  61. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  62. package/templates/naome-root/.naome/bin/check-task-state.js +4 -39
  63. package/templates/naome-root/.naome/bin/naome.js +2 -30
  64. package/templates/naome-root/.naome/manifest.json +2 -2
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.4.0"
79
+ version = "1.4.2"
80
80
  dependencies = [
81
81
  "naome-core",
82
82
  "serde_json",
@@ -84,7 +84,7 @@ dependencies = [
84
84
 
85
85
  [[package]]
86
86
  name = "naome-core"
87
- version = "1.4.0"
87
+ version = "1.4.2"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
package/README.md CHANGED
@@ -6,146 +6,41 @@
6
6
  <h1 align="center">NAOME</h1>
7
7
 
8
8
  <p align="center">
9
- A deterministic repository harness for AI coding agents.
9
+ The repository harness for long-running Codex work.
10
10
  </p>
11
11
 
12
- ```shell
13
- npm install -g @lamentis/naome
14
- ```
12
+ NAOME wraps your repository with deterministic controls and workflows so
13
+ agentic AI systems can take on real software work: longer tasks, safer
14
+ iterations, and eventually more autonomous execution.
15
15
 
16
- NAOME gives coding agents a repository-local operating protocol: what to read,
17
- what to ignore, how to admit a task, which files are in scope, which checks are
18
- required, and when work is safe to commit.
16
+ You install the harness. Codex and NAOME handle the workflow.
19
17
 
20
- ## Quickstart
18
+ ## Start
21
19
 
22
- Install the CLI, then sync NAOME into a repository:
20
+ Install the Lamentis NAOME package:
23
21
 
24
22
  ```shell
25
23
  npm install -g @lamentis/naome
26
- cd /path/to/repo
27
- naome sync
28
- ```
29
-
30
- For an initialized repository, start with:
31
-
32
- ```shell
33
- naome status
34
- naome next
35
- naome doctor
36
24
  ```
37
25
 
38
- For agent-driven work, route the user's request through the harness:
39
-
40
- ```shell
41
- naome route --prompt-file /path/to/prompt.txt --execute --json
42
- ```
43
-
44
- Prompt files should start with a fenced `naome-prompt-envelope-v1` JSON
45
- envelope that normalizes the raw user text into canonical routing fields. A raw
46
- natural-language prompt routes to a non-mutating normalization decision instead
47
- of becoming a task by keyword inference.
48
-
49
- ## Why NAOME?
50
-
51
- - Keeps agents inside explicit task scope.
52
- - Blocks unowned diffs before new work starts.
53
- - Separates current task work from repository cleanup debt.
54
- - Runs changed-code quality gates without forcing legacy repositories to be
55
- perfect on day one.
56
- - Records verification proof before a task can be treated as complete.
57
- - Keeps sync fast by making baseline and deep quality scans explicit.
58
-
59
- ## Safety Model
60
-
61
- NAOME is repository-local. The files under `.naome/`, `.naomeignore`, and
62
- `docs/naome/` define the local harness contract for a repository.
63
-
64
- The harness enforces read boundaries, task admission, scope drift checks,
65
- repository-quality policy, verification phases, and commit gates. Existing debt
66
- is reportable through cleanup flows, while changed files are held to the active
67
- policy.
68
-
69
- ## CLI Reference
70
-
71
- Common commands:
26
+ Sync NAOME into your repository:
72
27
 
73
28
  ```shell
29
+ cd /path/to/your/repo
74
30
  naome sync
75
- naome update
76
- naome status
77
- naome next
78
- naome doctor
79
- naome route --prompt-file /path/to/prompt.txt --execute --json
80
- naome quality init
81
- naome quality init --baseline
82
- naome quality report
83
- naome quality report --deep
84
- naome quality check --changed
85
- naome semantic check --changed
86
- naome arch validate --changed-only
87
- naome task render-state --write --json
88
- naome commit -m "type(scope): summary"
89
31
  ```
90
32
 
91
- Architecture fitness builds a language-agnostic graph, extracts direct imports
92
- for TypeScript, JavaScript, Rust, Python, and Go, and can enforce configured
93
- layer dependency rules such as keeping domain code independent from
94
- infrastructure adapters.
95
-
96
- `naome sync` installs or repairs the local harness files. It does not run a
97
- hidden full-repository quality scan. It also migrates any active legacy
98
- task-state into the local task ledger automatically and untracks local
99
- `.naome/tasks/` runtime folders from Git. If quality policy is newly seeded,
100
- run `naome quality init --baseline` deliberately; use `--deep` or
101
- `--deep-baseline` only when you want expensive repository-wide checks.
102
-
103
- ## Repository Docs
104
-
105
- After sync, NAOME writes the agent-facing workflow into `docs/naome/`:
106
-
107
- - `docs/naome/index.md` is the entry point.
108
- - `docs/naome/agent-workflow.md` explains the active task workflow.
109
- - `docs/naome/testing.md` maps change types to required checks.
110
- - `docs/naome/repository-quality.md` explains quality, structure, and cleanup
111
- policy.
112
- - `docs/naome/architecture-fitness.md` explains architecture graph validation
113
- and agent feedback.
114
-
115
- Agents should follow the repository's NAOME docs instead of guessing workflow
116
- rules from generic project files.
33
+ Open the repository in Codex and give the agent a normal task:
117
34
 
118
- ## Configuration
119
-
120
- The main local policy files are:
121
-
122
- - `.naomeignore` for read boundaries.
123
- - `.naome/verification.json` for check phases and proof requirements.
124
- - `.naome/repository-quality.json` for file, symbol, duplicate, and semantic
125
- quality policy.
126
- - `.naome/repository-structure.json` for path role, module, and directory
127
- structure policy.
128
- - `naome.arch.yaml` for language-agnostic architecture fitness rules.
129
- - `.naome/task-state.json` for the compact committed active task projection.
130
-
131
- Product defaults stay generic. Repository-specific policy belongs in the local
132
- `.naome/` config files.
133
-
134
- ## Development
135
-
136
- Useful checks for this repository:
137
-
138
- ```shell
139
- npm run build:rust
140
- npm run test:decision-engine
141
- npm run test:naome-installer
142
- npm run pack:dry-run
143
- node .naome/bin/naome.js quality check --changed --json
144
- node .naome/bin/naome.js semantic check --changed --json
145
- node .naome/bin/naome.js arch validate --changed-only --json
146
- git diff --check
35
+ ```text
36
+ Use NAOME in this repository and implement the next change.
147
37
  ```
148
38
 
39
+ That is the product flow. You do not need to learn NAOME commands, route
40
+ prompts, select context, or manage task state. Codex follows the repository
41
+ harness, and NAOME supplies the deterministic checks, boundaries, task memory,
42
+ and verification flow behind the scenes.
43
+
149
44
  ## License
150
45
 
151
46
  NAOME is licensed under the [Apache License 2.0](../../LICENSE).
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.4.0"
3
+ version = "1.4.2"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -37,6 +37,19 @@ const HELP: &str = r#"Usage:
37
37
  naome sync [--package-root <path>] [--installer-js <path>]
38
38
  naome install-plan [--harness-version <version>]
39
39
  naome seed-verification
40
+ naome task status [--json] [--exit-code] [--agent-session <id>]
41
+ naome task proof-plan [--json] [--exit-code] [--agent-session <id>]
42
+ naome task can-edit --path <path> --json [--agent-session <id>]
43
+ naome task run-check --check <check-id> [--record-proof] --json [--agent-session <id>]
44
+ naome task loop [--execute-safe] --json [--agent-session <id>]
45
+ naome task can-transition --to complete [--json] [--agent-session <id>]
46
+ naome task can-commit --json
47
+ naome task complete --from-can-transition --json [--agent-session <id>]
48
+ naome task repair --plan <id> (--dry-run|--execute-safe) --json [--agent-session <id>]
49
+ naome task record-proof --from-proof-plan [--dry-run] --json [--agent-session <id>]
50
+ naome task request-scope --path <path> --reason <reason> --json
51
+ naome task timeline --json
52
+ naome task loop-snapshot --json
40
53
  naome task render-state [--write] [--json]
41
54
  naome task migrate-ledger [--write] [--json]
42
55
  naome refresh-integrity [--root <path>] [--json]
@@ -0,0 +1,116 @@
1
+ use std::path::Path;
2
+
3
+ use naome_core::{naomeignore_patterns, path_matches_any, task_status_report};
4
+ use serde_json::json;
5
+
6
+ use super::common::{agent_session, print_json_with_session, value_after};
7
+
8
+ pub(super) fn can_edit(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
9
+ let session = agent_session(args)?;
10
+ let Some(raw_path) = value_after(args, "--path") else {
11
+ return Err("naome task can-edit requires --path <path>".into());
12
+ };
13
+ let (path, path_error) = normalize_requested_path(raw_path);
14
+ let mut findings = Vec::new();
15
+ if let Some(message) = path_error {
16
+ findings.push(finding("task.edit.unsafe_path", &message, raw_path));
17
+ } else if is_ignored(root, &path) {
18
+ findings.push(finding(
19
+ "task.edit.ignored_path",
20
+ "Path is ignored by .naomeignore and is outside the active harness context.",
21
+ &path,
22
+ ));
23
+ } else if is_control_path(&path) {
24
+ findings.push(finding(
25
+ "task.edit.control_path",
26
+ "NAOME control files cannot be edited through the autonomous can-edit path.",
27
+ &path,
28
+ ));
29
+ } else {
30
+ let status = task_status_report(root)?;
31
+ if status.task_id.is_none()
32
+ || matches!(
33
+ status.state.as_str(),
34
+ "idle" | "missing" | "complete" | "blocked" | "needs_human_review"
35
+ )
36
+ {
37
+ findings.push(finding(
38
+ "task.edit.no_active_task",
39
+ "No editable active task is available for this path.",
40
+ &path,
41
+ ));
42
+ } else if !path_matches_any(&path, &status.scope.allowed_paths) {
43
+ findings.push(finding(
44
+ "task.edit.out_of_scope",
45
+ "Path is outside activeTask.allowedPaths.",
46
+ &path,
47
+ ));
48
+ }
49
+ }
50
+
51
+ let allowed = findings.is_empty();
52
+ print_json_with_session(
53
+ json!({
54
+ "schema": "naome.task.can-edit.v1",
55
+ "path": path,
56
+ "allowed": allowed,
57
+ "reason": if allowed { "Path is inside active task scope." } else { "Path is not safe to edit for the active task." },
58
+ "findings": findings,
59
+ "agentInstruction": if allowed { "Agent may edit this path and must rerun task loop before completion." } else { "Do not edit this path unless task scope is explicitly revised." }
60
+ }),
61
+ session.as_deref(),
62
+ )
63
+ }
64
+
65
+ fn normalize_requested_path(raw_path: &str) -> (String, Option<String>) {
66
+ if raw_path.contains('\\') {
67
+ return (
68
+ raw_path.to_string(),
69
+ Some("Backslash path separators are not allowed.".to_string()),
70
+ );
71
+ }
72
+ let path = Path::new(raw_path);
73
+ if path.is_absolute() {
74
+ return (
75
+ raw_path.to_string(),
76
+ Some("Absolute paths are not allowed.".to_string()),
77
+ );
78
+ }
79
+ if raw_path.split('/').any(|part| part == "..") {
80
+ return (
81
+ raw_path.to_string(),
82
+ Some("Parent traversal is not allowed.".to_string()),
83
+ );
84
+ }
85
+ let normalized = raw_path
86
+ .split('/')
87
+ .filter(|part| !part.is_empty() && *part != ".")
88
+ .collect::<Vec<_>>()
89
+ .join("/");
90
+ if normalized.is_empty() {
91
+ return (
92
+ raw_path.to_string(),
93
+ Some("Path must not be empty.".to_string()),
94
+ );
95
+ }
96
+ (normalized, None)
97
+ }
98
+
99
+ fn is_control_path(path: &str) -> bool {
100
+ path == ".naome" || path.starts_with(".naome/") || path == ".naomeignore"
101
+ }
102
+
103
+ fn is_ignored(root: &Path, path: &str) -> bool {
104
+ path_matches_any(path, &naomeignore_patterns(root))
105
+ }
106
+
107
+ fn finding(id: &str, message: &str, path: &str) -> serde_json::Value {
108
+ json!({
109
+ "id": id,
110
+ "severity": "error",
111
+ "message": message,
112
+ "path": path,
113
+ "suggestedFix": "Use task request-scope for legitimate scope changes or choose an allowed path.",
114
+ "agentInstruction": "Do not edit this path in the current task."
115
+ })
116
+ }
@@ -0,0 +1,34 @@
1
+ use serde_json::{json, Value};
2
+
3
+ pub(super) fn run_check_response(
4
+ check_id: &str,
5
+ executed: bool,
6
+ exit_code: Option<i32>,
7
+ recorded_proof: bool,
8
+ findings: Vec<Value>,
9
+ instruction: &str,
10
+ session: Option<&str>,
11
+ ) -> Value {
12
+ json!({
13
+ "schema": "naome.task.run-check.v1",
14
+ "checkId": check_id,
15
+ "executed": executed,
16
+ "exitCode": exit_code,
17
+ "recordedProof": recorded_proof,
18
+ "summary": instruction,
19
+ "findings": findings,
20
+ "agentInstruction": instruction,
21
+ "agentSession": session
22
+ })
23
+ }
24
+
25
+ pub(super) fn finding(id: &str, message: impl Into<String>) -> Value {
26
+ json!({
27
+ "id": id,
28
+ "severity": "error",
29
+ "message": message.into(),
30
+ "path": null,
31
+ "suggestedFix": "Use task proof-plan and declared safe checks before recording proof.",
32
+ "agentInstruction": "Do not record proof for failed, unknown, or unsafe checks."
33
+ })
34
+ }
@@ -0,0 +1,155 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+
5
+ use naome_core::task_evidence_fingerprint;
6
+ use serde_json::{json, Value};
7
+
8
+ const RECEIPTS_PATH: &str = "naome-task-check-runs.json";
9
+
10
+ #[derive(Debug, Clone)]
11
+ pub(in crate::task_commands) struct CheckRunReceipt {
12
+ pub(in crate::task_commands) check_id: String,
13
+ pub(in crate::task_commands) command: String,
14
+ pub(in crate::task_commands) cwd: String,
15
+ pub(in crate::task_commands) exit_code: i32,
16
+ pub(in crate::task_commands) checked_at: String,
17
+ pub(in crate::task_commands) evidence_paths: Vec<String>,
18
+ pub(in crate::task_commands) evidence_fingerprint: String,
19
+ pub(in crate::task_commands) stdout_summary: String,
20
+ pub(in crate::task_commands) stderr_summary: String,
21
+ pub(in crate::task_commands) duration_ms: u128,
22
+ pub(in crate::task_commands) agent_session: Option<String>,
23
+ }
24
+
25
+ pub(in crate::task_commands) fn successful_receipts(
26
+ root: &Path,
27
+ ) -> Result<Vec<CheckRunReceipt>, Box<dyn std::error::Error>> {
28
+ let path = receipt_path(root)?;
29
+ let Ok(content) = fs::read_to_string(path) else {
30
+ return Ok(Vec::new());
31
+ };
32
+ let value: Value = serde_json::from_str(&content)?;
33
+ Ok(value
34
+ .as_array()
35
+ .into_iter()
36
+ .flatten()
37
+ .filter_map(receipt_from_value)
38
+ .filter(|receipt| receipt.exit_code == 0)
39
+ .collect())
40
+ }
41
+
42
+ pub(super) fn append_receipt(
43
+ root: &Path,
44
+ receipt: &CheckRunReceipt,
45
+ ) -> Result<(), Box<dyn std::error::Error>> {
46
+ let path = receipt_path(root)?;
47
+ if let Some(parent) = path.parent() {
48
+ fs::create_dir_all(parent)?;
49
+ }
50
+ let mut receipts = if path.exists() {
51
+ serde_json::from_str::<Value>(&fs::read_to_string(&path)?)?
52
+ .as_array()
53
+ .cloned()
54
+ .unwrap_or_default()
55
+ } else {
56
+ Vec::new()
57
+ };
58
+ receipts.push(receipt_to_value(receipt));
59
+ fs::write(
60
+ path,
61
+ format!("{}\n", serde_json::to_string_pretty(&receipts)?),
62
+ )?;
63
+ Ok(())
64
+ }
65
+
66
+ pub(super) fn changed_paths(root: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> {
67
+ let output = Command::new("git")
68
+ .args(["status", "--porcelain=v1"])
69
+ .current_dir(root)
70
+ .output()?;
71
+ if !output.status.success() {
72
+ return Ok(Vec::new());
73
+ }
74
+ let mut paths = String::from_utf8_lossy(&output.stdout)
75
+ .lines()
76
+ .filter_map(|line| line.get(3..))
77
+ .map(|line| line.split(" -> ").last().unwrap_or(line).to_string())
78
+ .collect::<Vec<_>>();
79
+ paths.sort();
80
+ Ok(paths)
81
+ }
82
+
83
+ fn receipt_path(root: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
84
+ let output = Command::new("git")
85
+ .args(["rev-parse", "--git-path", RECEIPTS_PATH])
86
+ .current_dir(root)
87
+ .output()?;
88
+ if output.status.success() {
89
+ let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
90
+ if !path.is_empty() {
91
+ return Ok(root.join(path));
92
+ }
93
+ }
94
+ Ok(root.join(".git").join(RECEIPTS_PATH))
95
+ }
96
+
97
+ fn receipt_to_value(receipt: &CheckRunReceipt) -> Value {
98
+ json!({
99
+ "checkId": receipt.check_id,
100
+ "command": receipt.command,
101
+ "cwd": receipt.cwd,
102
+ "exitCode": receipt.exit_code,
103
+ "checkedAt": receipt.checked_at,
104
+ "evidencePaths": receipt.evidence_paths,
105
+ "evidenceFingerprint": receipt.evidence_fingerprint,
106
+ "stdoutSummary": receipt.stdout_summary,
107
+ "stderrSummary": receipt.stderr_summary,
108
+ "durationMs": receipt.duration_ms,
109
+ "agentSession": receipt.agent_session
110
+ })
111
+ }
112
+
113
+ fn receipt_from_value(value: &Value) -> Option<CheckRunReceipt> {
114
+ Some(CheckRunReceipt {
115
+ check_id: value.get("checkId")?.as_str()?.to_string(),
116
+ command: value.get("command")?.as_str()?.to_string(),
117
+ cwd: value.get("cwd")?.as_str()?.to_string(),
118
+ exit_code: value.get("exitCode")?.as_i64()? as i32,
119
+ checked_at: value.get("checkedAt")?.as_str()?.to_string(),
120
+ evidence_paths: value
121
+ .get("evidencePaths")?
122
+ .as_array()?
123
+ .iter()
124
+ .filter_map(Value::as_str)
125
+ .map(ToString::to_string)
126
+ .collect(),
127
+ evidence_fingerprint: value
128
+ .get("evidenceFingerprint")
129
+ .and_then(Value::as_str)
130
+ .unwrap_or("")
131
+ .to_string(),
132
+ stdout_summary: value
133
+ .get("stdoutSummary")
134
+ .and_then(Value::as_str)
135
+ .unwrap_or("")
136
+ .to_string(),
137
+ stderr_summary: value
138
+ .get("stderrSummary")
139
+ .and_then(Value::as_str)
140
+ .unwrap_or("")
141
+ .to_string(),
142
+ duration_ms: value.get("durationMs").and_then(Value::as_u64).unwrap_or(0) as u128,
143
+ agent_session: value
144
+ .get("agentSession")
145
+ .and_then(Value::as_str)
146
+ .map(ToString::to_string),
147
+ })
148
+ }
149
+
150
+ pub(in crate::task_commands) fn evidence_fingerprint(
151
+ root: &Path,
152
+ paths: &[String],
153
+ ) -> Result<String, Box<dyn std::error::Error>> {
154
+ Ok(task_evidence_fingerprint(root, paths)?)
155
+ }
@@ -0,0 +1,165 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+ use std::process::Command;
4
+
5
+ use serde_json::Value;
6
+
7
+ #[derive(Debug, Clone)]
8
+ pub(in crate::task_commands) struct VerificationCheck {
9
+ pub(in crate::task_commands) id: String,
10
+ pub(in crate::task_commands) command: String,
11
+ pub(in crate::task_commands) cwd: String,
12
+ }
13
+
14
+ #[derive(Debug, Clone)]
15
+ pub(super) struct SafeCommand {
16
+ pub(super) program: String,
17
+ pub(super) args: Vec<String>,
18
+ }
19
+
20
+ pub(in crate::task_commands) fn read_verification_check(
21
+ root: &Path,
22
+ check_id: &str,
23
+ ) -> Result<Option<VerificationCheck>, Box<dyn std::error::Error>> {
24
+ let content = fs::read_to_string(root.join(".naome/verification.json"))?;
25
+ let verification: Value = serde_json::from_str(&content)?;
26
+ Ok(verification
27
+ .get("checks")
28
+ .and_then(Value::as_array)
29
+ .into_iter()
30
+ .flatten()
31
+ .find(|check| check.get("id").and_then(Value::as_str) == Some(check_id))
32
+ .and_then(|check| {
33
+ Some(VerificationCheck {
34
+ id: check.get("id")?.as_str()?.to_string(),
35
+ command: check.get("command")?.as_str()?.to_string(),
36
+ cwd: check.get("cwd")?.as_str()?.to_string(),
37
+ })
38
+ }))
39
+ }
40
+
41
+ pub(super) fn safe_command(
42
+ root: &Path,
43
+ check: &VerificationCheck,
44
+ ) -> Result<Option<SafeCommand>, Box<dyn std::error::Error>> {
45
+ if check.cwd != "." {
46
+ return Ok(None);
47
+ }
48
+ let changed_paths = changed_paths(root)?;
49
+ match check.command.as_str() {
50
+ "git diff --check" => Ok(Some(command("git", &["diff", "--check"]))),
51
+ "node .naome/bin/check-harness-health.js" => Ok(Some(naome_command(
52
+ root,
53
+ &["check-harness-health", "--root"],
54
+ )?)),
55
+ "node .naome/bin/check-task-state.js" => {
56
+ Ok(Some(naome_command(root, &["check-task-state", "--root"])?))
57
+ }
58
+ "node .naome/bin/naome.js quality check --changed" => Ok(Some(naome_command(
59
+ root,
60
+ &["quality", "check", "--changed"],
61
+ )?)),
62
+ "node .naome/bin/naome.js semantic check --changed" => Ok(Some(naome_command(
63
+ root,
64
+ &["semantic", "check", "--changed"],
65
+ )?)),
66
+ "node .naome/bin/naome.js arch validate --changed-only" => Ok(Some(naome_command(
67
+ root,
68
+ &["arch", "validate", "--changed-only"],
69
+ )?)),
70
+ "npm run check:task-state" => trusted_npm_script(
71
+ root,
72
+ &changed_paths,
73
+ "check:task-state",
74
+ "node scripts/check-task-state.js",
75
+ ),
76
+ "npm run test:task-state" => trusted_npm_script(
77
+ root,
78
+ &changed_paths,
79
+ "test:task-state",
80
+ "node --test scripts/check-task-state.test.js",
81
+ ),
82
+ "npm run test:decision-engine" => trusted_npm_script(
83
+ root,
84
+ &changed_paths,
85
+ "test:decision-engine",
86
+ "cargo test --manifest-path packages/naome/Cargo.toml -p naome-core",
87
+ ),
88
+ _ => Ok(None),
89
+ }
90
+ }
91
+
92
+ fn command(program: &str, args: &[&str]) -> SafeCommand {
93
+ SafeCommand {
94
+ program: program.to_string(),
95
+ args: args.iter().map(|arg| (*arg).to_string()).collect(),
96
+ }
97
+ }
98
+
99
+ fn naome_command(root: &Path, args: &[&str]) -> Result<SafeCommand, Box<dyn std::error::Error>> {
100
+ let mut owned_args = args
101
+ .iter()
102
+ .map(|arg| (*arg).to_string())
103
+ .collect::<Vec<_>>();
104
+ if args.last() == Some(&"--root") {
105
+ owned_args.push(root.to_string_lossy().to_string());
106
+ }
107
+ Ok(SafeCommand {
108
+ program: std::env::current_exe()?.to_string_lossy().to_string(),
109
+ args: owned_args,
110
+ })
111
+ }
112
+
113
+ fn trusted_npm_script(
114
+ root: &Path,
115
+ changed_paths: &[String],
116
+ script: &str,
117
+ expected: &str,
118
+ ) -> Result<Option<SafeCommand>, Box<dyn std::error::Error>> {
119
+ if changed_paths
120
+ .iter()
121
+ .any(|path| path == "package.json" || path == "packages/naome/package.json")
122
+ {
123
+ return Ok(None);
124
+ }
125
+
126
+ let package_json = root.join("package.json");
127
+ let Ok(content) = fs::read_to_string(package_json) else {
128
+ return Ok(None);
129
+ };
130
+ let package: Value = serde_json::from_str(&content)?;
131
+ let actual = package
132
+ .get("scripts")
133
+ .and_then(Value::as_object)
134
+ .and_then(|scripts| scripts.get(script))
135
+ .and_then(Value::as_str);
136
+ if actual != Some(expected) {
137
+ return Ok(None);
138
+ }
139
+
140
+ Ok(Some(command("npm", &["run", script])))
141
+ }
142
+
143
+ fn changed_paths(root: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> {
144
+ let output = Command::new("git")
145
+ .args(["status", "--porcelain=v1", "-z", "--untracked-files=all"])
146
+ .current_dir(root)
147
+ .output()?;
148
+ if !output.status.success() {
149
+ return Ok(Vec::new());
150
+ }
151
+
152
+ let mut paths = Vec::new();
153
+ for entry in output.stdout.split(|byte| *byte == 0) {
154
+ if entry.len() < 4 {
155
+ continue;
156
+ }
157
+ let path = String::from_utf8_lossy(&entry[3..]).replace('\\', "/");
158
+ if !path.is_empty() {
159
+ paths.push(path);
160
+ }
161
+ }
162
+ paths.sort();
163
+ paths.dedup();
164
+ Ok(paths)
165
+ }