@lamentis/naome 1.4.1 → 1.4.3

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.
Files changed (52) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +17 -122
  3. package/crates/naome-cli/Cargo.toml +1 -1
  4. package/crates/naome-cli/src/main.rs +14 -5
  5. package/crates/naome-cli/src/task_commands/agent_snapshot.rs +173 -0
  6. package/crates/naome-cli/src/task_commands/can_edit.rs +64 -0
  7. package/crates/naome-cli/src/task_commands/check_run/output.rs +34 -0
  8. package/crates/naome-cli/src/task_commands/check_run/receipts.rs +163 -0
  9. package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
  10. package/crates/naome-cli/src/task_commands/check_run.rs +196 -0
  11. package/crates/naome-cli/src/task_commands/commit_preflight.rs +89 -0
  12. package/crates/naome-cli/src/task_commands/common.rs +39 -1
  13. package/crates/naome-cli/src/task_commands/compact_proof.rs +69 -0
  14. package/crates/naome-cli/src/task_commands/complete.rs +43 -0
  15. package/crates/naome-cli/src/task_commands/loop_control.rs +73 -0
  16. package/crates/naome-cli/src/task_commands/path_policy.rs +57 -0
  17. package/crates/naome-cli/src/task_commands/planner/checks.rs +166 -0
  18. package/crates/naome-cli/src/task_commands/planner/impact.rs +35 -0
  19. package/crates/naome-cli/src/task_commands/planner/mod.rs +24 -0
  20. package/crates/naome-cli/src/task_commands/preflight.rs +208 -0
  21. package/crates/naome-cli/src/task_commands/readiness.rs +14 -10
  22. package/crates/naome-cli/src/task_commands/record.rs +176 -37
  23. package/crates/naome-cli/src/task_commands/repair.rs +58 -11
  24. package/crates/naome-cli/src/task_commands/scope_suggestions.rs +109 -0
  25. package/crates/naome-cli/src/task_commands.rs +26 -3
  26. package/crates/naome-cli/tests/task_cli_agent_controls.rs +9 -16
  27. package/crates/naome-cli/tests/task_cli_fast_flow.rs +290 -0
  28. package/crates/naome-cli/tests/task_cli_loop.rs +383 -0
  29. package/crates/naome-cli/tests/task_cli_loop_edit.rs +144 -0
  30. package/crates/naome-cli/tests/task_cli_support/mod.rs +28 -0
  31. package/crates/naome-core/Cargo.toml +1 -1
  32. package/crates/naome-core/src/lib.rs +7 -7
  33. package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
  34. package/crates/naome-core/src/task_state/mod.rs +2 -0
  35. package/crates/naome-core/src/task_state/status/control/repair.rs +2 -2
  36. package/crates/naome-core/src/task_state/status/model.rs +2 -0
  37. package/crates/naome-core/src/task_state/status/proof.rs +59 -9
  38. package/crates/naome-core/src/task_state/status/proof_read.rs +14 -0
  39. package/crates/naome-core/src/task_state/status/report_context.rs +23 -1
  40. package/crates/naome-core/src/task_state/status/transition.rs +29 -1
  41. package/crates/naome-core/tests/task_status.rs +122 -0
  42. package/installer/context.js +1 -1
  43. package/installer/harness-verification.js +2 -6
  44. package/installer/manifest-state.js +2 -2
  45. package/installer/native.js +3 -31
  46. package/native/darwin-arm64/naome +0 -0
  47. package/native/linux-x64/naome +0 -0
  48. package/package.json +1 -1
  49. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  50. package/templates/naome-root/.naome/bin/check-task-state.js +4 -39
  51. package/templates/naome-root/.naome/bin/naome.js +2 -30
  52. package/templates/naome-root/.naome/manifest.json +2 -2
