@lamentis/naome 1.4.2 → 1.4.4
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/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/main.rs +5 -0
- package/crates/naome-cli/src/task_commands/agent_snapshot.rs +231 -0
- package/crates/naome-cli/src/task_commands/can_edit.rs +7 -59
- package/crates/naome-cli/src/task_commands/check_run/receipts.rs +10 -2
- package/crates/naome-cli/src/task_commands/check_run.rs +7 -3
- package/crates/naome-cli/src/task_commands/commit_preflight.rs +89 -0
- package/crates/naome-cli/src/task_commands/compact_proof.rs +69 -0
- package/crates/naome-cli/src/task_commands/loop_control.rs +25 -7
- 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/record.rs +63 -26
- package/crates/naome-cli/src/task_commands/scope_suggestions.rs +109 -0
- package/crates/naome-cli/src/task_commands.rs +12 -0
- package/crates/naome-cli/tests/task_cli_fast_flow.rs +332 -0
- package/crates/naome-cli/tests/task_cli_loop.rs +1 -1
- package/crates/naome-core/Cargo.toml +1 -1
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
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.4"
|
|
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.4"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
|
@@ -39,6 +39,11 @@ const HELP: &str = r#"Usage:
|
|
|
39
39
|
naome seed-verification
|
|
40
40
|
naome task status [--json] [--exit-code] [--agent-session <id>]
|
|
41
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>]
|
|
42
47
|
naome task can-edit --path <path> --json [--agent-session <id>]
|
|
43
48
|
naome task run-check --check <check-id> [--record-proof] --json [--agent-session <id>]
|
|
44
49
|
naome task loop [--execute-safe] --json [--agent-session <id>]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use naome_core::{
|
|
5
|
+
task_proof_plan, task_status_exit_code, task_status_report, task_transition_readiness,
|
|
6
|
+
};
|
|
7
|
+
use serde_json::{json, Value};
|
|
8
|
+
|
|
9
|
+
use super::common::{agent_session, print_json_with_session};
|
|
10
|
+
use super::planner;
|
|
11
|
+
|
|
12
|
+
pub(super) fn agent_snapshot(
|
|
13
|
+
root: &Path,
|
|
14
|
+
args: &[String],
|
|
15
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
16
|
+
let session = agent_session(args)?;
|
|
17
|
+
let status = task_status_report(root)?;
|
|
18
|
+
let proof_plan = task_proof_plan(root)?;
|
|
19
|
+
let transition = task_transition_readiness(root, "complete")?;
|
|
20
|
+
let planned = merge_commands(
|
|
21
|
+
serde_json::to_value(&proof_plan.recommended_commands)?,
|
|
22
|
+
planner::planned_commands(
|
|
23
|
+
root,
|
|
24
|
+
&status.scope.in_scope_changed_paths,
|
|
25
|
+
Some(&status.proof),
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
let (safe_to_run, deferred) = planner::split_safe_commands(&planned);
|
|
29
|
+
let can_commit = transition.allowed && status.agent_loop.can_commit;
|
|
30
|
+
let value = json!({
|
|
31
|
+
"schema": "naome.task.agent-snapshot.v1",
|
|
32
|
+
"state": snapshot_state(&status),
|
|
33
|
+
"task": {
|
|
34
|
+
"state": status.state,
|
|
35
|
+
"taskId": status.task_id,
|
|
36
|
+
"request": status.request
|
|
37
|
+
},
|
|
38
|
+
"git": {
|
|
39
|
+
"head": status.git.head,
|
|
40
|
+
"admissionHead": status.git.admission_head,
|
|
41
|
+
"admissionHeadReachable": status.git.admission_head_reachable,
|
|
42
|
+
"operationInProgress": status.git.operation_in_progress,
|
|
43
|
+
"branchDiverged": status.git.ahead > 0 || status.git.behind > 0
|
|
44
|
+
},
|
|
45
|
+
"scope": {
|
|
46
|
+
"allowedPaths": status.scope.allowed_paths,
|
|
47
|
+
"changedPaths": status.scope.changed_paths,
|
|
48
|
+
"editablePaths": editable_paths(&status),
|
|
49
|
+
"mustNotEditPaths": status.scope.out_of_scope_changed_paths,
|
|
50
|
+
"outOfScopeChangedPaths": status.scope.out_of_scope_changed_paths
|
|
51
|
+
},
|
|
52
|
+
"proof": status.proof,
|
|
53
|
+
"checks": {
|
|
54
|
+
"recommended": planned,
|
|
55
|
+
"safeToRun": safe_to_run,
|
|
56
|
+
"deferred": deferred
|
|
57
|
+
},
|
|
58
|
+
"commit": {
|
|
59
|
+
"canCommit": can_commit,
|
|
60
|
+
"blockingFindings": if can_commit { json!([]) } else { serde_json::to_value(&transition.blocking_findings)? }
|
|
61
|
+
},
|
|
62
|
+
"transition": {
|
|
63
|
+
"canComplete": transition.allowed,
|
|
64
|
+
"blockingFindings": transition.blocking_findings
|
|
65
|
+
},
|
|
66
|
+
"nextAction": snapshot_next_action(&status, &proof_plan, can_commit),
|
|
67
|
+
"agentLoop": status.agent_loop,
|
|
68
|
+
"repairPlan": status.repair_plan,
|
|
69
|
+
"findings": status.findings,
|
|
70
|
+
"agentInstruction": if can_commit { "Task is commit-ready; do not edit further before committing." } else { "Follow nextAction and only execute commands listed as safeToRun." }
|
|
71
|
+
});
|
|
72
|
+
let code = task_status_exit_code(&status.findings, &status.proof);
|
|
73
|
+
print_json_with_session(value, session.as_deref())?;
|
|
74
|
+
if args.iter().any(|arg| arg == "--exit-code") {
|
|
75
|
+
std::process::exit(code);
|
|
76
|
+
}
|
|
77
|
+
Ok(())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn merge_commands(left: Value, right: Vec<Value>) -> Vec<Value> {
|
|
81
|
+
let commands = left
|
|
82
|
+
.as_array()
|
|
83
|
+
.cloned()
|
|
84
|
+
.unwrap_or_default()
|
|
85
|
+
.into_iter()
|
|
86
|
+
.chain(right)
|
|
87
|
+
.collect::<Vec<_>>();
|
|
88
|
+
let mut merged = BTreeMap::<String, Value>::new();
|
|
89
|
+
for command in commands {
|
|
90
|
+
let Some(check_id) = command
|
|
91
|
+
.get("checkId")
|
|
92
|
+
.and_then(Value::as_str)
|
|
93
|
+
.map(ToString::to_string)
|
|
94
|
+
else {
|
|
95
|
+
continue;
|
|
96
|
+
};
|
|
97
|
+
if let Some(existing) = merged.get_mut(&check_id) {
|
|
98
|
+
merge_command(existing, &command);
|
|
99
|
+
} else {
|
|
100
|
+
merged.insert(check_id, command);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
merged.into_values().collect()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn merge_command(existing: &mut Value, incoming: &Value) {
|
|
107
|
+
merge_csv_field(existing, incoming, "reason");
|
|
108
|
+
existing["selectionReason"] = existing["reason"].clone();
|
|
109
|
+
merge_string_array_field(existing, incoming, "impactedPaths");
|
|
110
|
+
let safe = existing
|
|
111
|
+
.get("safeToExecute")
|
|
112
|
+
.and_then(Value::as_bool)
|
|
113
|
+
.unwrap_or(false)
|
|
114
|
+
&& incoming
|
|
115
|
+
.get("safeToExecute")
|
|
116
|
+
.and_then(Value::as_bool)
|
|
117
|
+
.unwrap_or(false);
|
|
118
|
+
existing["safeToExecute"] = json!(safe);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn merge_csv_field(existing: &mut Value, incoming: &Value, field: &str) {
|
|
122
|
+
let mut values = BTreeSet::new();
|
|
123
|
+
collect_csv(existing.get(field), &mut values);
|
|
124
|
+
collect_csv(incoming.get(field), &mut values);
|
|
125
|
+
existing[field] = json!(values.into_iter().collect::<Vec<_>>().join(","));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn collect_csv(value: Option<&Value>, values: &mut BTreeSet<String>) {
|
|
129
|
+
if let Some(raw) = value.and_then(Value::as_str) {
|
|
130
|
+
values.extend(
|
|
131
|
+
raw.split(',')
|
|
132
|
+
.map(str::trim)
|
|
133
|
+
.filter(|part| !part.is_empty())
|
|
134
|
+
.map(ToString::to_string),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn merge_string_array_field(existing: &mut Value, incoming: &Value, field: &str) {
|
|
140
|
+
let mut values = BTreeSet::new();
|
|
141
|
+
collect_string_array(existing.get(field), &mut values);
|
|
142
|
+
collect_string_array(incoming.get(field), &mut values);
|
|
143
|
+
existing[field] = json!(values.into_iter().collect::<Vec<_>>());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn collect_string_array(value: Option<&Value>, values: &mut BTreeSet<String>) {
|
|
147
|
+
values.extend(
|
|
148
|
+
value
|
|
149
|
+
.and_then(Value::as_array)
|
|
150
|
+
.into_iter()
|
|
151
|
+
.flatten()
|
|
152
|
+
.filter_map(Value::as_str)
|
|
153
|
+
.map(ToString::to_string),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn snapshot_state(status: &naome_core::TaskStatusReportV1) -> &'static str {
|
|
158
|
+
if status
|
|
159
|
+
.findings
|
|
160
|
+
.iter()
|
|
161
|
+
.any(|finding| finding.id.starts_with("task.git."))
|
|
162
|
+
{
|
|
163
|
+
"blocked_by_git_state"
|
|
164
|
+
} else if !status.scope.out_of_scope_changed_paths.is_empty() {
|
|
165
|
+
"blocked_by_scope_drift"
|
|
166
|
+
} else if !status.proof.missing_checks.is_empty() {
|
|
167
|
+
"blocked_by_missing_proof"
|
|
168
|
+
} else if !status.proof.stale_checks.is_empty() {
|
|
169
|
+
"blocked_by_stale_proof"
|
|
170
|
+
} else if status.agent_loop.can_commit {
|
|
171
|
+
"ready_to_commit"
|
|
172
|
+
} else {
|
|
173
|
+
"healthy"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn editable_paths(status: &naome_core::TaskStatusReportV1) -> Vec<String> {
|
|
178
|
+
if matches!(
|
|
179
|
+
status.state.as_str(),
|
|
180
|
+
"idle" | "missing" | "complete" | "blocked" | "needs_human_review"
|
|
181
|
+
) {
|
|
182
|
+
return Vec::new();
|
|
183
|
+
}
|
|
184
|
+
status.scope.allowed_paths.clone()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fn snapshot_next_action(
|
|
188
|
+
status: &naome_core::TaskStatusReportV1,
|
|
189
|
+
proof_plan: &naome_core::TaskProofPlanReport,
|
|
190
|
+
can_commit: bool,
|
|
191
|
+
) -> Value {
|
|
192
|
+
let action_type = if !status.scope.out_of_scope_changed_paths.is_empty() {
|
|
193
|
+
"repair_scope"
|
|
194
|
+
} else if status
|
|
195
|
+
.findings
|
|
196
|
+
.iter()
|
|
197
|
+
.any(|finding| finding.id.starts_with("task.git."))
|
|
198
|
+
{
|
|
199
|
+
"recover_git"
|
|
200
|
+
} else if !status.proof.missing_checks.is_empty() || !status.proof.stale_checks.is_empty() {
|
|
201
|
+
"run_checks"
|
|
202
|
+
} else if !proof_plan.proof_recording.checks_to_record.is_empty() {
|
|
203
|
+
"record_proof"
|
|
204
|
+
} else if can_commit {
|
|
205
|
+
"commit_ready"
|
|
206
|
+
} else if status.state == "implementing" && status.agent_loop.can_continue_editing {
|
|
207
|
+
"edit"
|
|
208
|
+
} else {
|
|
209
|
+
"none"
|
|
210
|
+
};
|
|
211
|
+
if can_commit && action_type == "commit_ready" {
|
|
212
|
+
return json!({
|
|
213
|
+
"type": "commit_ready",
|
|
214
|
+
"reason": "Task is complete enough to commit; do not continue editing before commit.",
|
|
215
|
+
"commands": [],
|
|
216
|
+
"paths": status.scope.in_scope_changed_paths,
|
|
217
|
+
"checkIds": [],
|
|
218
|
+
"safeToExecute": false,
|
|
219
|
+
"requiresUserApproval": true
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
json!({
|
|
223
|
+
"type": action_type,
|
|
224
|
+
"reason": status.next_action_v2.reason,
|
|
225
|
+
"commands": status.next_action_v2.commands,
|
|
226
|
+
"paths": status.next_action_v2.paths,
|
|
227
|
+
"checkIds": status.next_action_v2.check_ids,
|
|
228
|
+
"safeToExecute": status.next_action_v2.safe_to_execute,
|
|
229
|
+
"requiresUserApproval": status.next_action_v2.requires_user_approval
|
|
230
|
+
})
|
|
231
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
use std::path::Path;
|
|
2
2
|
|
|
3
|
-
use naome_core::{
|
|
3
|
+
use naome_core::{path_matches_any, task_status_report};
|
|
4
4
|
use serde_json::json;
|
|
5
5
|
|
|
6
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};
|
|
7
8
|
|
|
8
9
|
pub(super) fn can_edit(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
9
10
|
let session = agent_session(args)?;
|
|
@@ -13,15 +14,15 @@ pub(super) fn can_edit(root: &Path, args: &[String]) -> Result<(), Box<dyn std::
|
|
|
13
14
|
let (path, path_error) = normalize_requested_path(raw_path);
|
|
14
15
|
let mut findings = Vec::new();
|
|
15
16
|
if let Some(message) = path_error {
|
|
16
|
-
findings.push(
|
|
17
|
+
findings.push(edit_finding("task.edit.unsafe_path", &message, raw_path));
|
|
17
18
|
} else if is_ignored(root, &path) {
|
|
18
|
-
findings.push(
|
|
19
|
+
findings.push(edit_finding(
|
|
19
20
|
"task.edit.ignored_path",
|
|
20
21
|
"Path is ignored by .naomeignore and is outside the active harness context.",
|
|
21
22
|
&path,
|
|
22
23
|
));
|
|
23
24
|
} else if is_control_path(&path) {
|
|
24
|
-
findings.push(
|
|
25
|
+
findings.push(edit_finding(
|
|
25
26
|
"task.edit.control_path",
|
|
26
27
|
"NAOME control files cannot be edited through the autonomous can-edit path.",
|
|
27
28
|
&path,
|
|
@@ -34,13 +35,13 @@ pub(super) fn can_edit(root: &Path, args: &[String]) -> Result<(), Box<dyn std::
|
|
|
34
35
|
"idle" | "missing" | "complete" | "blocked" | "needs_human_review"
|
|
35
36
|
)
|
|
36
37
|
{
|
|
37
|
-
findings.push(
|
|
38
|
+
findings.push(edit_finding(
|
|
38
39
|
"task.edit.no_active_task",
|
|
39
40
|
"No editable active task is available for this path.",
|
|
40
41
|
&path,
|
|
41
42
|
));
|
|
42
43
|
} else if !path_matches_any(&path, &status.scope.allowed_paths) {
|
|
43
|
-
findings.push(
|
|
44
|
+
findings.push(edit_finding(
|
|
44
45
|
"task.edit.out_of_scope",
|
|
45
46
|
"Path is outside activeTask.allowedPaths.",
|
|
46
47
|
&path,
|
|
@@ -61,56 +62,3 @@ pub(super) fn can_edit(root: &Path, args: &[String]) -> Result<(), Box<dyn std::
|
|
|
61
62
|
session.as_deref(),
|
|
62
63
|
)
|
|
63
64
|
}
|
|
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
|
-
}
|
|
@@ -9,6 +9,7 @@ const RECEIPTS_PATH: &str = "naome-task-check-runs.json";
|
|
|
9
9
|
|
|
10
10
|
#[derive(Debug, Clone)]
|
|
11
11
|
pub(in crate::task_commands) struct CheckRunReceipt {
|
|
12
|
+
pub(in crate::task_commands) task_id: Option<String>,
|
|
12
13
|
pub(in crate::task_commands) check_id: String,
|
|
13
14
|
pub(in crate::task_commands) command: String,
|
|
14
15
|
pub(in crate::task_commands) cwd: String,
|
|
@@ -63,9 +64,11 @@ pub(super) fn append_receipt(
|
|
|
63
64
|
Ok(())
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
pub(
|
|
67
|
+
pub(in crate::task_commands) fn changed_paths(
|
|
68
|
+
root: &Path,
|
|
69
|
+
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
|
67
70
|
let output = Command::new("git")
|
|
68
|
-
.args(["status", "--porcelain=v1"])
|
|
71
|
+
.args(["status", "--porcelain=v1", "--untracked-files=all"])
|
|
69
72
|
.current_dir(root)
|
|
70
73
|
.output()?;
|
|
71
74
|
if !output.status.success() {
|
|
@@ -96,6 +99,7 @@ fn receipt_path(root: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
|
96
99
|
|
|
97
100
|
fn receipt_to_value(receipt: &CheckRunReceipt) -> Value {
|
|
98
101
|
json!({
|
|
102
|
+
"taskId": receipt.task_id,
|
|
99
103
|
"checkId": receipt.check_id,
|
|
100
104
|
"command": receipt.command,
|
|
101
105
|
"cwd": receipt.cwd,
|
|
@@ -112,6 +116,10 @@ fn receipt_to_value(receipt: &CheckRunReceipt) -> Value {
|
|
|
112
116
|
|
|
113
117
|
fn receipt_from_value(value: &Value) -> Option<CheckRunReceipt> {
|
|
114
118
|
Some(CheckRunReceipt {
|
|
119
|
+
task_id: value
|
|
120
|
+
.get("taskId")
|
|
121
|
+
.and_then(Value::as_str)
|
|
122
|
+
.map(ToString::to_string),
|
|
115
123
|
check_id: value.get("checkId")?.as_str()?.to_string(),
|
|
116
124
|
command: value.get("command")?.as_str()?.to_string(),
|
|
117
125
|
cwd: value.get("cwd")?.as_str()?.to_string(),
|
|
@@ -9,12 +9,14 @@ mod output;
|
|
|
9
9
|
mod receipts;
|
|
10
10
|
mod verification;
|
|
11
11
|
|
|
12
|
-
pub(super) use receipts::{
|
|
12
|
+
pub(super) use receipts::{
|
|
13
|
+
changed_paths, evidence_fingerprint, successful_receipts, CheckRunReceipt,
|
|
14
|
+
};
|
|
13
15
|
pub(super) use verification::read_verification_check;
|
|
14
16
|
|
|
15
17
|
use super::common::{agent_session, print_json_with_session, value_after};
|
|
16
18
|
use output::{finding, run_check_response};
|
|
17
|
-
use receipts::
|
|
19
|
+
use receipts::append_receipt;
|
|
18
20
|
use verification::safe_command;
|
|
19
21
|
|
|
20
22
|
pub(super) fn run_check_command(
|
|
@@ -83,8 +85,10 @@ pub(super) fn run_check_by_id(
|
|
|
83
85
|
}
|
|
84
86
|
let duration_ms = start.elapsed().as_millis();
|
|
85
87
|
let after = changed_paths(root)?;
|
|
86
|
-
let
|
|
88
|
+
let status = task_status_report(root)?;
|
|
89
|
+
let evidence_paths = status.scope.in_scope_changed_paths;
|
|
87
90
|
let receipt = CheckRunReceipt {
|
|
91
|
+
task_id: status.task_id,
|
|
88
92
|
check_id: check.id.clone(),
|
|
89
93
|
command: check.command.clone(),
|
|
90
94
|
cwd: check.cwd.clone(),
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use naome_core::{task_status_exit_code, task_status_report, task_transition_readiness};
|
|
4
|
+
use naome_core::{TaskStatusFinding, TaskStatusReportV1};
|
|
5
|
+
use serde_json::{json, Value};
|
|
6
|
+
|
|
7
|
+
use super::common::{agent_session, print_json_with_session};
|
|
8
|
+
|
|
9
|
+
pub(super) fn commit_preflight(
|
|
10
|
+
root: &Path,
|
|
11
|
+
args: &[String],
|
|
12
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
13
|
+
let session = agent_session(args)?;
|
|
14
|
+
let status = task_status_report(root)?;
|
|
15
|
+
let transition = task_transition_readiness(root, "complete")?;
|
|
16
|
+
let would_pass = transition.allowed && status.agent_loop.can_commit;
|
|
17
|
+
let blocking = if would_pass {
|
|
18
|
+
Vec::new()
|
|
19
|
+
} else if !transition.blocking_findings.is_empty() {
|
|
20
|
+
transition.blocking_findings.clone()
|
|
21
|
+
} else {
|
|
22
|
+
status.findings.clone()
|
|
23
|
+
};
|
|
24
|
+
let exit_code = commit_preflight_exit_code(would_pass, &status);
|
|
25
|
+
print_json_with_session(
|
|
26
|
+
json!({
|
|
27
|
+
"schema": "naome.task.commit-preflight.v1",
|
|
28
|
+
"wouldPass": would_pass,
|
|
29
|
+
"commitPaths": status.scope.in_scope_changed_paths,
|
|
30
|
+
"blockingFindings": blocking,
|
|
31
|
+
"nextAction": commit_preflight_next_action(would_pass, &blocking, &status)?,
|
|
32
|
+
"agentInstruction": if would_pass { "Commit gate preflight is clean; use the normal NAOME commit path." } else { "Resolve blocking findings before committing." }
|
|
33
|
+
}),
|
|
34
|
+
session.as_deref(),
|
|
35
|
+
)?;
|
|
36
|
+
if args.iter().any(|arg| arg == "--exit-code") {
|
|
37
|
+
std::process::exit(exit_code);
|
|
38
|
+
}
|
|
39
|
+
Ok(())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn commit_preflight_exit_code(would_pass: bool, status: &TaskStatusReportV1) -> i32 {
|
|
43
|
+
if would_pass {
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
let code = task_status_exit_code(&status.findings, &status.proof);
|
|
47
|
+
if code == 0 {
|
|
48
|
+
1
|
|
49
|
+
} else {
|
|
50
|
+
code
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn commit_preflight_next_action(
|
|
55
|
+
would_pass: bool,
|
|
56
|
+
blocking: &[TaskStatusFinding],
|
|
57
|
+
status: &TaskStatusReportV1,
|
|
58
|
+
) -> Result<Value, Box<dyn std::error::Error>> {
|
|
59
|
+
if would_pass {
|
|
60
|
+
return Ok(json!({
|
|
61
|
+
"type": "commit_ready",
|
|
62
|
+
"reason": "Task state, scope, proof, and transition checks are commit-ready.",
|
|
63
|
+
"commands": [],
|
|
64
|
+
"paths": status.scope.in_scope_changed_paths,
|
|
65
|
+
"checkIds": [],
|
|
66
|
+
"safeToExecute": false,
|
|
67
|
+
"requiresUserApproval": true
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
if has_status_blocker(status) {
|
|
71
|
+
return Ok(serde_json::to_value(&status.next_action_v2)?);
|
|
72
|
+
}
|
|
73
|
+
if let Some(primary) = blocking.first() {
|
|
74
|
+
return Ok(json!({
|
|
75
|
+
"type": "blocked",
|
|
76
|
+
"reason": primary.message,
|
|
77
|
+
"commands": [],
|
|
78
|
+
"paths": primary.path.as_ref().map(|path| vec![path.clone()]).unwrap_or_default(),
|
|
79
|
+
"checkIds": [],
|
|
80
|
+
"safeToExecute": false,
|
|
81
|
+
"requiresUserApproval": true
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
Ok(serde_json::to_value(&status.next_action_v2)?)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn has_status_blocker(status: &TaskStatusReportV1) -> bool {
|
|
88
|
+
task_status_exit_code(&status.findings, &status.proof) != 0
|
|
89
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::{json, Value};
|
|
4
|
+
|
|
5
|
+
use super::common::{agent_session, print_json_with_session, read_task_state, write_task_state};
|
|
6
|
+
|
|
7
|
+
pub(super) fn compact_proof(
|
|
8
|
+
root: &Path,
|
|
9
|
+
args: &[String],
|
|
10
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
11
|
+
let session = agent_session(args)?;
|
|
12
|
+
let dry_run = args.iter().any(|arg| arg == "--dry-run");
|
|
13
|
+
let mut state = read_task_state(root)?;
|
|
14
|
+
let before = serde_json::to_vec_pretty(&state)?.len();
|
|
15
|
+
let removed = compact_state(&mut state);
|
|
16
|
+
let after = serde_json::to_vec_pretty(&state)?.len();
|
|
17
|
+
if !dry_run && removed > 0 {
|
|
18
|
+
write_task_state(root, &state)?;
|
|
19
|
+
}
|
|
20
|
+
print_json_with_session(
|
|
21
|
+
json!({
|
|
22
|
+
"schema": "naome.task.compact-proof.v1",
|
|
23
|
+
"dryRun": dry_run,
|
|
24
|
+
"compacted": !dry_run && removed > 0,
|
|
25
|
+
"removedVerboseFields": removed,
|
|
26
|
+
"beforeBytes": before,
|
|
27
|
+
"afterBytes": after,
|
|
28
|
+
"savedBytes": before.saturating_sub(after),
|
|
29
|
+
"agentInstruction": if dry_run { "Review compaction summary; rerun without --dry-run to compact tracked proof." } else { "Tracked proof is compact while preserving check ids, evidence path sets, and fingerprints." }
|
|
30
|
+
}),
|
|
31
|
+
session.as_deref(),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub(super) fn compact_state(state: &mut Value) -> usize {
|
|
36
|
+
let mut removed = 0;
|
|
37
|
+
let Some(active) = state.get_mut("activeTask") else {
|
|
38
|
+
return 0;
|
|
39
|
+
};
|
|
40
|
+
if let Some(batches) = active.get_mut("proofBatches").and_then(Value::as_array_mut) {
|
|
41
|
+
for batch in batches {
|
|
42
|
+
if let Some(proofs) = batch.get_mut("proofs").and_then(Value::as_array_mut) {
|
|
43
|
+
for proof in proofs {
|
|
44
|
+
removed += remove_key(proof, "stdoutSummary");
|
|
45
|
+
removed += remove_key(proof, "stderrSummary");
|
|
46
|
+
removed += remove_key(proof, "durationMs");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if let Some(results) = active.get_mut("proofResults").and_then(Value::as_array_mut) {
|
|
52
|
+
for proof in results {
|
|
53
|
+
removed += remove_key(proof, "stdoutSummary");
|
|
54
|
+
removed += remove_key(proof, "stderrSummary");
|
|
55
|
+
removed += remove_key(proof, "durationMs");
|
|
56
|
+
removed += remove_key(proof, "stdout");
|
|
57
|
+
removed += remove_key(proof, "stderr");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
removed
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn remove_key(value: &mut Value, key: &str) -> usize {
|
|
64
|
+
value
|
|
65
|
+
.as_object_mut()
|
|
66
|
+
.and_then(|object| object.remove(key))
|
|
67
|
+
.map(|_| 1)
|
|
68
|
+
.unwrap_or(0)
|
|
69
|
+
}
|
|
@@ -3,7 +3,7 @@ use std::path::Path;
|
|
|
3
3
|
use naome_core::{task_proof_plan, task_status_report, task_transition_readiness};
|
|
4
4
|
use serde_json::json;
|
|
5
5
|
|
|
6
|
-
use super::common::{agent_session, print_json_with_session};
|
|
6
|
+
use super::common::{agent_session, print_json_with_session, read_task_state, write_task_state};
|
|
7
7
|
|
|
8
8
|
pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
9
9
|
let session = agent_session(args)?;
|
|
@@ -18,12 +18,30 @@ pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
|
|
|
18
18
|
.filter(|item| item.kind == "rerun_check" && item.safe_to_execute)
|
|
19
19
|
{
|
|
20
20
|
if let Some(check_id) = item.check_ids.first() {
|
|
21
|
-
|
|
22
|
-
root,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
let step =
|
|
22
|
+
super::check_run::run_check_by_id(root, check_id, true, session.as_deref())?;
|
|
23
|
+
let failed = step
|
|
24
|
+
.get("exitCode")
|
|
25
|
+
.and_then(serde_json::Value::as_i64)
|
|
26
|
+
.is_some_and(|code| code != 0);
|
|
27
|
+
executed_steps.push(step);
|
|
28
|
+
if failed {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if executed_steps
|
|
34
|
+
.iter()
|
|
35
|
+
.all(|step| step.get("exitCode").and_then(serde_json::Value::as_i64) == Some(0))
|
|
36
|
+
{
|
|
37
|
+
let mut state = read_task_state(root)?;
|
|
38
|
+
if super::compact_proof::compact_state(&mut state) > 0 {
|
|
39
|
+
write_task_state(root, &state)?;
|
|
40
|
+
executed_steps.push(json!({
|
|
41
|
+
"schema": "naome.task.compact-proof.v1",
|
|
42
|
+
"compacted": true,
|
|
43
|
+
"agentInstruction": "Compacted tracked proof after safe check execution."
|
|
44
|
+
}));
|
|
27
45
|
}
|
|
28
46
|
}
|
|
29
47
|
}
|