@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
|
@@ -1,48 +1,65 @@
|
|
|
1
|
-
use std::fs;
|
|
2
1
|
use std::path::Path;
|
|
3
2
|
|
|
4
3
|
use naome_core::task_proof_plan;
|
|
5
4
|
use serde_json::{json, Value};
|
|
6
5
|
|
|
7
|
-
use super::
|
|
6
|
+
use super::check_run::{evidence_fingerprint, successful_receipts, CheckRunReceipt};
|
|
7
|
+
use super::common::{agent_session, print_json_with_session, read_task_state, write_task_state};
|
|
8
8
|
|
|
9
9
|
pub(super) fn record_proof(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
10
10
|
if !args.iter().any(|arg| arg == "--from-proof-plan") {
|
|
11
11
|
return Err("naome task record-proof requires --from-proof-plan".into());
|
|
12
12
|
}
|
|
13
|
-
let
|
|
13
|
+
let session = agent_session(args)?;
|
|
14
14
|
let dry_run = args.iter().any(|arg| arg == "--dry-run");
|
|
15
|
-
|
|
15
|
+
print_json_with_session(
|
|
16
|
+
record_proof_payload(root, dry_run, session.as_deref())?,
|
|
17
|
+
session.as_deref(),
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub(super) fn record_proof_value(
|
|
22
|
+
root: &Path,
|
|
23
|
+
session: Option<&str>,
|
|
24
|
+
) -> Result<Value, Box<dyn std::error::Error>> {
|
|
25
|
+
record_proof_payload(root, false, session)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn record_proof_payload(
|
|
29
|
+
root: &Path,
|
|
30
|
+
dry_run: bool,
|
|
31
|
+
session: Option<&str>,
|
|
32
|
+
) -> Result<Value, Box<dyn std::error::Error>> {
|
|
33
|
+
let plan = task_proof_plan(root)?;
|
|
34
|
+
let paths = plan.proof_recording.paths.clone();
|
|
35
|
+
let check_ids = plan.proof_recording.checks_to_record.clone();
|
|
36
|
+
let (can_record, findings, receipts) = recordable_receipts(root, &check_ids, &paths)?;
|
|
37
|
+
let recorded = !dry_run && can_record && !check_ids.is_empty();
|
|
16
38
|
let recorded_ids = if recorded {
|
|
17
|
-
write_proof_batch(
|
|
39
|
+
write_proof_batch(
|
|
40
|
+
root,
|
|
41
|
+
&plan.proof_recording.path_set_id,
|
|
42
|
+
&plan.proof_recording.proof_batch_id,
|
|
43
|
+
&paths,
|
|
44
|
+
&receipts,
|
|
45
|
+
)?
|
|
18
46
|
} else {
|
|
19
47
|
RecordedProof {
|
|
20
48
|
path_set_id: plan.proof_recording.path_set_id.clone(),
|
|
21
49
|
proof_batch_id: plan.proof_recording.proof_batch_id.clone(),
|
|
22
50
|
}
|
|
23
51
|
};
|
|
24
|
-
|
|
25
|
-
print_json(json!({
|
|
52
|
+
Ok(json!({
|
|
26
53
|
"schema": "naome.task.record-proof.v1",
|
|
27
54
|
"dryRun": dry_run,
|
|
28
55
|
"recorded": recorded,
|
|
29
56
|
"pathSetId": recorded_ids.path_set_id,
|
|
30
|
-
"paths":
|
|
57
|
+
"paths": paths,
|
|
31
58
|
"proofBatchId": recorded_ids.proof_batch_id,
|
|
32
|
-
"checksRecorded":
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
print_json(json!({
|
|
38
|
-
"schema": "naome.task.record-proof.v1",
|
|
39
|
-
"dryRun": dry_run,
|
|
40
|
-
"recorded": recorded,
|
|
41
|
-
"pathSetId": recorded_ids.path_set_id,
|
|
42
|
-
"paths": plan.proof_recording.paths,
|
|
43
|
-
"proofBatchId": recorded_ids.proof_batch_id,
|
|
44
|
-
"checksRecorded": plan.proof_recording.checks_to_record,
|
|
45
|
-
"agentInstruction": "Record proof only after the listed checks exited 0 for the current task diff."
|
|
59
|
+
"checksRecorded": if recorded || dry_run && can_record { check_ids } else { Vec::<String>::new() },
|
|
60
|
+
"findings": findings,
|
|
61
|
+
"agentInstruction": if recorded { "Proof recorded from recent successful safe check evidence." } else { "Run task run-check --record-proof or task loop --execute-safe before recording proof." },
|
|
62
|
+
"agentSession": session
|
|
46
63
|
}))
|
|
47
64
|
}
|
|
48
65
|
|
|
@@ -51,17 +68,48 @@ struct RecordedProof {
|
|
|
51
68
|
proof_batch_id: String,
|
|
52
69
|
}
|
|
53
70
|
|
|
71
|
+
pub(super) fn record_receipts_for_checks(
|
|
72
|
+
root: &Path,
|
|
73
|
+
check_ids: &[String],
|
|
74
|
+
paths: &[String],
|
|
75
|
+
session: Option<&str>,
|
|
76
|
+
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
77
|
+
let (can_record, _findings, receipts) = recordable_receipts(root, check_ids, paths)?;
|
|
78
|
+
if !can_record || receipts.is_empty() {
|
|
79
|
+
return Ok(false);
|
|
80
|
+
}
|
|
81
|
+
let task_id = read_task_state(root)?
|
|
82
|
+
.get("activeTask")
|
|
83
|
+
.and_then(|task| task.get("id"))
|
|
84
|
+
.and_then(Value::as_str)
|
|
85
|
+
.unwrap_or("task")
|
|
86
|
+
.to_string();
|
|
87
|
+
let batch_id = format!("{task_id}-proof");
|
|
88
|
+
let receipts = receipts
|
|
89
|
+
.into_iter()
|
|
90
|
+
.map(|mut receipt| {
|
|
91
|
+
if receipt.agent_session.is_none() {
|
|
92
|
+
receipt.agent_session = session.map(ToString::to_string);
|
|
93
|
+
}
|
|
94
|
+
receipt
|
|
95
|
+
})
|
|
96
|
+
.collect::<Vec<_>>();
|
|
97
|
+
write_proof_batch(root, "current-task-diff", &batch_id, paths, &receipts)?;
|
|
98
|
+
Ok(true)
|
|
99
|
+
}
|
|
100
|
+
|
|
54
101
|
fn write_proof_batch(
|
|
55
102
|
root: &Path,
|
|
56
|
-
|
|
103
|
+
path_set_base: &str,
|
|
104
|
+
proof_batch_base: &str,
|
|
105
|
+
paths: &[String],
|
|
106
|
+
receipts: &[CheckRunReceipt],
|
|
57
107
|
) -> Result<RecordedProof, Box<dyn std::error::Error>> {
|
|
58
|
-
let path = root.join(".naome/task-state.json");
|
|
59
108
|
let mut state = read_task_state(root)?;
|
|
60
|
-
let checked_at =
|
|
61
|
-
.
|
|
62
|
-
.
|
|
63
|
-
.
|
|
64
|
-
.to_string();
|
|
109
|
+
let checked_at = receipts
|
|
110
|
+
.first()
|
|
111
|
+
.map(|receipt| receipt.checked_at.clone())
|
|
112
|
+
.unwrap_or_else(|| "1970-01-01T00:00:00.000Z".to_string());
|
|
65
113
|
let active = state
|
|
66
114
|
.get_mut("activeTask")
|
|
67
115
|
.and_then(Value::as_object_mut)
|
|
@@ -71,21 +119,21 @@ fn write_proof_batch(
|
|
|
71
119
|
.or_insert_with(|| json!({}))
|
|
72
120
|
.as_object_mut()
|
|
73
121
|
.ok_or("activeTask.proofPathSets must be an object")?;
|
|
74
|
-
let path_set_id = unique_path_set_id(path_sets,
|
|
75
|
-
path_sets.insert(path_set_id.clone(), json!(
|
|
122
|
+
let path_set_id = unique_path_set_id(path_sets, path_set_base);
|
|
123
|
+
path_sets.insert(path_set_id.clone(), json!(paths));
|
|
76
124
|
let batches = active
|
|
77
125
|
.entry("proofBatches")
|
|
78
126
|
.or_insert_with(|| json!([]))
|
|
79
127
|
.as_array_mut()
|
|
80
128
|
.ok_or("activeTask.proofBatches must be an array")?;
|
|
81
|
-
let proof_batch_id = unique_proof_batch_id(batches,
|
|
129
|
+
let proof_batch_id = unique_proof_batch_id(batches, proof_batch_base);
|
|
82
130
|
batches.push(proof_batch(
|
|
83
|
-
|
|
131
|
+
receipts,
|
|
84
132
|
&checked_at,
|
|
85
133
|
&path_set_id,
|
|
86
134
|
&proof_batch_id,
|
|
87
135
|
));
|
|
88
|
-
|
|
136
|
+
write_task_state(root, &state)?;
|
|
89
137
|
Ok(RecordedProof {
|
|
90
138
|
path_set_id,
|
|
91
139
|
proof_batch_id,
|
|
@@ -93,7 +141,7 @@ fn write_proof_batch(
|
|
|
93
141
|
}
|
|
94
142
|
|
|
95
143
|
fn proof_batch(
|
|
96
|
-
|
|
144
|
+
receipts: &[CheckRunReceipt],
|
|
97
145
|
checked_at: &str,
|
|
98
146
|
path_set_id: &str,
|
|
99
147
|
proof_batch_id: &str,
|
|
@@ -102,12 +150,103 @@ fn proof_batch(
|
|
|
102
150
|
"id": proof_batch_id,
|
|
103
151
|
"checkedAt": checked_at,
|
|
104
152
|
"evidencePathSet": path_set_id,
|
|
105
|
-
"proofs":
|
|
106
|
-
json!({
|
|
153
|
+
"proofs": receipts.iter().map(|receipt| {
|
|
154
|
+
json!({
|
|
155
|
+
"checkId": receipt.check_id,
|
|
156
|
+
"command": receipt.command,
|
|
157
|
+
"cwd": receipt.cwd,
|
|
158
|
+
"exitCode": receipt.exit_code,
|
|
159
|
+
"checkedAt": receipt.checked_at,
|
|
160
|
+
"evidenceFingerprint": receipt.evidence_fingerprint,
|
|
161
|
+
"agentSession": receipt.agent_session
|
|
162
|
+
})
|
|
107
163
|
}).collect::<Vec<_>>()
|
|
108
164
|
})
|
|
109
165
|
}
|
|
110
166
|
|
|
167
|
+
fn recordable_receipts(
|
|
168
|
+
root: &Path,
|
|
169
|
+
check_ids: &[String],
|
|
170
|
+
paths: &[String],
|
|
171
|
+
) -> Result<(bool, Vec<Value>, Vec<CheckRunReceipt>), Box<dyn std::error::Error>> {
|
|
172
|
+
let receipts = successful_receipts(root)?;
|
|
173
|
+
let current_fingerprint = evidence_fingerprint(root, paths)?;
|
|
174
|
+
let task_id = read_task_state(root)?
|
|
175
|
+
.get("activeTask")
|
|
176
|
+
.and_then(|task| task.get("id"))
|
|
177
|
+
.and_then(Value::as_str)
|
|
178
|
+
.map(ToString::to_string);
|
|
179
|
+
let mut selected = Vec::new();
|
|
180
|
+
let mut findings = Vec::new();
|
|
181
|
+
for check_id in check_ids {
|
|
182
|
+
let expected = super::check_run::read_verification_check(root, check_id)?;
|
|
183
|
+
let matching_check = receipts
|
|
184
|
+
.iter()
|
|
185
|
+
.rev()
|
|
186
|
+
.find(|receipt| receipt.check_id == *check_id);
|
|
187
|
+
let Some(receipt) = matching_check else {
|
|
188
|
+
findings.push(no_recent_success(check_id));
|
|
189
|
+
continue;
|
|
190
|
+
};
|
|
191
|
+
if receipt.task_id != task_id {
|
|
192
|
+
findings.push(json!({
|
|
193
|
+
"id": "task.proof.receipt_task_mismatch",
|
|
194
|
+
"severity": "error",
|
|
195
|
+
"message": format!("Recent receipt for {check_id} was produced for a different task."),
|
|
196
|
+
"path": null,
|
|
197
|
+
"suggestedFix": "Rerun the check for the active task before recording proof.",
|
|
198
|
+
"agentInstruction": "Do not record proof from another task."
|
|
199
|
+
}));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if !expected
|
|
203
|
+
.as_ref()
|
|
204
|
+
.is_some_and(|check| receipt.command == check.command && receipt.cwd == check.cwd)
|
|
205
|
+
{
|
|
206
|
+
findings.push(no_recent_success(check_id));
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if paths != receipt.evidence_paths {
|
|
210
|
+
findings.push(json!({
|
|
211
|
+
"id": "task.proof.receipt_path_mismatch",
|
|
212
|
+
"severity": "error",
|
|
213
|
+
"message": format!("Recent receipt for {check_id} covers a different path set."),
|
|
214
|
+
"path": null,
|
|
215
|
+
"suggestedFix": "Rerun the check against the current task-owned changed paths.",
|
|
216
|
+
"agentInstruction": "Do not record proof when receipt paths differ from the current task diff."
|
|
217
|
+
}));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if receipt.evidence_fingerprint != current_fingerprint {
|
|
221
|
+
findings.push(json!({
|
|
222
|
+
"id": "task.proof.stale_receipt_content",
|
|
223
|
+
"severity": "error",
|
|
224
|
+
"message": format!("Recent receipt for {check_id} does not match current file contents."),
|
|
225
|
+
"path": null,
|
|
226
|
+
"suggestedFix": "Rerun the check after the latest edits.",
|
|
227
|
+
"agentInstruction": "Do not record proof from stale check output."
|
|
228
|
+
}));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
match matching_check.cloned() {
|
|
232
|
+
Some(receipt) => selected.push(receipt),
|
|
233
|
+
None => findings.push(no_recent_success(check_id)),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
Ok((findings.is_empty(), findings, selected))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fn no_recent_success(check_id: &str) -> Value {
|
|
240
|
+
json!({
|
|
241
|
+
"id": "task.proof.no_recent_success",
|
|
242
|
+
"severity": "error",
|
|
243
|
+
"message": format!("No recent successful safe check evidence covers current task paths for {check_id}."),
|
|
244
|
+
"path": null,
|
|
245
|
+
"suggestedFix": "Run naome task run-check --check <id> --record-proof --json or naome task loop --execute-safe --json.",
|
|
246
|
+
"agentInstruction": "Do not record proof until NAOME has just executed the check successfully."
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
111
250
|
fn unique_path_set_id(path_sets: &serde_json::Map<String, Value>, base: &str) -> String {
|
|
112
251
|
unique_id(base, |candidate| path_sets.contains_key(candidate))
|
|
113
252
|
}
|
|
@@ -3,14 +3,20 @@ use std::path::Path;
|
|
|
3
3
|
use naome_core::task_status_report;
|
|
4
4
|
use serde_json::json;
|
|
5
5
|
|
|
6
|
-
use super::common::{
|
|
6
|
+
use super::common::{agent_session, print_json_with_session, value_after};
|
|
7
7
|
|
|
8
8
|
pub(super) fn repair_preview(
|
|
9
9
|
root: &Path,
|
|
10
10
|
args: &[String],
|
|
11
11
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
let session = agent_session(args)?;
|
|
13
|
+
let dry_run = args.iter().any(|arg| arg == "--dry-run");
|
|
14
|
+
let execute_safe = args.iter().any(|arg| arg == "--execute-safe");
|
|
15
|
+
if dry_run && execute_safe {
|
|
16
|
+
return Err("naome task repair accepts only one of --dry-run or --execute-safe".into());
|
|
17
|
+
}
|
|
18
|
+
if !dry_run && !execute_safe {
|
|
19
|
+
return Err("naome task repair requires --dry-run or --execute-safe".into());
|
|
14
20
|
}
|
|
15
21
|
let plan_id = value_after(args, "--plan").ok_or("naome task repair requires --plan <id>")?;
|
|
16
22
|
let status = task_status_report(root)?;
|
|
@@ -19,12 +25,53 @@ pub(super) fn repair_preview(
|
|
|
19
25
|
.iter()
|
|
20
26
|
.find(|item| item.id == plan_id)
|
|
21
27
|
.cloned();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
let mut steps = Vec::new();
|
|
29
|
+
let mut executed = false;
|
|
30
|
+
let mut requires_user_approval = false;
|
|
31
|
+
if execute_safe {
|
|
32
|
+
if let Some(plan) = &plan {
|
|
33
|
+
if !can_execute_safe(plan) {
|
|
34
|
+
requires_user_approval = true;
|
|
35
|
+
} else {
|
|
36
|
+
match plan.kind.as_str() {
|
|
37
|
+
"rerun_check" if plan.check_ids.len() == 1 => {
|
|
38
|
+
steps.push(super::check_run::run_check_by_id(
|
|
39
|
+
root,
|
|
40
|
+
&plan.check_ids[0],
|
|
41
|
+
false,
|
|
42
|
+
session.as_deref(),
|
|
43
|
+
)?);
|
|
44
|
+
executed = true;
|
|
45
|
+
}
|
|
46
|
+
"record_proof" => {
|
|
47
|
+
steps.push(super::record::record_proof_value(root, session.as_deref())?);
|
|
48
|
+
executed = true;
|
|
49
|
+
}
|
|
50
|
+
_ => {
|
|
51
|
+
requires_user_approval = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
print_json_with_session(
|
|
58
|
+
json!({
|
|
59
|
+
"schema": if dry_run { "naome.task.repair-preview.v1" } else { "naome.task.repair-execute.v1" },
|
|
60
|
+
"planId": plan_id,
|
|
61
|
+
"found": plan.is_some(),
|
|
62
|
+
"wouldExecute": dry_run && plan.as_ref().is_some_and(can_execute_safe),
|
|
63
|
+
"executed": executed,
|
|
64
|
+
"requiresUserApproval": requires_user_approval || plan.as_ref().is_some_and(|item| item.requires_user_approval),
|
|
65
|
+
"plan": plan,
|
|
66
|
+
"steps": steps,
|
|
67
|
+
"agentInstruction": if executed { "Executed only NAOME safe check/proof repair steps." } else { "Review this output; unsafe repair plans require human approval." }
|
|
68
|
+
}),
|
|
69
|
+
session.as_deref(),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn can_execute_safe(item: &naome_core::RepairPlanItem) -> bool {
|
|
74
|
+
item.safe_to_execute
|
|
75
|
+
&& !item.requires_user_approval
|
|
76
|
+
&& matches!(item.kind.as_str(), "rerun_check" | "record_proof")
|
|
30
77
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
use std::process::Command;
|
|
3
|
+
|
|
4
|
+
use serde_json::json;
|
|
5
|
+
|
|
6
|
+
use super::common::{agent_session, print_json_with_session};
|
|
7
|
+
use super::path_policy::is_ignored;
|
|
8
|
+
use super::planner;
|
|
9
|
+
|
|
10
|
+
pub(super) fn scope_suggestions(
|
|
11
|
+
root: &Path,
|
|
12
|
+
args: &[String],
|
|
13
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
14
|
+
let session = agent_session(args)?;
|
|
15
|
+
if !args.iter().any(|arg| arg == "--from-changed") {
|
|
16
|
+
return Err("naome task scope-suggestions requires --from-changed".into());
|
|
17
|
+
}
|
|
18
|
+
let mut suggestions = rename_suggestions(root)
|
|
19
|
+
.into_iter()
|
|
20
|
+
.chain(loader_suggestions(root))
|
|
21
|
+
.filter(|suggestion| {
|
|
22
|
+
!suggestion
|
|
23
|
+
.get("path")
|
|
24
|
+
.and_then(serde_json::Value::as_str)
|
|
25
|
+
.is_some_and(|path| is_ignored(root, path))
|
|
26
|
+
})
|
|
27
|
+
.collect::<Vec<_>>();
|
|
28
|
+
suggestions.sort_by(|left, right| {
|
|
29
|
+
left.get("path")
|
|
30
|
+
.and_then(serde_json::Value::as_str)
|
|
31
|
+
.unwrap_or("")
|
|
32
|
+
.cmp(
|
|
33
|
+
right
|
|
34
|
+
.get("path")
|
|
35
|
+
.and_then(serde_json::Value::as_str)
|
|
36
|
+
.unwrap_or(""),
|
|
37
|
+
)
|
|
38
|
+
});
|
|
39
|
+
print_json_with_session(
|
|
40
|
+
json!({
|
|
41
|
+
"schema": "naome.task.scope-suggestions.v1",
|
|
42
|
+
"suggestions": suggestions,
|
|
43
|
+
"agentInstruction": "Suggestions are read-only hints; task scope mutation still requires explicit approval."
|
|
44
|
+
}),
|
|
45
|
+
session.as_deref(),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn rename_suggestions(root: &Path) -> Vec<serde_json::Value> {
|
|
50
|
+
let Ok(output) = Command::new("git")
|
|
51
|
+
.args(["status", "--porcelain=v1", "-z"])
|
|
52
|
+
.current_dir(root)
|
|
53
|
+
.output()
|
|
54
|
+
else {
|
|
55
|
+
return Vec::new();
|
|
56
|
+
};
|
|
57
|
+
if !output.status.success() {
|
|
58
|
+
return Vec::new();
|
|
59
|
+
}
|
|
60
|
+
let mut suggestions = Vec::new();
|
|
61
|
+
let entries = output.stdout.split(|byte| *byte == 0).collect::<Vec<_>>();
|
|
62
|
+
let mut index = 0;
|
|
63
|
+
while index < entries.len() {
|
|
64
|
+
let entry = entries[index];
|
|
65
|
+
index += 1;
|
|
66
|
+
if entry.len() < 4 {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
let status = String::from_utf8_lossy(&entry[..2]);
|
|
70
|
+
if status.contains('R') && index < entries.len() {
|
|
71
|
+
let old = String::from_utf8_lossy(entries[index]).replace('\\', "/");
|
|
72
|
+
index += 1;
|
|
73
|
+
let new = String::from_utf8_lossy(&entry[3..]).replace('\\', "/");
|
|
74
|
+
for path in [old, new] {
|
|
75
|
+
suggestions.push(json!({
|
|
76
|
+
"path": path,
|
|
77
|
+
"reason": "paired path for renamed file",
|
|
78
|
+
"confidence": 0.95,
|
|
79
|
+
"requiresUserApproval": true
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
suggestions
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn loader_suggestions(root: &Path) -> Vec<serde_json::Value> {
|
|
88
|
+
planner::changed_paths(root)
|
|
89
|
+
.into_iter()
|
|
90
|
+
.filter(|path| path.ends_with(".test.js") || path.ends_with("_test.rs"))
|
|
91
|
+
.filter_map(|path| {
|
|
92
|
+
let loader = if path.starts_with("scripts/") {
|
|
93
|
+
Some("scripts/naome-installer-support.js".to_string())
|
|
94
|
+
} else if path.contains("/tests/") {
|
|
95
|
+
path.split("/tests/")
|
|
96
|
+
.next()
|
|
97
|
+
.map(|prefix| format!("{prefix}/tests/support/mod.rs"))
|
|
98
|
+
} else {
|
|
99
|
+
None
|
|
100
|
+
}?;
|
|
101
|
+
Some(json!({
|
|
102
|
+
"path": loader,
|
|
103
|
+
"reason": "fixture support helper for changed test",
|
|
104
|
+
"confidence": 0.7,
|
|
105
|
+
"requiresUserApproval": true
|
|
106
|
+
}))
|
|
107
|
+
})
|
|
108
|
+
.collect()
|
|
109
|
+
}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
use std::path::Path;
|
|
2
2
|
|
|
3
|
+
mod agent_snapshot;
|
|
4
|
+
mod can_edit;
|
|
5
|
+
mod check_run;
|
|
6
|
+
mod commit_preflight;
|
|
3
7
|
mod common;
|
|
8
|
+
mod compact_proof;
|
|
9
|
+
mod complete;
|
|
10
|
+
mod loop_control;
|
|
11
|
+
mod path_policy;
|
|
12
|
+
mod planner;
|
|
13
|
+
mod preflight;
|
|
4
14
|
mod readiness;
|
|
5
15
|
mod record;
|
|
6
16
|
mod repair;
|
|
7
17
|
mod scope_request;
|
|
18
|
+
mod scope_suggestions;
|
|
8
19
|
mod timeline;
|
|
9
20
|
|
|
10
21
|
use naome_core::{
|
|
@@ -19,9 +30,18 @@ pub fn run_task_command(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
|
|
|
19
30
|
Some("migrate-ledger") => migrate_ledger(root, args),
|
|
20
31
|
Some("status") => task_status(root, args),
|
|
21
32
|
Some("proof-plan") => proof_plan(root, args),
|
|
33
|
+
Some("agent-snapshot") => agent_snapshot::agent_snapshot(root, args),
|
|
34
|
+
Some("preflight") => preflight::preflight(root, args),
|
|
35
|
+
Some("can-edit") => can_edit::can_edit(root, args),
|
|
36
|
+
Some("run-check") => check_run::run_check_command(root, args),
|
|
22
37
|
Some("can-transition") => can_transition(root, args),
|
|
23
38
|
Some("repair") => repair::repair_preview(root, args),
|
|
24
39
|
Some("record-proof") => record::record_proof(root, args),
|
|
40
|
+
Some("compact-proof") => compact_proof::compact_proof(root, args),
|
|
41
|
+
Some("complete") => complete::complete_task(root, args),
|
|
42
|
+
Some("loop") => loop_control::task_loop(root, args),
|
|
43
|
+
Some("commit-preflight") => commit_preflight::commit_preflight(root, args),
|
|
44
|
+
Some("scope-suggestions") => scope_suggestions::scope_suggestions(root, args),
|
|
25
45
|
Some("request-scope") => scope_request::request_scope(root, args),
|
|
26
46
|
Some("can-commit") => readiness::can_commit(root, args),
|
|
27
47
|
Some("timeline") => timeline::timeline(root, args),
|
|
@@ -31,9 +51,10 @@ pub fn run_task_command(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
|
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
fn task_status(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
54
|
+
let session = common::agent_session(args)?;
|
|
34
55
|
let report = task_status_report(root)?;
|
|
35
56
|
if args.iter().any(|arg| arg == "--json") {
|
|
36
|
-
|
|
57
|
+
common::print_json_with_session(serde_json::to_value(&report)?, session.as_deref())?;
|
|
37
58
|
} else {
|
|
38
59
|
print!("{}", format_task_status(&report));
|
|
39
60
|
}
|
|
@@ -42,9 +63,10 @@ fn task_status(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::E
|
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
fn proof_plan(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
66
|
+
let session = common::agent_session(args)?;
|
|
45
67
|
let report = task_proof_plan(root)?;
|
|
46
68
|
if args.iter().any(|arg| arg == "--json") {
|
|
47
|
-
|
|
69
|
+
common::print_json_with_session(serde_json::to_value(&report)?, session.as_deref())?;
|
|
48
70
|
} else {
|
|
49
71
|
print!("{}", format_task_proof_plan(&report));
|
|
50
72
|
}
|
|
@@ -53,6 +75,7 @@ fn proof_plan(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Er
|
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
fn can_transition(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
78
|
+
let session = common::agent_session(args)?;
|
|
56
79
|
let Some(target) = args
|
|
57
80
|
.windows(2)
|
|
58
81
|
.find(|window| window[0] == "--to")
|
|
@@ -62,7 +85,7 @@ fn can_transition(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error
|
|
|
62
85
|
};
|
|
63
86
|
let report = task_transition_readiness(root, target)?;
|
|
64
87
|
if args.iter().any(|arg| arg == "--json") {
|
|
65
|
-
|
|
88
|
+
common::print_json_with_session(serde_json::to_value(&report)?, session.as_deref())?;
|
|
66
89
|
} else {
|
|
67
90
|
println!(
|
|
68
91
|
"NAOME task transition {target}: {}",
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
use std::fs;
|
|
2
|
-
use std::process::Command;
|
|
3
2
|
|
|
4
3
|
use serde_json::{json, Value};
|
|
5
4
|
|
|
6
5
|
mod task_cli_support;
|
|
7
6
|
|
|
8
|
-
use task_cli_support::{fixture_root, init_git, task_state, write_fixture_file};
|
|
7
|
+
use task_cli_support::{fixture_root, init_git, run_json, task_state, write_fixture_file};
|
|
9
8
|
|
|
10
9
|
#[test]
|
|
11
10
|
fn status_json_exposes_policy_hints_and_recovery_guidance() {
|
|
@@ -83,6 +82,14 @@ fn record_proof_from_plan_writes_compact_batch() {
|
|
|
83
82
|
init_git(&root);
|
|
84
83
|
write_fixture_file(&root, "README.md", "changed\n");
|
|
85
84
|
|
|
85
|
+
let check = run_json(
|
|
86
|
+
&root,
|
|
87
|
+
["task", "run-check", "--check", "diff-check", "--json"],
|
|
88
|
+
);
|
|
89
|
+
assert_eq!(check["schema"], "naome.task.run-check.v1");
|
|
90
|
+
assert_eq!(check["executed"], true);
|
|
91
|
+
assert_eq!(check["recordedProof"], false);
|
|
92
|
+
|
|
86
93
|
let recorded = run_json(
|
|
87
94
|
&root,
|
|
88
95
|
["task", "record-proof", "--from-proof-plan", "--json"],
|
|
@@ -201,17 +208,3 @@ fn review_fix_mode_is_structured_not_inferred_from_prompt_text() {
|
|
|
201
208
|
.unwrap()
|
|
202
209
|
.contains("explicit allowedPaths"));
|
|
203
210
|
}
|
|
204
|
-
|
|
205
|
-
fn run_json<const N: usize>(root: &std::path::Path, args: [&str; N]) -> Value {
|
|
206
|
-
let output = Command::new(env!("CARGO_BIN_EXE_naome"))
|
|
207
|
-
.args(args)
|
|
208
|
-
.current_dir(root)
|
|
209
|
-
.output()
|
|
210
|
-
.unwrap();
|
|
211
|
-
assert!(
|
|
212
|
-
output.status.success(),
|
|
213
|
-
"{}",
|
|
214
|
-
String::from_utf8_lossy(&output.stderr)
|
|
215
|
-
);
|
|
216
|
-
serde_json::from_slice(&output.stdout).unwrap()
|
|
217
|
-
}
|