@@ -0,0 +1,165 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+ use std::process::Command;
4
+
5
+ use serde_json::Value;
6
+
7
+ #[derive(Debug, Clone)]
8
+ pub(in crate::task_commands) struct VerificationCheck {
9
+ pub(in crate::task_commands) id: String,
10
+ pub(in crate::task_commands) command: String,
11
+ pub(in crate::task_commands) cwd: String,
12
+ }
13
+
14
+ #[derive(Debug, Clone)]
15
+ pub(super) struct SafeCommand {
16
+ pub(super) program: String,
17
+ pub(super) args: Vec<String>,
18
+ }
19
+
20
+ pub(in crate::task_commands) fn read_verification_check(
21
+ root: &Path,
22
+ check_id: &str,
23
+ ) -> Result<Option<VerificationCheck>, Box<dyn std::error::Error>> {
24
+ let content = fs::read_to_string(root.join(".naome/verification.json"))?;
25
+ let verification: Value = serde_json::from_str(&content)?;
26
+ Ok(verification
27
+ .get("checks")
28
+ .and_then(Value::as_array)
29
+ .into_iter()
30
+ .flatten()
31
+ .find(|check| check.get("id").and_then(Value::as_str) == Some(check_id))
32
+ .and_then(|check| {
33
+ Some(VerificationCheck {
34
+ id: check.get("id")?.as_str()?.to_string(),
35
+ command: check.get("command")?.as_str()?.to_string(),
36
+ cwd: check.get("cwd")?.as_str()?.to_string(),
37
+ })
38
+ }))
39
+ }
40
+
41
+ pub(super) fn safe_command(
42
+ root: &Path,
43
+ check: &VerificationCheck,
44
+ ) -> Result<Option<SafeCommand>, Box<dyn std::error::Error>> {
45
+ if check.cwd != "." {
46
+ return Ok(None);
47
+ }
48
+ let changed_paths = changed_paths(root)?;
49
+ match check.command.as_str() {
50
+ "git diff --check" => Ok(Some(command("git", &["diff", "--check"]))),
51
+ "node .naome/bin/check-harness-health.js" => Ok(Some(naome_command(
52
+ root,
53
+ &["check-harness-health", "--root"],
54
+ )?)),
55
+ "node .naome/bin/check-task-state.js" => {
56
+ Ok(Some(naome_command(root, &["check-task-state", "--root"])?))
57
+ }
58
+ "node .naome/bin/naome.js quality check --changed" => Ok(Some(naome_command(
59
+ root,
60
+ &["quality", "check", "--changed"],
61
+ )?)),
62
+ "node .naome/bin/naome.js semantic check --changed" => Ok(Some(naome_command(
63
+ root,
64
+ &["semantic", "check", "--changed"],
65
+ )?)),
66
+ "node .naome/bin/naome.js arch validate --changed-only" => Ok(Some(naome_command(
67
+ root,
68
+ &["arch", "validate", "--changed-only"],
69
+ )?)),
70
+ "npm run check:task-state" => trusted_npm_script(
71
+ root,
72
+ &changed_paths,
73
+ "check:task-state",
74
+ "node scripts/check-task-state.js",
75
+ ),
76
+ "npm run test:task-state" => trusted_npm_script(
77
+ root,
78
+ &changed_paths,
79
+ "test:task-state",
80
+ "node --test scripts/check-task-state.test.js",
81
+ ),
82
+ "npm run test:decision-engine" => trusted_npm_script(
83
+ root,
84
+ &changed_paths,
85
+ "test:decision-engine",
86
+ "cargo test --manifest-path packages/naome/Cargo.toml -p naome-core",
87
+ ),
88
+ _ => Ok(None),
89
+ }
90
+ }
91
+
92
+ fn command(program: &str, args: &[&str]) -> SafeCommand {
93
+ SafeCommand {
94
+ program: program.to_string(),
95
+ args: args.iter().map(|arg| (*arg).to_string()).collect(),
96
+ }
97
+ }
98
+
99
+ fn naome_command(root: &Path, args: &[&str]) -> Result<SafeCommand, Box<dyn std::error::Error>> {
100
+ let mut owned_args = args
101
+ .iter()
102
+ .map(|arg| (*arg).to_string())
103
+ .collect::<Vec<_>>();
104
+ if args.last() == Some(&"--root") {
105
+ owned_args.push(root.to_string_lossy().to_string());
106
+ }
107
+ Ok(SafeCommand {
108
+ program: std::env::current_exe()?.to_string_lossy().to_string(),
109
+ args: owned_args,
110
+ })
111
+ }
112
+
113
+ fn trusted_npm_script(
114
+ root: &Path,
115
+ changed_paths: &[String],
116
+ script: &str,
117
+ expected: &str,
118
+ ) -> Result<Option<SafeCommand>, Box<dyn std::error::Error>> {
119
+ if changed_paths
120
+ .iter()
121
+ .any(|path| path == "package.json" || path == "packages/naome/package.json")
122
+ {
123
+ return Ok(None);
124
+ }
125
+
126
+ let package_json = root.join("package.json");
127
+ let Ok(content) = fs::read_to_string(package_json) else {
128
+ return Ok(None);
129
+ };
130
+ let package: Value = serde_json::from_str(&content)?;
131
+ let actual = package
132
+ .get("scripts")
133
+ .and_then(Value::as_object)
134
+ .and_then(|scripts| scripts.get(script))
135
+ .and_then(Value::as_str);
136
+ if actual != Some(expected) {
137
+ return Ok(None);
138
+ }
139
+
140
+ Ok(Some(command("npm", &["run", script])))
141
+ }
142
+
143
+ fn changed_paths(root: &Path) -> Result<Vec<String>, Box<dyn std::error::Error>> {
144
+ let output = Command::new("git")
145
+ .args(["status", "--porcelain=v1", "-z", "--untracked-files=all"])
146
+ .current_dir(root)
147
+ .output()?;
148
+ if !output.status.success() {
149
+ return Ok(Vec::new());
150
+ }
151
+
152
+ let mut paths = Vec::new();
153
+ for entry in output.stdout.split(|byte| *byte == 0) {
154
+ if entry.len() < 4 {
155
+ continue;
156
+ }
157
+ let path = String::from_utf8_lossy(&entry[3..]).replace('\\', "/");
158
+ if !path.is_empty() {
159
+ paths.push(path);
160
+ }
161
+ }
162
+ paths.sort();
163
+ paths.dedup();
164
+ Ok(paths)
165
+ }
@@ -0,0 +1,196 @@
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::{
13
+ changed_paths, evidence_fingerprint, successful_receipts, CheckRunReceipt,
14
+ };
15
+ pub(super) use verification::read_verification_check;
16
+
17
+ use super::common::{agent_session, print_json_with_session, value_after};
18
+ use output::{finding, run_check_response};
19
+ use receipts::append_receipt;
20
+ use verification::safe_command;
21
+
22
+ pub(super) fn run_check_command(
23
+ root: &Path,
24
+ args: &[String],
25
+ ) -> Result<(), Box<dyn std::error::Error>> {
26
+ let session = agent_session(args)?;
27
+ let Some(check_id) = value_after(args, "--check") else {
28
+ return Err("naome task run-check requires --check <check-id>".into());
29
+ };
30
+ let record = args.iter().any(|arg| arg == "--record-proof");
31
+ let result = run_check_by_id(root, check_id, record, session.as_deref())?;
32
+ print_json_with_session(result, session.as_deref())
33
+ }
34
+
35
+ pub(super) fn run_check_by_id(
36
+ root: &Path,
37
+ check_id: &str,
38
+ record: bool,
39
+ session: Option<&str>,
40
+ ) -> Result<Value, Box<dyn std::error::Error>> {
41
+ let Some(check) = read_verification_check(root, check_id)? else {
42
+ return Ok(rejected_check(
43
+ check_id,
44
+ "task.check.unknown",
45
+ format!("Unknown verification check id: {check_id}."),
46
+ "Check id is not declared in .naome/verification.json.",
47
+ session,
48
+ ));
49
+ };
50
+ let Some(command) = safe_command(root, &check)? else {
51
+ return Ok(rejected_check(
52
+ check_id,
53
+ "task.check.unsafe_command",
54
+ format!("Check {check_id} is not in the autonomous safe command allowlist."),
55
+ "NAOME refused to execute this check automatically.",
56
+ session,
57
+ ));
58
+ };
59
+
60
+ let before = changed_paths(root)?;
61
+ let start = Instant::now();
62
+ let output = Command::new(&command.program)
63
+ .args(&command.args)
64
+ .current_dir(root.join(&check.cwd))
65
+ .output()?;
66
+ let mut stdout = output.stdout;
67
+ let mut stderr = output.stderr;
68
+ let mut exit_code = output.status.code().unwrap_or(1);
69
+ if check.command == "git diff --check" {
70
+ let cached = Command::new("git")
71
+ .args(["diff", "--cached", "--check"])
72
+ .current_dir(root.join(&check.cwd))
73
+ .output()?;
74
+ if !stdout.is_empty() && !cached.stdout.is_empty() {
75
+ stdout.push(b'\n');
76
+ }
77
+ stdout.extend(cached.stdout);
78
+ if !stderr.is_empty() && !cached.stderr.is_empty() {
79
+ stderr.push(b'\n');
80
+ }
81
+ stderr.extend(cached.stderr);
82
+ if !cached.status.success() {
83
+ exit_code = cached.status.code().unwrap_or(1);
84
+ }
85
+ }
86
+ let duration_ms = start.elapsed().as_millis();
87
+ let after = changed_paths(root)?;
88
+ let status = task_status_report(root)?;
89
+ let evidence_paths = status.scope.in_scope_changed_paths;
90
+ let receipt = CheckRunReceipt {
91
+ task_id: status.task_id,
92
+ check_id: check.id.clone(),
93
+ command: check.command.clone(),
94
+ cwd: check.cwd.clone(),
95
+ exit_code,
96
+ checked_at: checked_at(),
97
+ evidence_fingerprint: evidence_fingerprint(root, &evidence_paths)?,
98
+ evidence_paths,
99
+ stdout_summary: bounded_summary(&stdout),
100
+ stderr_summary: bounded_summary(&stderr),
101
+ duration_ms,
102
+ agent_session: session.map(ToString::to_string),
103
+ };
104
+ append_receipt(root, &receipt)?;
105
+
106
+ let (recorded_proof, findings) =
107
+ maybe_record_proof(root, record, exit_code, &check.id, session)?;
108
+ let mut response = run_check_response(
109
+ check_id,
110
+ true,
111
+ Some(exit_code),
112
+ recorded_proof,
113
+ findings,
114
+ if exit_code == 0 {
115
+ "Check executed successfully."
116
+ } else {
117
+ "Check executed and failed; inspect summaries before continuing."
118
+ },
119
+ session,
120
+ );
121
+ response["command"] = json!(check.command);
122
+ response["cwd"] = json!(check.cwd);
123
+ response["durationMs"] = json!(duration_ms);
124
+ response["changedPathsBefore"] = json!(before);
125
+ response["changedPathsAfter"] = json!(after);
126
+ response["stdoutSummary"] = json!(bounded_summary(&stdout));
127
+ response["stderrSummary"] = json!(bounded_summary(&stderr));
128
+ Ok(response)
129
+ }
130
+
131
+ fn maybe_record_proof(
132
+ root: &Path,
133
+ record: bool,
134
+ exit_code: i32,
135
+ check_id: &str,
136
+ session: Option<&str>,
137
+ ) -> Result<(bool, Vec<Value>), Box<dyn std::error::Error>> {
138
+ let mut findings = Vec::new();
139
+ if !record {
140
+ return Ok((false, findings));
141
+ }
142
+ if exit_code != 0 {
143
+ findings.push(finding(
144
+ "task.check.failed",
145
+ "Check did not exit 0; proof was not recorded.",
146
+ ));
147
+ return Ok((false, findings));
148
+ }
149
+
150
+ let status = task_status_report(root)?;
151
+ if !status.scope.out_of_scope_changed_paths.is_empty() {
152
+ findings.push(finding(
153
+ "task.scope.out_of_scope_change",
154
+ "Changed paths are outside task scope; proof was not recorded.",
155
+ ));
156
+ return Ok((false, findings));
157
+ }
158
+
159
+ let recorded = super::record::record_receipts_for_checks(
160
+ root,
161
+ &[check_id.to_string()],
162
+ &status.scope.in_scope_changed_paths,
163
+ session,
164
+ )?;
165
+ Ok((recorded, findings))
166
+ }
167
+
168
+ fn rejected_check(
169
+ check_id: &str,
170
+ finding_id: &str,
171
+ message: String,
172
+ instruction: &str,
173
+ session: Option<&str>,
174
+ ) -> Value {
175
+ run_check_response(
176
+ check_id,
177
+ false,
178
+ None,
179
+ false,
180
+ vec![finding(finding_id, message)],
181
+ instruction,
182
+ session,
183
+ )
184
+ }
185
+
186
+ fn bounded_summary(bytes: &[u8]) -> String {
187
+ String::from_utf8_lossy(bytes)
188
+ .lines()
189
+ .take(8)
190
+ .collect::<Vec<_>>()
191
+ .join("\n")
192
+ }
193
+
194
+ fn checked_at() -> String {
195
+ "1970-01-01T00:00:00.000Z".to_string()
196
+ }
@@ -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
+ }
@@ -1,7 +1,7 @@
1
1
  use std::fs;
