@lamentis/naome 1.4.4 → 1.4.5
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/task_commands/agent_snapshot.rs +19 -70
- package/crates/naome-cli/src/task_commands/commit_preflight.rs +29 -41
- package/crates/naome-cli/src/task_commands/loop_control.rs +40 -9
- package/crates/naome-cli/src/task_commands/planner/checks.rs +4 -2
- package/crates/naome-cli/src/task_commands/preflight.rs +8 -2
- package/crates/naome-cli/src/task_commands/record.rs +9 -0
- package/crates/naome-cli/src/task_commands/single_pass_action.rs +215 -0
- package/crates/naome-cli/src/task_commands/single_pass_action_fields.rs +101 -0
- package/crates/naome-cli/src/task_commands.rs +2 -0
- package/crates/naome-cli/tests/task_cli_fast_flow.rs +147 -2
- package/crates/naome-cli/tests/task_cli_review_fixes.rs +107 -0
- 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.5"
|
|
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.5"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
|
@@ -8,6 +8,8 @@ use serde_json::{json, Value};
|
|
|
8
8
|
|
|
9
9
|
use super::common::{agent_session, print_json_with_session};
|
|
10
10
|
use super::planner;
|
|
11
|
+
pub(super) use super::single_pass_action::single_pass_next_action;
|
|
12
|
+
use super::single_pass_action_fields::{collect_csv, collect_string_array};
|
|
11
13
|
|
|
12
14
|
pub(super) fn agent_snapshot(
|
|
13
15
|
root: &Path,
|
|
@@ -27,6 +29,21 @@ pub(super) fn agent_snapshot(
|
|
|
27
29
|
);
|
|
28
30
|
let (safe_to_run, deferred) = planner::split_safe_commands(&planned);
|
|
29
31
|
let can_commit = transition.allowed && status.agent_loop.can_commit;
|
|
32
|
+
let can_record_proof = super::record::can_record_receipts(
|
|
33
|
+
root,
|
|
34
|
+
&proof_plan.proof_recording.checks_to_record,
|
|
35
|
+
&proof_plan.proof_recording.paths,
|
|
36
|
+
)?;
|
|
37
|
+
let next_action = single_pass_next_action(
|
|
38
|
+
&status,
|
|
39
|
+
&proof_plan,
|
|
40
|
+
&transition,
|
|
41
|
+
&planned,
|
|
42
|
+
&safe_to_run,
|
|
43
|
+
&deferred,
|
|
44
|
+
can_commit,
|
|
45
|
+
can_record_proof,
|
|
46
|
+
);
|
|
30
47
|
let value = json!({
|
|
31
48
|
"schema": "naome.task.agent-snapshot.v1",
|
|
32
49
|
"state": snapshot_state(&status),
|
|
@@ -63,7 +80,7 @@ pub(super) fn agent_snapshot(
|
|
|
63
80
|
"canComplete": transition.allowed,
|
|
64
81
|
"blockingFindings": transition.blocking_findings
|
|
65
82
|
},
|
|
66
|
-
"nextAction":
|
|
83
|
+
"nextAction": next_action,
|
|
67
84
|
"agentLoop": status.agent_loop,
|
|
68
85
|
"repairPlan": status.repair_plan,
|
|
69
86
|
"findings": status.findings,
|
|
@@ -77,7 +94,7 @@ pub(super) fn agent_snapshot(
|
|
|
77
94
|
Ok(())
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
fn merge_commands(left: Value, right: Vec<Value>) -> Vec<Value> {
|
|
97
|
+
pub(super) fn merge_commands(left: Value, right: Vec<Value>) -> Vec<Value> {
|
|
81
98
|
let commands = left
|
|
82
99
|
.as_array()
|
|
83
100
|
.cloned()
|
|
@@ -125,17 +142,6 @@ fn merge_csv_field(existing: &mut Value, incoming: &Value, field: &str) {
|
|
|
125
142
|
existing[field] = json!(values.into_iter().collect::<Vec<_>>().join(","));
|
|
126
143
|
}
|
|
127
144
|
|
|
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
145
|
fn merge_string_array_field(existing: &mut Value, incoming: &Value, field: &str) {
|
|
140
146
|
let mut values = BTreeSet::new();
|
|
141
147
|
collect_string_array(existing.get(field), &mut values);
|
|
@@ -143,17 +149,6 @@ fn merge_string_array_field(existing: &mut Value, incoming: &Value, field: &str)
|
|
|
143
149
|
existing[field] = json!(values.into_iter().collect::<Vec<_>>());
|
|
144
150
|
}
|
|
145
151
|
|
|
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
152
|
fn snapshot_state(status: &naome_core::TaskStatusReportV1) -> &'static str {
|
|
158
153
|
if status
|
|
159
154
|
.findings
|
|
@@ -183,49 +178,3 @@ fn editable_paths(status: &naome_core::TaskStatusReportV1) -> Vec<String> {
|
|
|
183
178
|
}
|
|
184
179
|
status.scope.allowed_paths.clone()
|
|
185
180
|
}
|
|
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,10 +1,12 @@
|
|
|
1
1
|
use std::path::Path;
|
|
2
2
|
|
|
3
|
-
use naome_core::{
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
use naome_core::{
|
|
4
|
+
task_status_exit_code, task_status_report, task_transition_readiness, TaskStatusReportV1,
|
|
5
|
+
};
|
|
6
|
+
use serde_json::json;
|
|
6
7
|
|
|
7
8
|
use super::common::{agent_session, print_json_with_session};
|
|
9
|
+
use super::{agent_snapshot, planner};
|
|
8
10
|
|
|
9
11
|
pub(super) fn commit_preflight(
|
|
10
12
|
root: &Path,
|
|
@@ -12,8 +14,18 @@ pub(super) fn commit_preflight(
|
|
|
12
14
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
13
15
|
let session = agent_session(args)?;
|
|
14
16
|
let status = task_status_report(root)?;
|
|
17
|
+
let proof_plan = naome_core::task_proof_plan(root)?;
|
|
15
18
|
let transition = task_transition_readiness(root, "complete")?;
|
|
16
19
|
let would_pass = transition.allowed && status.agent_loop.can_commit;
|
|
20
|
+
let planned = agent_snapshot::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);
|
|
17
29
|
let blocking = if would_pass {
|
|
18
30
|
Vec::new()
|
|
19
31
|
} else if !transition.blocking_findings.is_empty() {
|
|
@@ -28,7 +40,20 @@ pub(super) fn commit_preflight(
|
|
|
28
40
|
"wouldPass": would_pass,
|
|
29
41
|
"commitPaths": status.scope.in_scope_changed_paths,
|
|
30
42
|
"blockingFindings": blocking,
|
|
31
|
-
"nextAction":
|
|
43
|
+
"nextAction": agent_snapshot::single_pass_next_action(
|
|
44
|
+
&status,
|
|
45
|
+
&proof_plan,
|
|
46
|
+
&transition,
|
|
47
|
+
&planned,
|
|
48
|
+
&safe_to_run,
|
|
49
|
+
&deferred,
|
|
50
|
+
would_pass,
|
|
51
|
+
super::record::can_record_receipts(
|
|
52
|
+
root,
|
|
53
|
+
&proof_plan.proof_recording.checks_to_record,
|
|
54
|
+
&proof_plan.proof_recording.paths
|
|
55
|
+
)?
|
|
56
|
+
),
|
|
32
57
|
"agentInstruction": if would_pass { "Commit gate preflight is clean; use the normal NAOME commit path." } else { "Resolve blocking findings before committing." }
|
|
33
58
|
}),
|
|
34
59
|
session.as_deref(),
|
|
@@ -50,40 +75,3 @@ fn commit_preflight_exit_code(would_pass: bool, status: &TaskStatusReportV1) ->
|
|
|
50
75
|
code
|
|
51
76
|
}
|
|
52
77
|
}
|
|
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
|
-
}
|
|
@@ -4,6 +4,7 @@ use naome_core::{task_proof_plan, task_status_report, task_transition_readiness}
|
|
|
4
4
|
use serde_json::json;
|
|
5
5
|
|
|
6
6
|
use super::common::{agent_session, print_json_with_session, read_task_state, write_task_state};
|
|
7
|
+
use super::{agent_snapshot, planner};
|
|
7
8
|
|
|
8
9
|
pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
9
10
|
let session = agent_session(args)?;
|
|
@@ -11,13 +12,19 @@ pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
|
|
|
11
12
|
let mut executed_steps = Vec::new();
|
|
12
13
|
|
|
13
14
|
if execute_safe {
|
|
14
|
-
let
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
let status = task_status_report(root)?;
|
|
16
|
+
let proof_plan = task_proof_plan(root)?;
|
|
17
|
+
let planned = agent_snapshot::merge_commands(
|
|
18
|
+
serde_json::to_value(&proof_plan.recommended_commands)?,
|
|
19
|
+
planner::planned_commands(
|
|
20
|
+
root,
|
|
21
|
+
&status.scope.in_scope_changed_paths,
|
|
22
|
+
Some(&status.proof),
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
let (safe_to_run, _deferred) = planner::split_safe_commands(&planned);
|
|
26
|
+
for command in safe_to_run {
|
|
27
|
+
if let Some(check_id) = command.get("checkId").and_then(serde_json::Value::as_str) {
|
|
21
28
|
let step =
|
|
22
29
|
super::check_run::run_check_by_id(root, check_id, true, session.as_deref())?;
|
|
23
30
|
let failed = step
|
|
@@ -49,9 +56,33 @@ pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
|
|
|
49
56
|
let status = task_status_report(root)?;
|
|
50
57
|
let proof_plan = task_proof_plan(root)?;
|
|
51
58
|
let transition = task_transition_readiness(root, "complete")?;
|
|
59
|
+
let planned = agent_snapshot::merge_commands(
|
|
60
|
+
serde_json::to_value(&proof_plan.recommended_commands)?,
|
|
61
|
+
planner::planned_commands(
|
|
62
|
+
root,
|
|
63
|
+
&status.scope.in_scope_changed_paths,
|
|
64
|
+
Some(&status.proof),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
let (safe_to_run, deferred) = planner::split_safe_commands(&planned);
|
|
68
|
+
let commit_ready = transition.allowed && status.agent_loop.can_commit;
|
|
69
|
+
let next_action = agent_snapshot::single_pass_next_action(
|
|
70
|
+
&status,
|
|
71
|
+
&proof_plan,
|
|
72
|
+
&transition,
|
|
73
|
+
&planned,
|
|
74
|
+
&safe_to_run,
|
|
75
|
+
&deferred,
|
|
76
|
+
commit_ready,
|
|
77
|
+
super::record::can_record_receipts(
|
|
78
|
+
root,
|
|
79
|
+
&proof_plan.proof_recording.checks_to_record,
|
|
80
|
+
&proof_plan.proof_recording.paths,
|
|
81
|
+
)?,
|
|
82
|
+
);
|
|
52
83
|
let can_commit = json!({
|
|
53
84
|
"schema": "naome.task.commit-readiness.v1",
|
|
54
|
-
"allowed":
|
|
85
|
+
"allowed": commit_ready,
|
|
55
86
|
"commitPaths": status.scope.in_scope_changed_paths,
|
|
56
87
|
"blockingFindings": transition.blocking_findings,
|
|
57
88
|
"agentLoop": status.agent_loop
|
|
@@ -65,7 +96,7 @@ pub(super) fn task_loop(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
|
|
|
65
96
|
"canTransition": transition,
|
|
66
97
|
"canCommit": can_commit,
|
|
67
98
|
"executedSteps": executed_steps,
|
|
68
|
-
"nextAction":
|
|
99
|
+
"nextAction": next_action,
|
|
69
100
|
"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." }
|
|
70
101
|
}),
|
|
71
102
|
session.as_deref(),
|
|
@@ -119,6 +119,9 @@ fn proof_reason(reasons: &BTreeSet<String>) -> &'static str {
|
|
|
119
119
|
|
|
120
120
|
fn check_is_impacted(check: &VerificationCheck, path: &str) -> bool {
|
|
121
121
|
let command = check.command.as_str();
|
|
122
|
+
if command == "git diff --check" {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
122
125
|
match impact_kind(path) {
|
|
123
126
|
"changed_task_state" => {
|
|
124
127
|
command.contains("check-task-state") || command.contains("test:task-state")
|
|
@@ -137,8 +140,7 @@ fn check_is_impacted(check: &VerificationCheck, path: &str) -> bool {
|
|
|
137
140
|
|| command.contains("test:")
|
|
138
141
|
}
|
|
139
142
|
_ => {
|
|
140
|
-
command
|
|
141
|
-
|| command.contains("quality check")
|
|
143
|
+
command.contains("quality check")
|
|
142
144
|
|| command.contains("semantic check")
|
|
143
145
|
|| command.contains("arch validate")
|
|
144
146
|
}
|
|
@@ -29,6 +29,11 @@ pub(super) fn preflight(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
|
|
|
29
29
|
let recommended = planner::planned_commands(root, &normalized_paths, Some(&status.proof));
|
|
30
30
|
let (safe, _deferred) = planner::split_safe_commands(&recommended);
|
|
31
31
|
let blocked = !findings.is_empty() || !must_not_edit.is_empty();
|
|
32
|
+
let next_action_paths = if blocked {
|
|
33
|
+
must_not_edit.clone()
|
|
34
|
+
} else {
|
|
35
|
+
normalized_paths.clone()
|
|
36
|
+
};
|
|
32
37
|
print_json_with_session(
|
|
33
38
|
json!({
|
|
34
39
|
"schema": "naome.task.preflight.v1",
|
|
@@ -40,8 +45,9 @@ pub(super) fn preflight(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
|
|
|
40
45
|
"findings": findings,
|
|
41
46
|
"nextAction": {
|
|
42
47
|
"type": if blocked { "blocked" } else { "edit" },
|
|
43
|
-
"
|
|
44
|
-
"
|
|
48
|
+
"actionId": if blocked { "preflight.blocked" } else { "preflight.edit_allowed" },
|
|
49
|
+
"reasonCodes": if blocked { vec!["preflight-blocked"] } else { vec!["preflight-clean"] },
|
|
50
|
+
"paths": next_action_paths,
|
|
45
51
|
"commands": [],
|
|
46
52
|
"checkIds": [],
|
|
47
53
|
"safeToExecute": !blocked,
|
|
@@ -98,6 +98,15 @@ pub(super) fn record_receipts_for_checks(
|
|
|
98
98
|
Ok(true)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
pub(super) fn can_record_receipts(
|
|
102
|
+
root: &Path,
|
|
103
|
+
check_ids: &[String],
|
|
104
|
+
paths: &[String],
|
|
105
|
+
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
106
|
+
let (can_record, _findings, receipts) = recordable_receipts(root, check_ids, paths)?;
|
|
107
|
+
Ok(can_record && !receipts.is_empty())
|
|
108
|
+
}
|
|
109
|
+
|
|
101
110
|
fn write_proof_batch(
|
|
102
111
|
root: &Path,
|
|
103
112
|
path_set_base: &str,
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
use naome_core::{TaskProofPlanReport, TaskStatusReportV1, TransitionReadinessReport};
|
|
2
|
+
use serde_json::{json, Value};
|
|
3
|
+
|
|
4
|
+
use super::single_pass_action_fields::{
|
|
5
|
+
action, check_ids, impacted_paths, primary_finding_id, reason_codes,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
pub(super) fn single_pass_next_action(
|
|
9
|
+
status: &TaskStatusReportV1,
|
|
10
|
+
proof_plan: &TaskProofPlanReport,
|
|
11
|
+
transition: &TransitionReadinessReport,
|
|
12
|
+
planned: &[Value],
|
|
13
|
+
safe_to_run: &[Value],
|
|
14
|
+
deferred: &[Value],
|
|
15
|
+
can_commit: bool,
|
|
16
|
+
can_record_proof: bool,
|
|
17
|
+
) -> Value {
|
|
18
|
+
human_review_action(transition)
|
|
19
|
+
.or_else(|| git_recovery_action(status))
|
|
20
|
+
.or_else(|| scope_drift_action(status))
|
|
21
|
+
.or_else(|| record_action(proof_plan, can_record_proof))
|
|
22
|
+
.or_else(|| proof_action(status, safe_to_run, deferred))
|
|
23
|
+
.or_else(|| transition_blocker_action(transition))
|
|
24
|
+
.unwrap_or_else(|| {
|
|
25
|
+
if can_commit {
|
|
26
|
+
commit_action(status)
|
|
27
|
+
} else if status.state == "implementing" && status.agent_loop.can_continue_editing {
|
|
28
|
+
edit_action(status)
|
|
29
|
+
} else {
|
|
30
|
+
no_action(status, planned)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn human_review_action(transition: &TransitionReadinessReport) -> Option<Value> {
|
|
36
|
+
transition
|
|
37
|
+
.blocking_findings
|
|
38
|
+
.iter()
|
|
39
|
+
.find(|finding| finding.id == "task.transition.human_review_required")
|
|
40
|
+
.map(|finding| {
|
|
41
|
+
blocking_action(
|
|
42
|
+
"human_review",
|
|
43
|
+
"human_review.required",
|
|
44
|
+
&["human-review-required"],
|
|
45
|
+
finding.path.iter().cloned().collect(),
|
|
46
|
+
&finding.id,
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn git_recovery_action(status: &TaskStatusReportV1) -> Option<Value> {
|
|
52
|
+
status
|
|
53
|
+
.findings
|
|
54
|
+
.iter()
|
|
55
|
+
.any(|finding| finding.id.starts_with("task.git."))
|
|
56
|
+
.then(|| {
|
|
57
|
+
action(
|
|
58
|
+
"recover_git_state",
|
|
59
|
+
"git.recover_state",
|
|
60
|
+
&["git-recovery-required"],
|
|
61
|
+
&[],
|
|
62
|
+
Vec::new(),
|
|
63
|
+
&[],
|
|
64
|
+
false,
|
|
65
|
+
true,
|
|
66
|
+
primary_finding_id(status, "task.git.").as_deref(),
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn scope_drift_action(status: &TaskStatusReportV1) -> Option<Value> {
|
|
72
|
+
(!status.scope.out_of_scope_changed_paths.is_empty()).then(|| {
|
|
73
|
+
action(
|
|
74
|
+
"repair_scope",
|
|
75
|
+
"scope.repair_drift",
|
|
76
|
+
&["scope-drift"],
|
|
77
|
+
&[],
|
|
78
|
+
status.scope.out_of_scope_changed_paths.clone(),
|
|
79
|
+
&[],
|
|
80
|
+
false,
|
|
81
|
+
true,
|
|
82
|
+
primary_finding_id(status, "task.scope.out_of_scope_change").as_deref(),
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn proof_action(status: &TaskStatusReportV1, safe: &[Value], deferred: &[Value]) -> Option<Value> {
|
|
88
|
+
(!status.proof.missing_checks.is_empty() || !status.proof.stale_checks.is_empty()).then(|| {
|
|
89
|
+
let commands = if safe.is_empty() { deferred } else { safe };
|
|
90
|
+
action(
|
|
91
|
+
"run_checks",
|
|
92
|
+
"proof.run_required_checks",
|
|
93
|
+
&reason_codes(commands, &status.proof),
|
|
94
|
+
commands,
|
|
95
|
+
impacted_paths(commands, &status.scope.in_scope_changed_paths),
|
|
96
|
+
&check_ids(commands, &status.proof),
|
|
97
|
+
!safe.is_empty(),
|
|
98
|
+
safe.is_empty(),
|
|
99
|
+
primary_finding_id(status, "task.proof.")
|
|
100
|
+
.or_else(|| primary_finding_id(status, "task.scope.missing_evidence"))
|
|
101
|
+
.as_deref(),
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn record_action(proof_plan: &TaskProofPlanReport, can_record_proof: bool) -> Option<Value> {
|
|
107
|
+
can_record_proof.then(|| {
|
|
108
|
+
action(
|
|
109
|
+
"record_proof",
|
|
110
|
+
"proof.record_from_receipts",
|
|
111
|
+
&["proof-recording-available"],
|
|
112
|
+
&[json!({
|
|
113
|
+
"command": "node .naome/bin/naome.js task record-proof --from-proof-plan --json",
|
|
114
|
+
"cwd": ".",
|
|
115
|
+
"safeToExecute": true
|
|
116
|
+
})],
|
|
117
|
+
proof_plan.proof_recording.paths.clone(),
|
|
118
|
+
&proof_plan.proof_recording.checks_to_record,
|
|
119
|
+
true,
|
|
120
|
+
false,
|
|
121
|
+
None,
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn transition_blocker_action(transition: &TransitionReadinessReport) -> Option<Value> {
|
|
127
|
+
transition
|
|
128
|
+
.blocking_findings
|
|
129
|
+
.iter()
|
|
130
|
+
.find(|finding| unhandled_transition_blocker(&finding.id))
|
|
131
|
+
.map(|finding| {
|
|
132
|
+
blocking_action(
|
|
133
|
+
"blocked",
|
|
134
|
+
"transition.blocked",
|
|
135
|
+
&["transition-blocked"],
|
|
136
|
+
finding.path.iter().cloned().collect(),
|
|
137
|
+
&finding.id,
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn blocking_action(
|
|
143
|
+
action_type: &str,
|
|
144
|
+
action_id: &str,
|
|
145
|
+
reason_codes: &[&str],
|
|
146
|
+
paths: Vec<String>,
|
|
147
|
+
finding_id: &str,
|
|
148
|
+
) -> Value {
|
|
149
|
+
action(
|
|
150
|
+
action_type,
|
|
151
|
+
action_id,
|
|
152
|
+
reason_codes,
|
|
153
|
+
&[],
|
|
154
|
+
paths,
|
|
155
|
+
&[],
|
|
156
|
+
false,
|
|
157
|
+
true,
|
|
158
|
+
Some(finding_id),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn unhandled_transition_blocker(id: &str) -> bool {
|
|
163
|
+
!(id == "task.transition.human_review_required"
|
|
164
|
+
|| id.starts_with("task.git.")
|
|
165
|
+
|| id.starts_with("task.proof.")
|
|
166
|
+
|| id == "task.scope.out_of_scope_change"
|
|
167
|
+
|| id == "task.scope.missing_evidence"
|
|
168
|
+
|| id == "task.transition.stale_proof")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn commit_action(status: &TaskStatusReportV1) -> Value {
|
|
172
|
+
action(
|
|
173
|
+
"commit_ready",
|
|
174
|
+
"commit.ready",
|
|
175
|
+
&["commit-ready"],
|
|
176
|
+
&[],
|
|
177
|
+
status.scope.in_scope_changed_paths.clone(),
|
|
178
|
+
&[],
|
|
179
|
+
false,
|
|
180
|
+
true,
|
|
181
|
+
None,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn edit_action(status: &TaskStatusReportV1) -> Value {
|
|
186
|
+
action(
|
|
187
|
+
"edit",
|
|
188
|
+
"edit.required",
|
|
189
|
+
&["edit-required"],
|
|
190
|
+
&[json!({
|
|
191
|
+
"command": "node .naome/bin/naome.js task preflight --path <path> --json",
|
|
192
|
+
"cwd": ".",
|
|
193
|
+
"safeToExecute": false
|
|
194
|
+
})],
|
|
195
|
+
status.scope.allowed_paths.clone(),
|
|
196
|
+
&[],
|
|
197
|
+
false,
|
|
198
|
+
true,
|
|
199
|
+
None,
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn no_action(status: &TaskStatusReportV1, planned: &[Value]) -> Value {
|
|
204
|
+
action(
|
|
205
|
+
"none",
|
|
206
|
+
"task.no_action",
|
|
207
|
+
&["no-action"],
|
|
208
|
+
planned,
|
|
209
|
+
status.scope.in_scope_changed_paths.clone(),
|
|
210
|
+
&[],
|
|
211
|
+
false,
|
|
212
|
+
false,
|
|
213
|
+
None,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
|
|
3
|
+
use naome_core::{TaskProofStatus, TaskStatusReportV1};
|
|
4
|
+
use serde_json::{json, Value};
|
|
5
|
+
|
|
6
|
+
pub(super) fn action(
|
|
7
|
+
action_type: &str,
|
|
8
|
+
action_id: &str,
|
|
9
|
+
reason_codes: &[impl AsRef<str>],
|
|
10
|
+
commands: &[Value],
|
|
11
|
+
paths: Vec<String>,
|
|
12
|
+
check_ids: &[String],
|
|
13
|
+
safe_to_execute: bool,
|
|
14
|
+
requires_user_approval: bool,
|
|
15
|
+
primary_finding_id: Option<&str>,
|
|
16
|
+
) -> Value {
|
|
17
|
+
json!({
|
|
18
|
+
"type": action_type,
|
|
19
|
+
"actionId": action_id,
|
|
20
|
+
"reasonCodes": reason_codes.iter().map(|code| code.as_ref()).collect::<Vec<_>>(),
|
|
21
|
+
"primaryFindingId": primary_finding_id,
|
|
22
|
+
"commands": commands,
|
|
23
|
+
"paths": paths,
|
|
24
|
+
"checkIds": check_ids,
|
|
25
|
+
"safeToExecute": safe_to_execute,
|
|
26
|
+
"requiresUserApproval": requires_user_approval
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub(super) fn primary_finding_id(status: &TaskStatusReportV1, prefix: &str) -> Option<String> {
|
|
31
|
+
status
|
|
32
|
+
.findings
|
|
33
|
+
.iter()
|
|
34
|
+
.find(|finding| finding.id.starts_with(prefix))
|
|
35
|
+
.map(|finding| finding.id.clone())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub(super) fn reason_codes(commands: &[Value], proof: &TaskProofStatus) -> Vec<String> {
|
|
39
|
+
let mut codes = BTreeSet::new();
|
|
40
|
+
for command in commands {
|
|
41
|
+
collect_csv(command.get("reason"), &mut codes);
|
|
42
|
+
collect_csv(command.get("selectionReason"), &mut codes);
|
|
43
|
+
}
|
|
44
|
+
if !proof.missing_checks.is_empty() {
|
|
45
|
+
codes.insert("missing-proof".to_string());
|
|
46
|
+
}
|
|
47
|
+
if !proof.stale_checks.is_empty() {
|
|
48
|
+
codes.insert("stale-proof".to_string());
|
|
49
|
+
}
|
|
50
|
+
codes.into_iter().collect()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(super) fn impacted_paths(commands: &[Value], fallback: &[String]) -> Vec<String> {
|
|
54
|
+
let mut paths = BTreeSet::new();
|
|
55
|
+
for command in commands {
|
|
56
|
+
collect_string_array(command.get("impactedPaths"), &mut paths);
|
|
57
|
+
}
|
|
58
|
+
if paths.is_empty() {
|
|
59
|
+
paths.extend(fallback.iter().cloned());
|
|
60
|
+
}
|
|
61
|
+
paths.into_iter().collect()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub(super) fn check_ids(commands: &[Value], proof: &TaskProofStatus) -> Vec<String> {
|
|
65
|
+
let mut ids = BTreeSet::new();
|
|
66
|
+
ids.extend(commands.iter().filter_map(check_id));
|
|
67
|
+
if ids.is_empty() {
|
|
68
|
+
ids.extend(proof.missing_checks.iter().cloned());
|
|
69
|
+
ids.extend(proof.stale_checks.iter().cloned());
|
|
70
|
+
}
|
|
71
|
+
ids.into_iter().collect()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn check_id(command: &Value) -> Option<String> {
|
|
75
|
+
command
|
|
76
|
+
.get("checkId")
|
|
77
|
+
.and_then(Value::as_str)
|
|
78
|
+
.map(ToString::to_string)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub(super) fn collect_csv(value: Option<&Value>, values: &mut BTreeSet<String>) {
|
|
82
|
+
if let Some(raw) = value.and_then(Value::as_str) {
|
|
83
|
+
values.extend(
|
|
84
|
+
raw.split(',')
|
|
85
|
+
.map(str::trim)
|
|
86
|
+
.filter(|part| !part.is_empty())
|
|
87
|
+
.map(ToString::to_string),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub(super) fn collect_string_array(value: Option<&Value>, values: &mut BTreeSet<String>) {
|
|
93
|
+
values.extend(
|
|
94
|
+
value
|
|
95
|
+
.and_then(Value::as_array)
|
|
96
|
+
.into_iter()
|
|
97
|
+
.flatten()
|
|
98
|
+
.filter_map(Value::as_str)
|
|
99
|
+
.map(ToString::to_string),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -21,6 +21,18 @@ fn agent_snapshot_reports_missing_proof_and_safe_commands() {
|
|
|
21
21
|
assert_eq!(snapshot["schema"], "naome.task.agent-snapshot.v1");
|
|
22
22
|
assert_eq!(snapshot["proof"]["missingChecks"], json!(["diff-check"]));
|
|
23
23
|
assert_eq!(snapshot["nextAction"]["type"], "run_checks");
|
|
24
|
+
assert_eq!(
|
|
25
|
+
snapshot["nextAction"]["actionId"],
|
|
26
|
+
"proof.run_required_checks"
|
|
27
|
+
);
|
|
28
|
+
assert_eq!(
|
|
29
|
+
snapshot["nextAction"]["reasonCodes"],
|
|
30
|
+
json!(["changed_docs", "missing-proof"])
|
|
31
|
+
);
|
|
32
|
+
assert_eq!(
|
|
33
|
+
snapshot["nextAction"]["commands"][0]["command"],
|
|
34
|
+
"git diff --check"
|
|
35
|
+
);
|
|
24
36
|
assert_eq!(snapshot["checks"]["safeToRun"][0]["checkId"], "diff-check");
|
|
25
37
|
}
|
|
26
38
|
|
|
@@ -85,6 +97,12 @@ fn preflight_reports_path_policy_and_check_plan() {
|
|
|
85
97
|
);
|
|
86
98
|
assert_eq!(allowed["paths"][0]["editable"], true);
|
|
87
99
|
assert_eq!(allowed["paths"][0]["risk"], "medium");
|
|
100
|
+
assert_eq!(allowed["nextAction"]["actionId"], "preflight.edit_allowed");
|
|
101
|
+
assert_eq!(allowed["nextAction"]["paths"], json!(["scripts/check.js"]));
|
|
102
|
+
assert_eq!(
|
|
103
|
+
allowed["nextAction"]["reasonCodes"],
|
|
104
|
+
json!(["preflight-clean"])
|
|
105
|
+
);
|
|
88
106
|
|
|
89
107
|
let ignored = run_json(
|
|
90
108
|
&root,
|
|
@@ -92,6 +110,12 @@ fn preflight_reports_path_policy_and_check_plan() {
|
|
|
92
110
|
);
|
|
93
111
|
assert_eq!(ignored["paths"][0]["editable"], false);
|
|
94
112
|
assert_eq!(ignored["findings"][0]["id"], "task.preflight.ignored_path");
|
|
113
|
+
assert_eq!(ignored["nextAction"]["actionId"], "preflight.blocked");
|
|
114
|
+
assert_eq!(ignored["nextAction"]["paths"], json!(["dist/bundle.js"]));
|
|
115
|
+
assert_eq!(
|
|
116
|
+
ignored["nextAction"]["reasonCodes"],
|
|
117
|
+
json!(["preflight-blocked"])
|
|
118
|
+
);
|
|
95
119
|
|
|
96
120
|
let traversal = run_json(
|
|
97
121
|
&root,
|
|
@@ -121,6 +145,7 @@ fn preflight_blocks_when_no_target_paths_are_selected() {
|
|
|
121
145
|
"task.preflight.missing_target_paths"
|
|
122
146
|
);
|
|
123
147
|
assert_eq!(preflight["nextAction"]["type"], "blocked");
|
|
148
|
+
assert_eq!(preflight["nextAction"]["actionId"], "preflight.blocked");
|
|
124
149
|
assert_eq!(preflight["nextAction"]["safeToExecute"], false);
|
|
125
150
|
}
|
|
126
151
|
|
|
@@ -153,7 +178,11 @@ fn commit_preflight_blocks_missing_proof() {
|
|
|
153
178
|
preflight["blockingFindings"][0]["id"],
|
|
154
179
|
"task.proof.missing_check"
|
|
155
180
|
);
|
|
156
|
-
assert_eq!(preflight["nextAction"]["type"], "
|
|
181
|
+
assert_eq!(preflight["nextAction"]["type"], "run_checks");
|
|
182
|
+
assert_eq!(
|
|
183
|
+
preflight["nextAction"]["actionId"],
|
|
184
|
+
"proof.run_required_checks"
|
|
185
|
+
);
|
|
157
186
|
assert_eq!(preflight["nextAction"]["checkIds"], json!(["diff-check"]));
|
|
158
187
|
}
|
|
159
188
|
|
|
@@ -180,9 +209,120 @@ fn agent_snapshot_prefers_commit_ready_over_more_editing() {
|
|
|
180
209
|
|
|
181
210
|
assert_eq!(snapshot["commit"]["canCommit"], true);
|
|
182
211
|
assert_eq!(snapshot["nextAction"]["type"], "commit_ready");
|
|
212
|
+
assert_eq!(snapshot["nextAction"]["actionId"], "commit.ready");
|
|
213
|
+
assert_eq!(
|
|
214
|
+
snapshot["nextAction"]["reasonCodes"],
|
|
215
|
+
json!(["commit-ready"])
|
|
216
|
+
);
|
|
183
217
|
assert_eq!(snapshot["nextAction"]["safeToExecute"], false);
|
|
184
218
|
}
|
|
185
219
|
|
|
220
|
+
#[test]
|
|
221
|
+
fn agent_snapshot_reports_stale_proof_as_concrete_check_action() {
|
|
222
|
+
let root = fixture_root(task_state_with_active_task(active_task(json!({
|
|
223
|
+
"allowedPaths": ["README.md", "docs/**"],
|
|
224
|
+
"requiredCheckIds": ["diff-check"],
|
|
225
|
+
"proofPathSets": {
|
|
226
|
+
"old": ["README.md"]
|
|
227
|
+
},
|
|
228
|
+
"proofBatches": [{
|
|
229
|
+
"id": "old-proof",
|
|
230
|
+
"checkedAt": "2026-05-04T12:00:00.000Z",
|
|
231
|
+
"evidencePathSet": "old",
|
|
232
|
+
"proofs": [{ "checkId": "diff-check", "exitCode": 0 }]
|
|
233
|
+
}]
|
|
234
|
+
}))));
|
|
235
|
+
init_git(&root);
|
|
236
|
+
write_fixture_file(&root, "README.md", "covered\n");
|
|
237
|
+
write_fixture_file(&root, "docs/new.md", "not-covered\n");
|
|
238
|
+
|
|
239
|
+
let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
|
|
240
|
+
|
|
241
|
+
assert_eq!(snapshot["nextAction"]["type"], "run_checks");
|
|
242
|
+
assert_eq!(
|
|
243
|
+
snapshot["nextAction"]["actionId"],
|
|
244
|
+
"proof.run_required_checks"
|
|
245
|
+
);
|
|
246
|
+
assert_eq!(
|
|
247
|
+
snapshot["nextAction"]["reasonCodes"],
|
|
248
|
+
json!(["changed_docs", "stale-proof"])
|
|
249
|
+
);
|
|
250
|
+
assert_eq!(snapshot["nextAction"]["checkIds"], json!(["diff-check"]));
|
|
251
|
+
assert_eq!(
|
|
252
|
+
snapshot["nextAction"]["paths"],
|
|
253
|
+
json!(["README.md", "docs/new.md"])
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn agent_snapshot_blocks_scope_drift_before_edit_or_commit_paths() {
|
|
259
|
+
let root = fixture_root(task_state());
|
|
260
|
+
init_git(&root);
|
|
261
|
+
write_fixture_file(&root, "README.md", "changed\n");
|
|
262
|
+
write_fixture_file(&root, "src/outside.rs", "pub fn outside() {}\n");
|
|
263
|
+
|
|
264
|
+
let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
|
|
265
|
+
|
|
266
|
+
assert_eq!(snapshot["nextAction"]["type"], "repair_scope");
|
|
267
|
+
assert_eq!(snapshot["nextAction"]["actionId"], "scope.repair_drift");
|
|
268
|
+
assert_eq!(
|
|
269
|
+
snapshot["nextAction"]["reasonCodes"],
|
|
270
|
+
json!(["scope-drift"])
|
|
271
|
+
);
|
|
272
|
+
assert_eq!(snapshot["nextAction"]["paths"], json!(["src/outside.rs"]));
|
|
273
|
+
assert_eq!(snapshot["nextAction"]["safeToExecute"], false);
|
|
274
|
+
assert_eq!(snapshot["nextAction"]["requiresUserApproval"], true);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#[test]
|
|
278
|
+
fn task_loop_execute_safe_records_proof_compacts_and_returns_commit_ready() {
|
|
279
|
+
let root = fixture_root(task_state());
|
|
280
|
+
let mut state: Value =
|
|
281
|
+
serde_json::from_str(&fs::read_to_string(root.join(".naome/task-state.json")).unwrap())
|
|
282
|
+
.unwrap();
|
|
283
|
+
state["activeTask"]["proofResults"] = json!([{
|
|
284
|
+
"checkId": "old-check",
|
|
285
|
+
"command": "old",
|
|
286
|
+
"cwd": ".",
|
|
287
|
+
"exitCode": 0,
|
|
288
|
+
"checkedAt": "2026-05-14T00:00:00.000Z",
|
|
289
|
+
"evidence": ["README.md"],
|
|
290
|
+
"stdoutSummary": "verbose",
|
|
291
|
+
"stderrSummary": "verbose",
|
|
292
|
+
"durationMs": 99
|
|
293
|
+
}]);
|
|
294
|
+
write_json(&root, ".naome/task-state.json", &state);
|
|
295
|
+
init_git(&root);
|
|
296
|
+
write_fixture_file(&root, "README.md", "changed\n");
|
|
297
|
+
|
|
298
|
+
let looped = run_json(&root, ["task", "loop", "--execute-safe", "--json"]);
|
|
299
|
+
|
|
300
|
+
assert_eq!(looped["schema"], "naome.task.loop.v1");
|
|
301
|
+
assert_eq!(looped["nextAction"]["type"], "commit_ready");
|
|
302
|
+
assert_eq!(looped["nextAction"]["actionId"], "commit.ready");
|
|
303
|
+
assert_eq!(looped["nextAction"]["reasonCodes"], json!(["commit-ready"]));
|
|
304
|
+
assert!(looped["executedSteps"]
|
|
305
|
+
.as_array()
|
|
306
|
+
.unwrap()
|
|
307
|
+
.iter()
|
|
308
|
+
.any(|step| {
|
|
309
|
+
step["schema"] == "naome.task.run-check.v1"
|
|
310
|
+
&& step["checkId"] == "diff-check"
|
|
311
|
+
&& step["recordedProof"] == true
|
|
312
|
+
}));
|
|
313
|
+
assert!(looped["executedSteps"]
|
|
314
|
+
.as_array()
|
|
315
|
+
.unwrap()
|
|
316
|
+
.iter()
|
|
317
|
+
.any(|step| {
|
|
318
|
+
step["schema"] == "naome.task.compact-proof.v1" && step["compacted"] == true
|
|
319
|
+
}));
|
|
320
|
+
|
|
321
|
+
let changed = fs::read_to_string(root.join(".naome/task-state.json")).unwrap();
|
|
322
|
+
assert!(!changed.contains("stdoutSummary"));
|
|
323
|
+
assert!(changed.contains("evidenceFingerprint"));
|
|
324
|
+
}
|
|
325
|
+
|
|
186
326
|
#[test]
|
|
187
327
|
fn commit_preflight_surfaces_transition_blocker_action_and_exit_code() {
|
|
188
328
|
let root = fixture_root(task_state_with_active_task(active_task(json!({
|
|
@@ -204,7 +344,12 @@ fn commit_preflight_surfaces_transition_blocker_action_and_exit_code() {
|
|
|
204
344
|
preflight["blockingFindings"][0]["id"],
|
|
205
345
|
"task.transition.human_review_required"
|
|
206
346
|
);
|
|
207
|
-
assert_eq!(preflight["nextAction"]["type"], "
|
|
347
|
+
assert_eq!(preflight["nextAction"]["type"], "human_review");
|
|
348
|
+
assert_eq!(preflight["nextAction"]["actionId"], "human_review.required");
|
|
349
|
+
assert_eq!(
|
|
350
|
+
preflight["nextAction"]["reasonCodes"],
|
|
351
|
+
json!(["human-review-required"])
|
|
352
|
+
);
|
|
208
353
|
assert_eq!(preflight["nextAction"]["safeToExecute"], false);
|
|
209
354
|
|
|
210
355
|
let output = Command::new(env!("CARGO_BIN_EXE_naome"))
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
use serde_json::json;
|
|
2
|
+
|
|
3
|
+
mod task_cli_support;
|
|
4
|
+
|
|
5
|
+
use task_cli_support::{
|
|
6
|
+
active_task, fixture_root, init_git, run_json, task_state, task_state_with_active_task,
|
|
7
|
+
write_fixture_file, write_verification_checks,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
#[test]
|
|
11
|
+
fn agent_snapshot_records_existing_receipts_before_rerunning_checks() {
|
|
12
|
+
let root = fixture_root(task_state());
|
|
13
|
+
init_git(&root);
|
|
14
|
+
write_fixture_file(&root, "README.md", "changed\n");
|
|
15
|
+
|
|
16
|
+
let checked = run_json(
|
|
17
|
+
&root,
|
|
18
|
+
["task", "run-check", "--check", "diff-check", "--json"],
|
|
19
|
+
);
|
|
20
|
+
assert_eq!(checked["exitCode"], 0);
|
|
21
|
+
assert_eq!(checked["recordedProof"], false);
|
|
22
|
+
|
|
23
|
+
let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
|
|
24
|
+
|
|
25
|
+
assert_eq!(snapshot["nextAction"]["type"], "record_proof");
|
|
26
|
+
assert_eq!(
|
|
27
|
+
snapshot["nextAction"]["actionId"],
|
|
28
|
+
"proof.record_from_receipts"
|
|
29
|
+
);
|
|
30
|
+
assert_eq!(snapshot["nextAction"]["checkIds"], json!(["diff-check"]));
|
|
31
|
+
assert_eq!(snapshot["nextAction"]["safeToExecute"], true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[test]
|
|
35
|
+
fn commit_preflight_surfaces_generic_transition_blocker_action() {
|
|
36
|
+
let mut state = task_state_with_active_task(active_task(json!({
|
|
37
|
+
"requiredCheckIds": [],
|
|
38
|
+
"proofResults": []
|
|
39
|
+
})));
|
|
40
|
+
state["status"] = json!("blocked");
|
|
41
|
+
let root = fixture_root(state);
|
|
42
|
+
init_git(&root);
|
|
43
|
+
|
|
44
|
+
let preflight = run_json(&root, ["task", "commit-preflight", "--json"]);
|
|
45
|
+
|
|
46
|
+
assert_eq!(preflight["wouldPass"], false);
|
|
47
|
+
assert_eq!(
|
|
48
|
+
preflight["blockingFindings"][0]["id"],
|
|
49
|
+
"task.transition.blocked_state"
|
|
50
|
+
);
|
|
51
|
+
assert_eq!(preflight["nextAction"]["type"], "blocked");
|
|
52
|
+
assert_eq!(preflight["nextAction"]["actionId"], "transition.blocked");
|
|
53
|
+
assert_eq!(
|
|
54
|
+
preflight["nextAction"]["reasonCodes"],
|
|
55
|
+
json!(["transition-blocked"])
|
|
56
|
+
);
|
|
57
|
+
assert_eq!(
|
|
58
|
+
preflight["nextAction"]["primaryFindingId"],
|
|
59
|
+
"task.transition.blocked_state"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn task_loop_execute_safe_runs_merged_safe_impact_plan() {
|
|
65
|
+
let root = fixture_root(task_state_with_active_task(active_task(json!({
|
|
66
|
+
"allowedPaths": ["src/lib.rs"],
|
|
67
|
+
"requiredCheckIds": ["diff-check"],
|
|
68
|
+
"proofResults": []
|
|
69
|
+
}))));
|
|
70
|
+
write_verification_checks(
|
|
71
|
+
&root,
|
|
72
|
+
json!([
|
|
73
|
+
{
|
|
74
|
+
"id": "diff-check",
|
|
75
|
+
"command": "git diff --check",
|
|
76
|
+
"cwd": ".",
|
|
77
|
+
"purpose": "Detect whitespace and patch formatting issues.",
|
|
78
|
+
"cost": "fast",
|
|
79
|
+
"source": "git",
|
|
80
|
+
"evidence": ["src/lib.rs"],
|
|
81
|
+
"lastVerified": null
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "repository-quality-check",
|
|
85
|
+
"command": "git diff --check",
|
|
86
|
+
"cwd": ".",
|
|
87
|
+
"purpose": "Exercise an additional safe impact-planned check.",
|
|
88
|
+
"cost": "fast",
|
|
89
|
+
"source": "git",
|
|
90
|
+
"evidence": ["src/lib.rs"],
|
|
91
|
+
"lastVerified": null
|
|
92
|
+
}
|
|
93
|
+
]),
|
|
94
|
+
);
|
|
95
|
+
init_git(&root);
|
|
96
|
+
write_fixture_file(&root, "src/lib.rs", "pub fn changed() {}\n");
|
|
97
|
+
|
|
98
|
+
let looped = run_json(&root, ["task", "loop", "--execute-safe", "--json"]);
|
|
99
|
+
let executed = looped["executedSteps"].as_array().unwrap();
|
|
100
|
+
|
|
101
|
+
assert!(executed
|
|
102
|
+
.iter()
|
|
103
|
+
.any(|step| step["checkId"] == "diff-check" && step["executed"] == true));
|
|
104
|
+
assert!(executed
|
|
105
|
+
.iter()
|
|
106
|
+
.any(|step| { step["checkId"] == "repository-quality-check" && step["executed"] == true }));
|
|
107
|
+
}
|
|
Binary file
|
package/native/linux-x64/naome
CHANGED
|
Binary file
|