@lamentis/naome 1.4.1 → 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.
- 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 +9 -5
- package/crates/naome-cli/src/task_commands/can_edit.rs +116 -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 +155 -0
- package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
- package/crates/naome-cli/src/task_commands/check_run.rs +192 -0
- package/crates/naome-cli/src/task_commands/common.rs +39 -1
- package/crates/naome-cli/src/task_commands/complete.rs +43 -0
- package/crates/naome-cli/src/task_commands/loop_control.rs +55 -0
- package/crates/naome-cli/src/task_commands/readiness.rs +14 -10
- package/crates/naome-cli/src/task_commands/record.rs +139 -37
- package/crates/naome-cli/src/task_commands/repair.rs +58 -11
- package/crates/naome-cli/src/task_commands.rs +14 -3
- package/crates/naome-cli/tests/task_cli_agent_controls.rs +9 -16
- 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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
use std::process::Command;
|
|
3
|
+
use std::time::Instant;
|
|
4
|
+
|
|
5
|
+
use naome_core::task_status_report;
|
|
6
|
+
use serde_json::{json, Value};
|
|
7
|
+
|
|
8
|
+
mod output;
|
|
9
|
+
mod receipts;
|
|
10
|
+
mod verification;
|
|
11
|
+
|
|
12
|
+
pub(super) use receipts::{evidence_fingerprint, successful_receipts, CheckRunReceipt};
|
|
13
|
+
pub(super) use verification::read_verification_check;
|
|
14
|
+
|
|
15
|
+
use super::common::{agent_session, print_json_with_session, value_after};
|
|
16
|
+
use output::{finding, run_check_response};
|
|
17
|
+
use receipts::{append_receipt, changed_paths};
|
|
18
|
+
use verification::safe_command;
|
|
19
|
+
|
|
20
|
+
pub(super) fn run_check_command(
|
|
21
|
+
root: &Path,
|
|
22
|
+
args: &[String],
|
|
23
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
24
|
+
let session = agent_session(args)?;
|
|
25
|
+
let Some(check_id) = value_after(args, "--check") else {
|
|
26
|
+
return Err("naome task run-check requires --check <check-id>".into());
|
|
27
|
+
};
|
|
28
|
+
let record = args.iter().any(|arg| arg == "--record-proof");
|
|
29
|
+
let result = run_check_by_id(root, check_id, record, session.as_deref())?;
|
|
30
|
+
print_json_with_session(result, session.as_deref())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub(super) fn run_check_by_id(
|
|
34
|
+
root: &Path,
|
|
35
|
+
check_id: &str,
|
|
36
|
+
record: bool,
|
|
37
|
+
session: Option<&str>,
|
|
38
|
+
) -> Result<Value, Box<dyn std::error::Error>> {
|
|
39
|
+
let Some(check) = read_verification_check(root, check_id)? else {
|
|
40
|
+
return Ok(rejected_check(
|
|
41
|
+
check_id,
|
|
42
|
+
"task.check.unknown",
|
|
43
|
+
format!("Unknown verification check id: {check_id}."),
|
|
44
|
+
"Check id is not declared in .naome/verification.json.",
|
|
45
|
+
session,
|
|
46
|
+
));
|
|
47
|
+
};
|
|
48
|
+
let Some(command) = safe_command(root, &check)? else {
|
|
49
|
+
return Ok(rejected_check(
|
|
50
|
+
check_id,
|
|
51
|
+
"task.check.unsafe_command",
|
|
52
|
+
format!("Check {check_id} is not in the autonomous safe command allowlist."),
|
|
53
|
+
"NAOME refused to execute this check automatically.",
|
|
54
|
+
session,
|
|
55
|
+
));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let before = changed_paths(root)?;
|
|
59
|
+
let start = Instant::now();
|
|
60
|
+
let output = Command::new(&command.program)
|
|
61
|
+
.args(&command.args)
|
|
62
|
+
.current_dir(root.join(&check.cwd))
|
|
63
|
+
.output()?;
|
|
64
|
+
let mut stdout = output.stdout;
|
|
65
|
+
let mut stderr = output.stderr;
|
|
66
|
+
let mut exit_code = output.status.code().unwrap_or(1);
|
|
67
|
+
if check.command == "git diff --check" {
|
|
68
|
+
let cached = Command::new("git")
|
|
69
|
+
.args(["diff", "--cached", "--check"])
|
|
70
|
+
.current_dir(root.join(&check.cwd))
|
|
71
|
+
.output()?;
|
|
72
|
+
if !stdout.is_empty() && !cached.stdout.is_empty() {
|
|
73
|
+
stdout.push(b'\n');
|
|
74
|
+
}
|
|
75
|
+
stdout.extend(cached.stdout);
|
|
76
|
+
if !stderr.is_empty() && !cached.stderr.is_empty() {
|
|
77
|
+
stderr.push(b'\n');
|
|
78
|
+
}
|
|
79
|
+
stderr.extend(cached.stderr);
|
|
80
|
+
if !cached.status.success() {
|
|
81
|
+
exit_code = cached.status.code().unwrap_or(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let duration_ms = start.elapsed().as_millis();
|
|
85
|
+
let after = changed_paths(root)?;
|
|
86
|
+
let evidence_paths = task_status_report(root)?.scope.in_scope_changed_paths;
|
|
87
|
+
let receipt = CheckRunReceipt {
|
|
88
|
+
check_id: check.id.clone(),
|
|
89
|
+
command: check.command.clone(),
|
|
90
|
+
cwd: check.cwd.clone(),
|
|
91
|
+
exit_code,
|
|
92
|
+
checked_at: checked_at(),
|
|
93
|
+
evidence_fingerprint: evidence_fingerprint(root, &evidence_paths)?,
|
|
94
|
+
evidence_paths,
|
|
95
|
+
stdout_summary: bounded_summary(&stdout),
|
|
96
|
+
stderr_summary: bounded_summary(&stderr),
|
|
97
|
+
duration_ms,
|
|
98
|
+
agent_session: session.map(ToString::to_string),
|
|
99
|
+
};
|
|
100
|
+
append_receipt(root, &receipt)?;
|
|
101
|
+
|
|
102
|
+
let (recorded_proof, findings) =
|
|
103
|
+
maybe_record_proof(root, record, exit_code, &check.id, session)?;
|
|
104
|
+
let mut response = run_check_response(
|
|
105
|
+
check_id,
|
|
106
|
+
true,
|
|
107
|
+
Some(exit_code),
|
|
108
|
+
recorded_proof,
|
|
109
|
+
findings,
|
|
110
|
+
if exit_code == 0 {
|
|
111
|
+
"Check executed successfully."
|
|
112
|
+
} else {
|
|
113
|
+
"Check executed and failed; inspect summaries before continuing."
|
|
114
|
+
},
|
|
115
|
+
session,
|
|
116
|
+
);
|
|
117
|
+
response["command"] = json!(check.command);
|
|
118
|
+
response["cwd"] = json!(check.cwd);
|
|
119
|
+
response["durationMs"] = json!(duration_ms);
|
|
120
|
+
response["changedPathsBefore"] = json!(before);
|
|
121
|
+
response["changedPathsAfter"] = json!(after);
|
|
122
|
+
response["stdoutSummary"] = json!(bounded_summary(&stdout));
|
|
123
|
+
response["stderrSummary"] = json!(bounded_summary(&stderr));
|
|
124
|
+
Ok(response)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn maybe_record_proof(
|
|
128
|
+
root: &Path,
|
|
129
|
+
record: bool,
|
|
130
|
+
exit_code: i32,
|
|
131
|
+
check_id: &str,
|
|
132
|
+
session: Option<&str>,
|
|
133
|
+
) -> Result<(bool, Vec<Value>), Box<dyn std::error::Error>> {
|
|
134
|
+
let mut findings = Vec::new();
|
|
135
|
+
if !record {
|
|
136
|
+
return Ok((false, findings));
|
|
137
|
+
}
|
|
138
|
+
if exit_code != 0 {
|
|
139
|
+
findings.push(finding(
|
|
140
|
+
"task.check.failed",
|
|
141
|
+
"Check did not exit 0; proof was not recorded.",
|
|
142
|
+
));
|
|
143
|
+
return Ok((false, findings));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let status = task_status_report(root)?;
|
|
147
|
+
if !status.scope.out_of_scope_changed_paths.is_empty() {
|
|
148
|
+
findings.push(finding(
|
|
149
|
+
"task.scope.out_of_scope_change",
|
|
150
|
+
"Changed paths are outside task scope; proof was not recorded.",
|
|
151
|
+
));
|
|
152
|
+
return Ok((false, findings));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let recorded = super::record::record_receipts_for_checks(
|
|
156
|
+
root,
|
|
157
|
+
&[check_id.to_string()],
|
|
158
|
+
&status.scope.in_scope_changed_paths,
|
|
159
|
+
session,
|
|
160
|
+
)?;
|
|
161
|
+
Ok((recorded, findings))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn rejected_check(
|
|
165
|
+
check_id: &str,
|
|
166
|
+
finding_id: &str,
|
|
167
|
+
message: String,
|
|
168
|
+
instruction: &str,
|
|
169
|
+
session: Option<&str>,
|
|
170
|
+
) -> Value {
|
|
171
|
+
run_check_response(
|
|
172
|
+
check_id,
|
|
173
|
+
false,
|
|
174
|
+
None,
|
|
175
|
+
false,
|
|
176
|
+
vec![finding(finding_id, message)],
|
|
177
|
+
instruction,
|
|
178
|
+
session,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn bounded_summary(bytes: &[u8]) -> String {
|
|
183
|
+
String::from_utf8_lossy(bytes)
|
|
184
|
+
.lines()
|
|
185
|
+
.take(8)
|
|
186
|
+
.collect::<Vec<_>>()
|
|
187
|
+
.join("\n")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn checked_at() -> String {
|
|
191
|
+
"1970-01-01T00:00:00.000Z".to_string()
|
|
192
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use std::fs;
|
|
2
2
|
use std::path::Path;
|
|
3
3
|
|
|
4
|
-
use serde_json::Value;
|
|
4
|
+
use serde_json::{json, Value};
|
|
5
5
|
|
|
6
6
|
pub(super) fn read_task_state(root: &Path) -> Result<Value, Box<dyn std::error::Error>> {
|
|
7
7
|
Ok(serde_json::from_str(&fs::read_to_string(
|
|
@@ -9,11 +9,30 @@ pub(super) fn read_task_state(root: &Path) -> Result<Value, Box<dyn std::error::
|
|
|
9
9
|
)?)?)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
pub(super) fn write_task_state(
|
|
13
|
+
root: &Path,
|
|
14
|
+
state: &Value,
|
|
15
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
16
|
+
fs::write(
|
|
17
|
+
root.join(".naome/task-state.json"),
|
|
18
|
+
format!("{}\n", serde_json::to_string_pretty(state)?),
|
|
19
|
+
)?;
|
|
20
|
+
Ok(())
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
pub(super) fn print_json(value: Value) -> Result<(), Box<dyn std::error::Error>> {
|
|
13
24
|
println!("{}", serde_json::to_string_pretty(&value)?);
|
|
14
25
|
Ok(())
|
|
15
26
|
}
|
|
16
27
|
|
|
28
|
+
pub(super) fn print_json_with_session(
|
|
29
|
+
mut value: Value,
|
|
30
|
+
session: Option<&str>,
|
|
31
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
32
|
+
value["agentSession"] = session.map_or(Value::Null, |session| json!(session));
|
|
33
|
+
print_json(value)
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
pub(super) fn value_after<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
|
|
18
37
|
args.windows(2)
|
|
19
38
|
.find(|window| window[0] == flag)
|
|
@@ -30,3 +49,22 @@ pub(super) fn repeated_values(args: &[String], flag: &str) -> Vec<String> {
|
|
|
30
49
|
values.dedup();
|
|
31
50
|
values
|
|
32
51
|
}
|
|
52
|
+
|
|
53
|
+
pub(super) fn agent_session(args: &[String]) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
|
54
|
+
let Some(value) = value_after(args, "--agent-session") else {
|
|
55
|
+
return Ok(None);
|
|
56
|
+
};
|
|
57
|
+
if value.is_empty()
|
|
58
|
+
|| value.len() > 80
|
|
59
|
+
|| value.contains('/')
|
|
60
|
+
|| value.contains('\\')
|
|
61
|
+
|| !value.is_ascii()
|
|
62
|
+
|| value.chars().any(|character| character.is_control())
|
|
63
|
+
{
|
|
64
|
+
return Err(
|
|
65
|
+
"invalid --agent-session; expected ASCII id up to 80 chars without path separators"
|
|
66
|
+
.into(),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
Ok(Some(value.to_string()))
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use naome_core::task_transition_readiness;
|
|
4
|
+
use serde_json::json;
|
|
5
|
+
|
|
6
|
+
use super::common::{agent_session, print_json_with_session, read_task_state, write_task_state};
|
|
7
|
+
|
|
8
|
+
pub(super) fn complete_task(
|
|
9
|
+
root: &Path,
|
|
10
|
+
args: &[String],
|
|
11
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
12
|
+
if !args.iter().any(|arg| arg == "--from-can-transition") {
|
|
13
|
+
return Err("naome task complete requires --from-can-transition".into());
|
|
14
|
+
}
|
|
15
|
+
let session = agent_session(args)?;
|
|
16
|
+
let readiness = task_transition_readiness(root, "complete")?;
|
|
17
|
+
if !readiness.allowed {
|
|
18
|
+
return print_json_with_session(
|
|
19
|
+
json!({
|
|
20
|
+
"schema": "naome.task.complete.v1",
|
|
21
|
+
"completed": false,
|
|
22
|
+
"blockingFindings": readiness.blocking_findings,
|
|
23
|
+
"agentLoop": readiness.agent_loop,
|
|
24
|
+
"agentInstruction": "Do not complete the task until can-transition allows completion."
|
|
25
|
+
}),
|
|
26
|
+
session.as_deref(),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
let mut state = read_task_state(root)?;
|
|
30
|
+
state["status"] = json!("complete");
|
|
31
|
+
state["updatedAt"] = json!("1970-01-01T00:00:00.000Z");
|
|
32
|
+
write_task_state(root, &state)?;
|
|
33
|
+
print_json_with_session(
|
|
34
|
+
json!({
|
|
35
|
+
"schema": "naome.task.complete.v1",
|
|
36
|
+
"completed": true,
|
|
37
|
+
"blockingFindings": [],
|
|
38
|
+
"agentLoop": readiness.agent_loop,
|
|
39
|
+
"agentInstruction": "Task completed through guarded can-transition readiness."
|
|
40
|
+
}),
|
|
41
|
+
session.as_deref(),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use naome_core::{task_proof_plan, task_status_report, task_transition_readiness};
|
|
4
|
+
use serde_json::json;
|
|
5
|
+
|
|
6
|
+
use super::common::{agent_session, print_json_with_session};
|
|
7
|
+
|
|
8
|
+
pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
9
|
+
let session = agent_session(args)?;
|
|
10
|
+
let execute_safe = args.iter().any(|arg| arg == "--execute-safe");
|
|
11
|
+
let mut executed_steps = Vec::new();
|
|
12
|
+
|
|
13
|
+
if execute_safe {
|
|
14
|
+
let plan = task_proof_plan(root)?;
|
|
15
|
+
for item in plan
|
|
16
|
+
.repair_plan
|
|
17
|
+
.iter()
|
|
18
|
+
.filter(|item| item.kind == "rerun_check" && item.safe_to_execute)
|
|
19
|
+
{
|
|
20
|
+
if let Some(check_id) = item.check_ids.first() {
|
|
21
|
+
executed_steps.push(super::check_run::run_check_by_id(
|
|
22
|
+
root,
|
|
23
|
+
check_id,
|
|
24
|
+
true,
|
|
25
|
+
session.as_deref(),
|
|
26
|
+
)?);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let status = task_status_report(root)?;
|
|
32
|
+
let proof_plan = task_proof_plan(root)?;
|
|
33
|
+
let transition = task_transition_readiness(root, "complete")?;
|
|
34
|
+
let can_commit = json!({
|
|
35
|
+
"schema": "naome.task.commit-readiness.v1",
|
|
36
|
+
"allowed": transition.allowed && status.agent_loop.can_commit,
|
|
37
|
+
"commitPaths": status.scope.in_scope_changed_paths,
|
|
38
|
+
"blockingFindings": transition.blocking_findings,
|
|
39
|
+
"agentLoop": status.agent_loop
|
|
40
|
+
});
|
|
41
|
+
print_json_with_session(
|
|
42
|
+
json!({
|
|
43
|
+
"schema": "naome.task.loop.v1",
|
|
44
|
+
"mode": if execute_safe { "execute_safe" } else { "read_only" },
|
|
45
|
+
"status": status,
|
|
46
|
+
"proofPlan": proof_plan,
|
|
47
|
+
"canTransition": transition,
|
|
48
|
+
"canCommit": can_commit,
|
|
49
|
+
"executedSteps": executed_steps,
|
|
50
|
+
"nextAction": status.next_action_v2,
|
|
51
|
+
"agentInstruction": if execute_safe { "Executed only safe check/proof steps; no edits, git recovery, commit, push, or PR actions were performed." } else { "Read-only loop report; execute only safe plans explicitly marked safe." }
|
|
52
|
+
}),
|
|
53
|
+
session.as_deref(),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -4,20 +4,24 @@ use std::path::Path;
|
|
|
4
4
|
use naome_core::{task_status_report, task_transition_readiness};
|
|
5
5
|
use serde_json::json;
|
|
6
6
|
|
|
7
|
-
use super::common::
|
|
7
|
+
use super::common::{agent_session, print_json_with_session};
|
|
8
8
|
|
|
9
|
-
pub(super) fn can_commit(root: &Path,
|
|
9
|
+
pub(super) fn can_commit(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
10
|
+
let session = agent_session(args)?;
|
|
10
11
|
let status = task_status_report(root)?;
|
|
11
12
|
let transition = task_transition_readiness(root, "complete")?;
|
|
12
13
|
let required = commit_requirements(&status, &transition.blocking_findings);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
print_json_with_session(
|
|
15
|
+
json!({
|
|
16
|
+
"schema": "naome.task.commit-readiness.v1",
|
|
17
|
+
"allowed": transition.allowed && status.agent_loop.can_commit && required.is_empty(),
|
|
18
|
+
"commitPaths": status.scope.in_scope_changed_paths,
|
|
19
|
+
"blockingFindings": transition.blocking_findings,
|
|
20
|
+
"requiredBeforeCommit": required,
|
|
21
|
+
"agentLoop": status.agent_loop
|
|
22
|
+
}),
|
|
23
|
+
session.as_deref(),
|
|
24
|
+
)
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
fn commit_requirements(
|
|
@@ -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,66 @@ 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
|
+
"stdoutSummary": receipt.stdout_summary,
|
|
162
|
+
"stderrSummary": receipt.stderr_summary,
|
|
163
|
+
"durationMs": receipt.duration_ms,
|
|
164
|
+
"agentSession": receipt.agent_session
|
|
165
|
+
})
|
|
107
166
|
}).collect::<Vec<_>>()
|
|
108
167
|
})
|
|
109
168
|
}
|
|
110
169
|
|
|
170
|
+
fn recordable_receipts(
|
|
171
|
+
root: &Path,
|
|
172
|
+
check_ids: &[String],
|
|
173
|
+
paths: &[String],
|
|
174
|
+
) -> Result<(bool, Vec<Value>, Vec<CheckRunReceipt>), Box<dyn std::error::Error>> {
|
|
175
|
+
let receipts = successful_receipts(root)?;
|
|
176
|
+
let current_fingerprint = evidence_fingerprint(root, paths)?;
|
|
177
|
+
let mut selected = Vec::new();
|
|
178
|
+
let mut findings = Vec::new();
|
|
179
|
+
for check_id in check_ids {
|
|
180
|
+
let expected = super::check_run::read_verification_check(root, check_id)?;
|
|
181
|
+
let receipt = receipts
|
|
182
|
+
.iter()
|
|
183
|
+
.rev()
|
|
184
|
+
.find(|receipt| {
|
|
185
|
+
receipt.check_id == *check_id
|
|
186
|
+
&& expected.as_ref().is_some_and(|check| {
|
|
187
|
+
receipt.command == check.command && receipt.cwd == check.cwd
|
|
188
|
+
})
|
|
189
|
+
&& paths.iter().all(|path| {
|
|
190
|
+
receipt
|
|
191
|
+
.evidence_paths
|
|
192
|
+
.iter()
|
|
193
|
+
.any(|evidence| evidence == path)
|
|
194
|
+
})
|
|
195
|
+
&& receipt.evidence_fingerprint == current_fingerprint
|
|
196
|
+
})
|
|
197
|
+
.cloned();
|
|
198
|
+
match receipt {
|
|
199
|
+
Some(receipt) => selected.push(receipt),
|
|
200
|
+
None => findings.push(json!({
|
|
201
|
+
"id": "task.proof.no_recent_success",
|
|
202
|
+
"severity": "error",
|
|
203
|
+
"message": format!("No recent successful safe check evidence covers current task paths for {check_id}."),
|
|
204
|
+
"path": null,
|
|
205
|
+
"suggestedFix": "Run naome task run-check --check <id> --record-proof --json or naome task loop --execute-safe --json.",
|
|
206
|
+
"agentInstruction": "Do not record proof until NAOME has just executed the check successfully."
|
|
207
|
+
})),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
Ok((findings.is_empty(), findings, selected))
|
|
211
|
+
}
|
|
212
|
+
|
|
111
213
|
fn unique_path_set_id(path_sets: &serde_json::Map<String, Value>, base: &str) -> String {
|
|
112
214
|
unique_id(base, |candidate| path_sets.contains_key(candidate))
|
|
113
215
|
}
|