2
2
  use std::path::Path;
3
3
 
4
- use serde_json::Value;
4
+ use serde_json::{json, Value};
5
5
 
6
6
  pub(super) fn read_task_state(root: &Path) -> Result<Value, Box<dyn std::error::Error>> {
7
7
  Ok(serde_json::from_str(&fs::read_to_string(
@@ -9,11 +9,30 @@ pub(super) fn read_task_state(root: &Path) -> Result<Value, Box<dyn std::error::
9
9
  )?)?)
10
10
  }
11
11
 
12
+ pub(super) fn write_task_state(
13
+ root: &Path,
14
+ state: &Value,
15
+ ) -> Result<(), Box<dyn std::error::Error>> {
16
+ fs::write(
17
+ root.join(".naome/task-state.json"),
18
+ format!("{}\n", serde_json::to_string_pretty(state)?),
19
+ )?;
20
+ Ok(())
21
+ }
22
+
12
23
  pub(super) fn print_json(value: Value) -> Result<(), Box<dyn std::error::Error>> {
13
24
  println!("{}", serde_json::to_string_pretty(&value)?);
14
25
  Ok(())
15
26
  }
16
27
 
28
+ pub(super) fn print_json_with_session(
29
+ mut value: Value,
30
+ session: Option<&str>,
31
+ ) -> Result<(), Box<dyn std::error::Error>> {
32
+ value["agentSession"] = session.map_or(Value::Null, |session| json!(session));
33
+ print_json(value)
34
+ }
35
+
17
36
  pub(super) fn value_after<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
