@lamentis/naome 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +13 -0
- 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 +70 -0
- 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 +44 -0
- package/crates/naome-cli/src/task_commands/record.rs +236 -0
- package/crates/naome-cli/src/task_commands/repair.rs +77 -0
- package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
- package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
- package/crates/naome-cli/src/task_commands.rs +80 -1
- package/crates/naome-cli/tests/task_cli.rs +58 -0
- package/crates/naome-cli/tests/task_cli_agent_controls.rs +210 -0
- package/crates/naome-cli/tests/task_cli_control.rs +126 -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 +178 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/lib.rs +7 -2
- package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
- package/crates/naome-core/src/task_state/mod.rs +12 -0
- package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
- package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
- package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
- package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
- package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
- package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
- package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
- package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
- package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
- package/crates/naome-core/src/task_state/status/control.rs +16 -0
- package/crates/naome-core/src/task_state/status/git.rs +133 -0
- package/crates/naome-core/src/task_state/status/model.rs +152 -0
- package/crates/naome-core/src/task_state/status/proof.rs +217 -0
- package/crates/naome-core/src/task_state/status/proof_read.rs +164 -0
- package/crates/naome-core/src/task_state/status/report.rs +148 -0
- package/crates/naome-core/src/task_state/status/report_context.rs +148 -0
- package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
- package/crates/naome-core/src/task_state/status/scope.rs +111 -0
- package/crates/naome-core/src/task_state/status/transition.rs +101 -0
- package/crates/naome-core/src/task_state/status.rs +23 -0
- package/crates/naome-core/src/task_state/status_output.rs +103 -0
- package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
- package/crates/naome-core/tests/task_state_support/states.rs +4 -0
- package/crates/naome-core/tests/task_status.rs +423 -0
- package/crates/naome-core/tests/task_status_git.rs +141 -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
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use serde_json::{json, Value};
|
|
5
|
+
|
|
6
|
+
pub(super) fn read_task_state(root: &Path) -> Result<Value, Box<dyn std::error::Error>> {
|
|
7
|
+
Ok(serde_json::from_str(&fs::read_to_string(
|
|
8
|
+
root.join(".naome/task-state.json"),
|
|
9
|
+
)?)?)
|
|
10
|
+
}
|
|
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
|
+
|
|
23
|
+
pub(super) fn print_json(value: Value) -> Result<(), Box<dyn std::error::Error>> {
|
|
24
|
+
println!("{}", serde_json::to_string_pretty(&value)?);
|
|
25
|
+
Ok(())
|
|
26
|
+
}
|
|
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
|
+
|
|
36
|
+
pub(super) fn value_after<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
|
|
37
|
+
args.windows(2)
|
|
38
|
+
.find(|window| window[0] == flag)
|
|
39
|
+
.map(|window| window[1].as_str())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub(super) fn repeated_values(args: &[String], flag: &str) -> Vec<String> {
|
|
43
|
+
let mut values = args
|
|
44
|
+
.windows(2)
|
|
45
|
+
.filter(|window| window[0] == flag)
|
|
46
|
+
.map(|window| window[1].clone())
|
|
47
|
+
.collect::<Vec<_>>();
|
|
48
|
+
values.sort();
|
|
49
|
+
values.dedup();
|
|
50
|
+
values
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use naome_core::{task_status_report, task_transition_readiness};
|
|
5
|
+
use serde_json::json;
|
|
6
|
+
|
|
7
|
+
use super::common::{agent_session, print_json_with_session};
|
|
8
|
+
|
|
9
|
+
pub(super) fn can_commit(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
10
|
+
let session = agent_session(args)?;
|
|
11
|
+
let status = task_status_report(root)?;
|
|
12
|
+
let transition = task_transition_readiness(root, "complete")?;
|
|
13
|
+
let required = commit_requirements(&status, &transition.blocking_findings);
|
|
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
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn commit_requirements(
|
|
28
|
+
status: &naome_core::TaskStatusReportV1,
|
|
29
|
+
blockers: &[naome_core::TaskStatusFinding],
|
|
30
|
+
) -> Vec<String> {
|
|
31
|
+
let mut requirements = BTreeSet::new();
|
|
32
|
+
for check in status
|
|
33
|
+
.proof
|
|
34
|
+
.missing_checks
|
|
35
|
+
.iter()
|
|
36
|
+
.chain(status.proof.stale_checks.iter())
|
|
37
|
+
{
|
|
38
|
+
requirements.insert(format!("Run and record required check: {check}."));
|
|
39
|
+
}
|
|
40
|
+
for finding in blockers {
|
|
41
|
+
requirements.insert(finding.suggested_fix.clone());
|
|
42
|
+
}
|
|
43
|
+
requirements.into_iter().collect()
|
|
44
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use naome_core::task_proof_plan;
|
|
4
|
+
use serde_json::{json, Value};
|
|
5
|
+
|
|
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
|
+
|
|
9
|
+
pub(super) fn record_proof(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
10
|
+
if !args.iter().any(|arg| arg == "--from-proof-plan") {
|
|
11
|
+
return Err("naome task record-proof requires --from-proof-plan".into());
|
|
12
|
+
}
|
|
13
|
+
let session = agent_session(args)?;
|
|
14
|
+
let dry_run = args.iter().any(|arg| arg == "--dry-run");
|
|
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();
|
|
38
|
+
let recorded_ids = if recorded {
|
|
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
|
+
)?
|
|
46
|
+
} else {
|
|
47
|
+
RecordedProof {
|
|
48
|
+
path_set_id: plan.proof_recording.path_set_id.clone(),
|
|
49
|
+
proof_batch_id: plan.proof_recording.proof_batch_id.clone(),
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
Ok(json!({
|
|
53
|
+
"schema": "naome.task.record-proof.v1",
|
|
54
|
+
"dryRun": dry_run,
|
|
55
|
+
"recorded": recorded,
|
|
56
|
+
"pathSetId": recorded_ids.path_set_id,
|
|
57
|
+
"paths": paths,
|
|
58
|
+
"proofBatchId": recorded_ids.proof_batch_id,
|
|
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
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
struct RecordedProof {
|
|
67
|
+
path_set_id: String,
|
|
68
|
+
proof_batch_id: String,
|
|
69
|
+
}
|
|
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
|
+
|
|
101
|
+
fn write_proof_batch(
|
|
102
|
+
root: &Path,
|
|
103
|
+
path_set_base: &str,
|
|
104
|
+
proof_batch_base: &str,
|
|
105
|
+
paths: &[String],
|
|
106
|
+
receipts: &[CheckRunReceipt],
|
|
107
|
+
) -> Result<RecordedProof, Box<dyn std::error::Error>> {
|
|
108
|
+
let mut state = read_task_state(root)?;
|
|
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());
|
|
113
|
+
let active = state
|
|
114
|
+
.get_mut("activeTask")
|
|
115
|
+
.and_then(Value::as_object_mut)
|
|
116
|
+
.ok_or("task-state has no activeTask object")?;
|
|
117
|
+
let path_sets = active
|
|
118
|
+
.entry("proofPathSets")
|
|
119
|
+
.or_insert_with(|| json!({}))
|
|
120
|
+
.as_object_mut()
|
|
121
|
+
.ok_or("activeTask.proofPathSets must be an object")?;
|
|
122
|
+
let path_set_id = unique_path_set_id(path_sets, path_set_base);
|
|
123
|
+
path_sets.insert(path_set_id.clone(), json!(paths));
|
|
124
|
+
let batches = active
|
|
125
|
+
.entry("proofBatches")
|
|
126
|
+
.or_insert_with(|| json!([]))
|
|
127
|
+
.as_array_mut()
|
|
128
|
+
.ok_or("activeTask.proofBatches must be an array")?;
|
|
129
|
+
let proof_batch_id = unique_proof_batch_id(batches, proof_batch_base);
|
|
130
|
+
batches.push(proof_batch(
|
|
131
|
+
receipts,
|
|
132
|
+
&checked_at,
|
|
133
|
+
&path_set_id,
|
|
134
|
+
&proof_batch_id,
|
|
135
|
+
));
|
|
136
|
+
write_task_state(root, &state)?;
|
|
137
|
+
Ok(RecordedProof {
|
|
138
|
+
path_set_id,
|
|
139
|
+
proof_batch_id,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn proof_batch(
|
|
144
|
+
receipts: &[CheckRunReceipt],
|
|
145
|
+
checked_at: &str,
|
|
146
|
+
path_set_id: &str,
|
|
147
|
+
proof_batch_id: &str,
|
|
148
|
+
) -> Value {
|
|
149
|
+
json!({
|
|
150
|
+
"id": proof_batch_id,
|
|
151
|
+
"checkedAt": checked_at,
|
|
152
|
+
"evidencePathSet": path_set_id,
|
|
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
|
+
})
|
|
166
|
+
}).collect::<Vec<_>>()
|
|
167
|
+
})
|
|
168
|
+
}
|
|
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
|
+
|
|
213
|
+
fn unique_path_set_id(path_sets: &serde_json::Map<String, Value>, base: &str) -> String {
|
|
214
|
+
unique_id(base, |candidate| path_sets.contains_key(candidate))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fn unique_proof_batch_id(batches: &[Value], base: &str) -> String {
|
|
218
|
+
unique_id(base, |candidate| {
|
|
219
|
+
batches
|
|
220
|
+
.iter()
|
|
221
|
+
.any(|batch| batch.get("id").and_then(Value::as_str) == Some(candidate))
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn unique_id(base: &str, exists: impl Fn(&str) -> bool) -> String {
|
|
226
|
+
if !exists(base) {
|
|
227
|
+
return base.to_string();
|
|
228
|
+
}
|
|
229
|
+
for index in 2.. {
|
|
230
|
+
let candidate = format!("{base}-{index}");
|
|
231
|
+
if !exists(&candidate) {
|
|
232
|
+
return candidate;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
unreachable!("unbounded proof id suffix search must find an available id")
|
|
236
|
+
}
|