@lamentis/naome 1.4.2 → 1.4.4

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