18
37
  args.windows(2)
19
38
  .find(|window| window[0] == flag)
@@ -30,3 +49,22 @@ pub(super) fn repeated_values(args: &[String], flag: &str) -> Vec<String> {
30
49
  values.dedup();
31
50
  values
32
51
  }
52
+
53
+ pub(super) fn agent_session(args: &[String]) -> Result<Option<String>, Box<dyn std::error::Error>> {
54
+ let Some(value) = value_after(args, "--agent-session") else {
55
+ return Ok(None);
56
+ };
57
+ if value.is_empty()
58
+ || value.len() > 80
59
+ || value.contains('/')
60
+ || value.contains('\\')
61
+ || !value.is_ascii()
62
+ || value.chars().any(|character| character.is_control())
63
+ {
64
+ return Err(
65
+ "invalid --agent-session; expected ASCII id up to 80 chars without path separators"
66
+ .into(),
67
+ );
68
+ }
69
+ Ok(Some(value.to_string()))
70
+ }
@@ -0,0 +1,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
+ }
@@ -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,73 @@
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, read_task_state, write_task_state};
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
+ 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
+ }));
45
+ }
46
+ }
47
+ }
48
+
49
+ let status = task_status_report(root)?;
50
+ let proof_plan = task_proof_plan(root)?;
51
+ let transition = task_transition_readiness(root, "complete")?;
52
+ let can_commit = json!({
53
+ "schema": "naome.task.commit-readiness.v1",
54
+ "allowed": transition.allowed && status.agent_loop.can_commit,
55
+ "commitPaths": status.scope.in_scope_changed_paths,
56
+ "blockingFindings": transition.blocking_findings,
57
+ "agentLoop": status.agent_loop
58
+ });
59
+ print_json_with_session(
60
+ json!({
61
+ "schema": "naome.task.loop.v1",
62
+ "mode": if execute_safe { "execute_safe" } else { "read_only" },
63
+ "status": status,
64
+ "proofPlan": proof_plan,
65
+ "canTransition": transition,
66
+ "canCommit": can_commit,
67
+ "executedSteps": executed_steps,
68
+ "nextAction": status.next_action_v2,
69
+ "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
+ }),
71
+ session.as_deref(),
72
+ )
73
+ }