@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.
- package/Cargo.lock +2 -2
- package/README.md +17 -122
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/main.rs +14 -5
- package/crates/naome-cli/src/task_commands/agent_snapshot.rs +173 -0
- package/crates/naome-cli/src/task_commands/can_edit.rs +64 -0
- package/crates/naome-cli/src/task_commands/check_run/output.rs +34 -0
- package/crates/naome-cli/src/task_commands/check_run/receipts.rs +163 -0
- package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
- package/crates/naome-cli/src/task_commands/check_run.rs +196 -0
- package/crates/naome-cli/src/task_commands/commit_preflight.rs +89 -0
- package/crates/naome-cli/src/task_commands/common.rs +39 -1
- package/crates/naome-cli/src/task_commands/compact_proof.rs +69 -0
- package/crates/naome-cli/src/task_commands/complete.rs +43 -0
- package/crates/naome-cli/src/task_commands/loop_control.rs +73 -0
- package/crates/naome-cli/src/task_commands/path_policy.rs +57 -0
- package/crates/naome-cli/src/task_commands/planner/checks.rs +166 -0
- package/crates/naome-cli/src/task_commands/planner/impact.rs +35 -0
- package/crates/naome-cli/src/task_commands/planner/mod.rs +24 -0
- package/crates/naome-cli/src/task_commands/preflight.rs +208 -0
- package/crates/naome-cli/src/task_commands/readiness.rs +14 -10
- package/crates/naome-cli/src/task_commands/record.rs +176 -37
- package/crates/naome-cli/src/task_commands/repair.rs +58 -11
- package/crates/naome-cli/src/task_commands/scope_suggestions.rs +109 -0
- package/crates/naome-cli/src/task_commands.rs +26 -3
- package/crates/naome-cli/tests/task_cli_agent_controls.rs +9 -16
- package/crates/naome-cli/tests/task_cli_fast_flow.rs +290 -0
- package/crates/naome-cli/tests/task_cli_loop.rs +383 -0
- package/crates/naome-cli/tests/task_cli_loop_edit.rs +144 -0
- package/crates/naome-cli/tests/task_cli_support/mod.rs +28 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/lib.rs +7 -7
- package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
- package/crates/naome-core/src/task_state/mod.rs +2 -0
- package/crates/naome-core/src/task_state/status/control/repair.rs +2 -2
- package/crates/naome-core/src/task_state/status/model.rs +2 -0
- package/crates/naome-core/src/task_state/status/proof.rs +59 -9
- package/crates/naome-core/src/task_state/status/proof_read.rs +14 -0
- package/crates/naome-core/src/task_state/status/report_context.rs +23 -1
- package/crates/naome-core/src/task_state/status/transition.rs +29 -1
- package/crates/naome-core/tests/task_status.rs +122 -0
- package/installer/context.js +1 -1
- package/installer/harness-verification.js +2 -6
- package/installer/manifest-state.js +2 -2
- package/installer/native.js +3 -31
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
- package/templates/naome-root/.naome/bin/check-task-state.js +4 -39
- package/templates/naome-root/.naome/bin/naome.js +2 -30
- 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.
|
|
79
|
+
version = "1.4.3"
|
|
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.
|
|
87
|
+
version = "1.4.3"
|
|
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
|
-
|
|
9
|
+
The repository harness for long-running Codex work.
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
18
|
+
## Start
|
|
21
19
|
|
|
22
|
-
Install the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
@@ -37,12 +37,21 @@ 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]
|
|
41
|
-
naome task proof-plan [--json] [--exit-code]
|
|
42
|
-
naome task
|
|
40
|
+
naome task status [--json] [--exit-code] [--agent-session <id>]
|
|
41
|
+
naome task proof-plan [--json] [--exit-code] [--agent-session <id>]
|
|
42
|
+
naome task agent-snapshot --json [--exit-code] [--agent-session <id>]
|
|
43
|
+
naome task preflight (--path <path>...|--from-changed) --json [--agent-session <id>]
|
|
44
|
+
naome task commit-preflight --json [--exit-code] [--agent-session <id>]
|
|
45
|
+
naome task compact-proof [--dry-run] --json [--agent-session <id>]
|
|
46
|
+
naome task scope-suggestions --from-changed --json [--agent-session <id>]
|
|
47
|
+
naome task can-edit --path <path> --json [--agent-session <id>]
|
|
48
|
+
naome task run-check --check <check-id> [--record-proof] --json [--agent-session <id>]
|
|
49
|
+
naome task loop [--execute-safe] --json [--agent-session <id>]
|
|
50
|
+
naome task can-transition --to complete [--json] [--agent-session <id>]
|
|
43
51
|
naome task can-commit --json
|
|
44
|
-
naome task
|
|
45
|
-
naome task
|
|
52
|
+
naome task complete --from-can-transition --json [--agent-session <id>]
|
|
53
|
+
naome task repair --plan <id> (--dry-run|--execute-safe) --json [--agent-session <id>]
|
|
54
|
+
naome task record-proof --from-proof-plan [--dry-run] --json [--agent-session <id>]
|
|
46
55
|
naome task request-scope --path <path> --reason <reason> --json
|
|
47
56
|
naome task timeline --json
|
|
48
57
|
naome task loop-snapshot --json
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use naome_core::{
|
|
4
|
+
task_proof_plan, task_status_exit_code, task_status_report, task_transition_readiness,
|
|
5
|
+
};
|
|
6
|
+
use serde_json::{json, Value};
|
|
7
|
+
|
|
8
|
+
use super::common::{agent_session, print_json_with_session};
|
|
9
|
+
use super::planner;
|
|
10
|
+
|
|
11
|
+
pub(super) fn agent_snapshot(
|
|
12
|
+
root: &Path,
|
|
13
|
+
args: &[String],
|
|
14
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
15
|
+
let session = agent_session(args)?;
|
|
16
|
+
let status = task_status_report(root)?;
|
|
17
|
+
let proof_plan = task_proof_plan(root)?;
|
|
18
|
+
let transition = task_transition_readiness(root, "complete")?;
|
|
19
|
+
let planned = merge_commands(
|
|
20
|
+
serde_json::to_value(&proof_plan.recommended_commands)?,
|
|
21
|
+
planner::planned_commands(
|
|
22
|
+
root,
|
|
23
|
+
&status.scope.in_scope_changed_paths,
|
|
24
|
+
Some(&status.proof),
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
let (safe_to_run, deferred) = planner::split_safe_commands(&planned);
|
|
28
|
+
let can_commit = transition.allowed && status.agent_loop.can_commit;
|
|
29
|
+
let value = json!({
|
|
30
|
+
"schema": "naome.task.agent-snapshot.v1",
|
|
31
|
+
"state": snapshot_state(&status),
|
|
32
|
+
"task": {
|
|
33
|
+
"state": status.state,
|
|
34
|
+
"taskId": status.task_id,
|
|
35
|
+
"request": status.request
|
|
36
|
+
},
|
|
37
|
+
"git": {
|
|
38
|
+
"head": status.git.head,
|
|
39
|
+
"admissionHead": status.git.admission_head,
|
|
40
|
+
"admissionHeadReachable": status.git.admission_head_reachable,
|
|
41
|
+
"operationInProgress": status.git.operation_in_progress,
|
|
42
|
+
"branchDiverged": status.git.ahead > 0 || status.git.behind > 0
|
|
43
|
+
},
|
|
44
|
+
"scope": {
|
|
45
|
+
"allowedPaths": status.scope.allowed_paths,
|
|
46
|
+
"changedPaths": status.scope.changed_paths,
|
|
47
|
+
"editablePaths": editable_paths(&status),
|
|
48
|
+
"mustNotEditPaths": status.scope.out_of_scope_changed_paths,
|
|
49
|
+
"outOfScopeChangedPaths": status.scope.out_of_scope_changed_paths
|
|
50
|
+
},
|
|
51
|
+
"proof": status.proof,
|
|
52
|
+
"checks": {
|
|
53
|
+
"recommended": planned,
|
|
54
|
+
"safeToRun": safe_to_run,
|
|
55
|
+
"deferred": deferred
|
|
56
|
+
},
|
|
57
|
+
"commit": {
|
|
58
|
+
"canCommit": can_commit,
|
|
59
|
+
"blockingFindings": if can_commit { json!([]) } else { serde_json::to_value(&transition.blocking_findings)? }
|
|
60
|
+
},
|
|
61
|
+
"transition": {
|
|
62
|
+
"canComplete": transition.allowed,
|
|
63
|
+
"blockingFindings": transition.blocking_findings
|
|
64
|
+
},
|
|
65
|
+
"nextAction": snapshot_next_action(&status, &proof_plan, can_commit),
|
|
66
|
+
"agentLoop": status.agent_loop,
|
|
67
|
+
"repairPlan": status.repair_plan,
|
|
68
|
+
"findings": status.findings,
|
|
69
|
+
"agentInstruction": if can_commit { "Task is commit-ready; do not edit further before committing." } else { "Follow nextAction and only execute commands listed as safeToRun." }
|
|
70
|
+
});
|
|
71
|
+
let code = task_status_exit_code(&status.findings, &status.proof);
|
|
72
|
+
print_json_with_session(value, session.as_deref())?;
|
|
73
|
+
if args.iter().any(|arg| arg == "--exit-code") {
|
|
74
|
+
std::process::exit(code);
|
|
75
|
+
}
|
|
76
|
+
Ok(())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn merge_commands(left: Value, right: Vec<Value>) -> Vec<Value> {
|
|
80
|
+
let mut commands = left
|
|
81
|
+
.as_array()
|
|
82
|
+
.cloned()
|
|
83
|
+
.unwrap_or_default()
|
|
84
|
+
.into_iter()
|
|
85
|
+
.chain(right)
|
|
86
|
+
.collect::<Vec<_>>();
|
|
87
|
+
commands.sort_by(|a, b| {
|
|
88
|
+
a.get("checkId")
|
|
89
|
+
.and_then(Value::as_str)
|
|
90
|
+
.unwrap_or("")
|
|
91
|
+
.cmp(b.get("checkId").and_then(Value::as_str).unwrap_or(""))
|
|
92
|
+
});
|
|
93
|
+
commands.dedup_by(|a, b| {
|
|
94
|
+
a.get("checkId").and_then(Value::as_str) == b.get("checkId").and_then(Value::as_str)
|
|
95
|
+
});
|
|
96
|
+
commands
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn snapshot_state(status: &naome_core::TaskStatusReportV1) -> &'static str {
|
|
100
|
+
if status
|
|
101
|
+
.findings
|
|
102
|
+
.iter()
|
|
103
|
+
.any(|finding| finding.id.starts_with("task.git."))
|
|
104
|
+
{
|
|
105
|
+
"blocked_by_git_state"
|
|
106
|
+
} else if !status.scope.out_of_scope_changed_paths.is_empty() {
|
|
107
|
+
"blocked_by_scope_drift"
|
|
108
|
+
} else if !status.proof.missing_checks.is_empty() {
|
|
109
|
+
"blocked_by_missing_proof"
|
|
110
|
+
} else if !status.proof.stale_checks.is_empty() {
|
|
111
|
+
"blocked_by_stale_proof"
|
|
112
|
+
} else if status.agent_loop.can_commit {
|
|
113
|
+
"ready_to_commit"
|
|
114
|
+
} else {
|
|
115
|
+
"healthy"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn editable_paths(status: &naome_core::TaskStatusReportV1) -> Vec<String> {
|
|
120
|
+
if matches!(
|
|
121
|
+
status.state.as_str(),
|
|
122
|
+
"idle" | "missing" | "complete" | "blocked" | "needs_human_review"
|
|
123
|
+
) {
|
|
124
|
+
return Vec::new();
|
|
125
|
+
}
|
|
126
|
+
status.scope.allowed_paths.clone()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn snapshot_next_action(
|
|
130
|
+
status: &naome_core::TaskStatusReportV1,
|
|
131
|
+
proof_plan: &naome_core::TaskProofPlanReport,
|
|
132
|
+
can_commit: bool,
|
|
133
|
+
) -> Value {
|
|
134
|
+
let action_type = if !status.scope.out_of_scope_changed_paths.is_empty() {
|
|
135
|
+
"repair_scope"
|
|
136
|
+
} else if status
|
|
137
|
+
.findings
|
|
138
|
+
.iter()
|
|
139
|
+
.any(|finding| finding.id.starts_with("task.git."))
|
|
140
|
+
{
|
|
141
|
+
"recover_git"
|
|
142
|
+
} else if !status.proof.missing_checks.is_empty() || !status.proof.stale_checks.is_empty() {
|
|
143
|
+
"run_checks"
|
|
144
|
+
} else if !proof_plan.proof_recording.checks_to_record.is_empty() {
|
|
145
|
+
"record_proof"
|
|
146
|
+
} else if can_commit {
|
|
147
|
+
"commit_ready"
|
|
148
|
+
} else if status.state == "implementing" && status.agent_loop.can_continue_editing {
|
|
149
|
+
"edit"
|
|
150
|
+
} else {
|
|
151
|
+
"none"
|
|
152
|
+
};
|
|
153
|
+
if can_commit && action_type == "commit_ready" {
|
|
154
|
+
return json!({
|
|
155
|
+
"type": "commit_ready",
|
|
156
|
+
"reason": "Task is complete enough to commit; do not continue editing before commit.",
|
|
157
|
+
"commands": [],
|
|
158
|
+
"paths": status.scope.in_scope_changed_paths,
|
|
159
|
+
"checkIds": [],
|
|
160
|
+
"safeToExecute": false,
|
|
161
|
+
"requiresUserApproval": true
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
json!({
|
|
165
|
+
"type": action_type,
|
|
166
|
+
"reason": status.next_action_v2.reason,
|
|
167
|
+
"commands": status.next_action_v2.commands,
|
|
168
|
+
"paths": status.next_action_v2.paths,
|
|
169
|
+
"checkIds": status.next_action_v2.check_ids,
|
|
170
|
+
"safeToExecute": status.next_action_v2.safe_to_execute,
|
|
171
|
+
"requiresUserApproval": status.next_action_v2.requires_user_approval
|
|
172
|
+
})
|
|
173
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use naome_core::{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
|
+
use super::path_policy::{edit_finding, is_control_path, is_ignored, normalize_requested_path};
|
|
8
|
+
|
|
9
|
+
pub(super) fn can_edit(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
10
|
+
let session = agent_session(args)?;
|
|
11
|
+
let Some(raw_path) = value_after(args, "--path") else {
|
|
12
|
+
return Err("naome task can-edit requires --path <path>".into());
|
|
13
|
+
};
|
|
14
|
+
let (path, path_error) = normalize_requested_path(raw_path);
|
|
15
|
+
let mut findings = Vec::new();
|
|
16
|
+
if let Some(message) = path_error {
|
|
17
|
+
findings.push(edit_finding("task.edit.unsafe_path", &message, raw_path));
|
|
18
|
+
} else if is_ignored(root, &path) {
|
|
19
|
+
findings.push(edit_finding(
|
|
20
|
+
"task.edit.ignored_path",
|
|
21
|
+
"Path is ignored by .naomeignore and is outside the active harness context.",
|
|
22
|
+
&path,
|
|
23
|
+
));
|
|
24
|
+
} else if is_control_path(&path) {
|
|
25
|
+
findings.push(edit_finding(
|
|
26
|
+
"task.edit.control_path",
|
|
27
|
+
"NAOME control files cannot be edited through the autonomous can-edit path.",
|
|
28
|
+
&path,
|
|
29
|
+
));
|
|
30
|
+
} else {
|
|
31
|
+
let status = task_status_report(root)?;
|
|
32
|
+
if status.task_id.is_none()
|
|
33
|
+
|| matches!(
|
|
34
|
+
status.state.as_str(),
|
|
35
|
+
"idle" | "missing" | "complete" | "blocked" | "needs_human_review"
|
|
36
|
+
)
|
|
37
|
+
{
|
|
38
|
+
findings.push(edit_finding(
|
|
39
|
+
"task.edit.no_active_task",
|
|
40
|
+
"No editable active task is available for this path.",
|
|
41
|
+
&path,
|
|
42
|
+
));
|
|
43
|
+
} else if !path_matches_any(&path, &status.scope.allowed_paths) {
|
|
44
|
+
findings.push(edit_finding(
|
|
45
|
+
"task.edit.out_of_scope",
|
|
46
|
+
"Path is outside activeTask.allowedPaths.",
|
|
47
|
+
&path,
|
|
48
|
+
));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let allowed = findings.is_empty();
|
|
53
|
+
print_json_with_session(
|
|
54
|
+
json!({
|
|
55
|
+
"schema": "naome.task.can-edit.v1",
|
|
56
|
+
"path": path,
|
|
57
|
+
"allowed": allowed,
|
|
58
|
+
"reason": if allowed { "Path is inside active task scope." } else { "Path is not safe to edit for the active task." },
|
|
59
|
+
"findings": findings,
|
|
60
|
+
"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." }
|
|
61
|
+
}),
|
|
62
|
+
session.as_deref(),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -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,163 @@
|
|
|
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) task_id: Option<String>,
|
|
13
|
+
pub(in crate::task_commands) check_id: String,
|
|
14
|
+
pub(in crate::task_commands) command: String,
|
|
15
|
+
pub(in crate::task_commands) cwd: String,
|
|
16
|
+
pub(in crate::task_commands) exit_code: i32,
|
|
17
|
+
pub(in crate::task_commands) checked_at: String,
|
|
18
|
+
pub(in crate::task_commands) evidence_paths: Vec<String>,
|
|
19
|
+
pub(in crate::task_commands) evidence_fingerprint: String,
|
|
20
|
+
pub(in crate::task_commands) stdout_summary: String,
|
|
21
|
+
pub(in crate::task_commands) stderr_summary: String,
|
|
22
|
+
pub(in crate::task_commands) duration_ms: u128,
|
|
23
|
+
pub(in crate::task_commands) agent_session: Option<String>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub(in crate::task_commands) fn successful_receipts(
|
|
27
|
+
root: &Path,
|
|
28
|
+
) -> Result<Vec<CheckRunReceipt>, Box<dyn std::error::Error>> {
|
|
29
|
+
let path = receipt_path(root)?;
|
|
30
|
+
let Ok(content) = fs::read_to_string(path) else {
|
|
31
|
+
return Ok(Vec::new());
|
|
32
|
+
};
|
|
33
|
+
let value: Value = serde_json::from_str(&content)?;
|
|
34
|
+
Ok(value
|
|
35
|
+
.as_array()
|
|
36
|
+
.into_iter()
|
|
37
|
+
.flatten()
|
|
38
|
+
.filter_map(receipt_from_value)
|
|
39
|
+
.filter(|receipt| receipt.exit_code == 0)
|
|
40
|
+
.collect())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub(super) fn append_receipt(
|
|
44
|
+
root: &Path,
|
|
45
|
+
receipt: &CheckRunReceipt,
|
|
46
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
47
|
+
let path = receipt_path(root)?;
|
|
48
|
+
if let Some(parent) = path.parent() {
|
|
49
|
+
fs::create_dir_all(parent)?;
|
|
50
|
+
}
|
|
51
|
+
let mut receipts = if path.exists() {
|
|
52
|
+
serde_json::from_str::<Value>(&fs::read_to_string(&path)?)?
|
|
53
|
+
.as_array()
|
|
54
|
+
.cloned()
|
|
55
|
+
.unwrap_or_default()
|
|
56
|
+
} else {
|
|
57
|
+
Vec::new()
|
|
58
|
+
};
|
|
59
|
+
receipts.push(receipt_to_value(receipt));
|
|
60
|
+
fs::write(
|
|
61
|
+
path,
|
|
62
|
+
format!("{}\n", serde_json::to_string_pretty(&receipts)?),
|
|
63
|
+
)?;
|
|
64
|
+
Ok(())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub(in crate::task_commands) fn changed_paths(
|
|
68
|
+
root: &Path,
|
|
69
|
+
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
|
70
|
+
let output = Command::new("git")
|
|
71
|
+
.args(["status", "--porcelain=v1", "--untracked-files=all"])
|
|
72
|
+
.current_dir(root)
|
|
73
|
+
.output()?;
|
|
74
|
+
if !output.status.success() {
|
|
75
|
+
return Ok(Vec::new());
|
|
76
|
+
}
|
|
77
|
+
let mut paths = String::from_utf8_lossy(&output.stdout)
|
|
78
|
+
.lines()
|
|
79
|
+
.filter_map(|line| line.get(3..))
|
|
80
|
+
.map(|line| line.split(" -> ").last().unwrap_or(line).to_string())
|
|
81
|
+
.collect::<Vec<_>>();
|
|
82
|
+
paths.sort();
|
|
83
|
+
Ok(paths)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn receipt_path(root: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
87
|
+
let output = Command::new("git")
|
|
88
|
+
.args(["rev-parse", "--git-path", RECEIPTS_PATH])
|
|
89
|
+
.current_dir(root)
|
|
90
|
+
.output()?;
|
|
91
|
+
if output.status.success() {
|
|
92
|
+
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
93
|
+
if !path.is_empty() {
|
|
94
|
+
return Ok(root.join(path));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
Ok(root.join(".git").join(RECEIPTS_PATH))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn receipt_to_value(receipt: &CheckRunReceipt) -> Value {
|
|
101
|
+
json!({
|
|
102
|
+
"taskId": receipt.task_id,
|
|
103
|
+
"checkId": receipt.check_id,
|
|
104
|
+
"command": receipt.command,
|
|
105
|
+
"cwd": receipt.cwd,
|
|
106
|
+
"exitCode": receipt.exit_code,
|
|
107
|
+
"checkedAt": receipt.checked_at,
|
|
108
|
+
"evidencePaths": receipt.evidence_paths,
|
|
109
|
+
"evidenceFingerprint": receipt.evidence_fingerprint,
|
|
110
|
+
"stdoutSummary": receipt.stdout_summary,
|
|
111
|
+
"stderrSummary": receipt.stderr_summary,
|
|
112
|
+
"durationMs": receipt.duration_ms,
|
|
113
|
+
"agentSession": receipt.agent_session
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn receipt_from_value(value: &Value) -> Option<CheckRunReceipt> {
|
|
118
|
+
Some(CheckRunReceipt {
|
|
119
|
+
task_id: value
|
|
120
|
+
.get("taskId")
|
|
121
|
+
.and_then(Value::as_str)
|
|
122
|
+
.map(ToString::to_string),
|
|
123
|
+
check_id: value.get("checkId")?.as_str()?.to_string(),
|
|
124
|
+
command: value.get("command")?.as_str()?.to_string(),
|
|
125
|
+
cwd: value.get("cwd")?.as_str()?.to_string(),
|
|
126
|
+
exit_code: value.get("exitCode")?.as_i64()? as i32,
|
|
127
|
+
checked_at: value.get("checkedAt")?.as_str()?.to_string(),
|
|
128
|
+
evidence_paths: value
|
|
129
|
+
.get("evidencePaths")?
|
|
130
|
+
.as_array()?
|
|
131
|
+
.iter()
|
|
132
|
+
.filter_map(Value::as_str)
|
|
133
|
+
.map(ToString::to_string)
|
|
134
|
+
.collect(),
|
|
135
|
+
evidence_fingerprint: value
|
|
136
|
+
.get("evidenceFingerprint")
|
|
137
|
+
.and_then(Value::as_str)
|
|
138
|
+
.unwrap_or("")
|
|
139
|
+
.to_string(),
|
|
140
|
+
stdout_summary: value
|
|
141
|
+
.get("stdoutSummary")
|
|
142
|
+
.and_then(Value::as_str)
|
|
143
|
+
.unwrap_or("")
|
|
144
|
+
.to_string(),
|
|
145
|
+
stderr_summary: value
|
|
146
|
+
.get("stderrSummary")
|
|
147
|
+
.and_then(Value::as_str)
|
|
148
|
+
.unwrap_or("")
|
|
149
|
+
.to_string(),
|
|
150
|
+
duration_ms: value.get("durationMs").and_then(Value::as_u64).unwrap_or(0) as u128,
|
|
151
|
+
agent_session: value
|
|
152
|
+
.get("agentSession")
|
|
153
|
+
.and_then(Value::as_str)
|
|
154
|
+
.map(ToString::to_string),
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pub(in crate::task_commands) fn evidence_fingerprint(
|
|
159
|
+
root: &Path,
|
|
160
|
+
paths: &[String],
|
|
161
|
+
) -> Result<String, Box<dyn std::error::Error>> {
|
|
162
|
+
Ok(task_evidence_fingerprint(root, paths)?)
|
|
163
|
+
}
|