@lamentis/naome 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) 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 +9 -5
  5. package/crates/naome-cli/src/task_commands/can_edit.rs +116 -0
  6. package/crates/naome-cli/src/task_commands/check_run/output.rs +34 -0
  7. package/crates/naome-cli/src/task_commands/check_run/receipts.rs +155 -0
  8. package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
  9. package/crates/naome-cli/src/task_commands/check_run.rs +192 -0
  10. package/crates/naome-cli/src/task_commands/common.rs +39 -1
  11. package/crates/naome-cli/src/task_commands/complete.rs +43 -0
  12. package/crates/naome-cli/src/task_commands/loop_control.rs +55 -0
  13. package/crates/naome-cli/src/task_commands/readiness.rs +14 -10
  14. package/crates/naome-cli/src/task_commands/record.rs +139 -37
  15. package/crates/naome-cli/src/task_commands/repair.rs +58 -11
  16. package/crates/naome-cli/src/task_commands.rs +14 -3
  17. package/crates/naome-cli/tests/task_cli_agent_controls.rs +9 -16
  18. package/crates/naome-cli/tests/task_cli_loop.rs +383 -0
  19. package/crates/naome-cli/tests/task_cli_loop_edit.rs +144 -0
  20. package/crates/naome-cli/tests/task_cli_support/mod.rs +28 -0
  21. package/crates/naome-core/Cargo.toml +1 -1
  22. package/crates/naome-core/src/lib.rs +7 -7
  23. package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
  24. package/crates/naome-core/src/task_state/mod.rs +2 -0
  25. package/crates/naome-core/src/task_state/status/control/repair.rs +2 -2
  26. package/crates/naome-core/src/task_state/status/model.rs +2 -0
  27. package/crates/naome-core/src/task_state/status/proof.rs +59 -9
  28. package/crates/naome-core/src/task_state/status/proof_read.rs +14 -0
  29. package/crates/naome-core/src/task_state/status/report_context.rs +23 -1
  30. package/crates/naome-core/src/task_state/status/transition.rs +29 -1
  31. package/crates/naome-core/tests/task_status.rs +122 -0
  32. package/installer/context.js +1 -1
  33. package/installer/harness-verification.js +2 -6
  34. package/installer/manifest-state.js +2 -2
  35. package/installer/native.js +3 -31
  36. package/native/darwin-arm64/naome +0 -0
  37. package/native/linux-x64/naome +0 -0
  38. package/package.json +1 -1
  39. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  40. package/templates/naome-root/.naome/bin/check-task-state.js +4 -39
  41. package/templates/naome-root/.naome/bin/naome.js +2 -30
  42. package/templates/naome-root/.naome/manifest.json +2 -2
@@ -3,14 +3,20 @@ use std::path::Path;
3
3
  use naome_core::task_status_report;
4
4
  use serde_json::json;
5
5
 
6
- use super::common::{print_json, value_after};
6
+ use super::common::{agent_session, print_json_with_session, value_after};
7
7
 
8
8
  pub(super) fn repair_preview(
9
9
  root: &Path,
10
10
  args: &[String],
11
11
  ) -> Result<(), Box<dyn std::error::Error>> {
12
- if !args.iter().any(|arg| arg == "--dry-run") {
13
- return Err("naome task repair requires --dry-run in v1.4.1".into());
12
+ let session = agent_session(args)?;
13
+ let dry_run = args.iter().any(|arg| arg == "--dry-run");
14
+ let execute_safe = args.iter().any(|arg| arg == "--execute-safe");
15
+ if dry_run && execute_safe {
16
+ return Err("naome task repair accepts only one of --dry-run or --execute-safe".into());
17
+ }
18
+ if !dry_run && !execute_safe {
19
+ return Err("naome task repair requires --dry-run or --execute-safe".into());
14
20
  }
15
21
  let plan_id = value_after(args, "--plan").ok_or("naome task repair requires --plan <id>")?;
16
22
  let status = task_status_report(root)?;
@@ -19,12 +25,53 @@ pub(super) fn repair_preview(
19
25
  .iter()
20
26
  .find(|item| item.id == plan_id)
21
27
  .cloned();
22
- print_json(json!({
23
- "schema": "naome.task.repair-preview.v1",
24
- "planId": plan_id,
25
- "found": plan.is_some(),
26
- "wouldExecute": false,
27
- "plan": plan,
28
- "agentInstruction": "Review this dry-run output and execute only safe commands explicitly allowed by NAOME."
29
- }))
28
+ let mut steps = Vec::new();
29
+ let mut executed = false;
30
+ let mut requires_user_approval = false;
31
+ if execute_safe {
32
+ if let Some(plan) = &plan {
33
+ if !can_execute_safe(plan) {
34
+ requires_user_approval = true;
35
+ } else {
36
+ match plan.kind.as_str() {
37
+ "rerun_check" if plan.check_ids.len() == 1 => {
38
+ steps.push(super::check_run::run_check_by_id(
39
+ root,
40
+ &plan.check_ids[0],
41
+ false,
42
+ session.as_deref(),
43
+ )?);
44
+ executed = true;
45
+ }
46
+ "record_proof" => {
47
+ steps.push(super::record::record_proof_value(root, session.as_deref())?);
48
+ executed = true;
49
+ }
50
+ _ => {
51
+ requires_user_approval = true;
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ print_json_with_session(
58
+ json!({
59
+ "schema": if dry_run { "naome.task.repair-preview.v1" } else { "naome.task.repair-execute.v1" },
60
+ "planId": plan_id,
61
+ "found": plan.is_some(),
62
+ "wouldExecute": dry_run && plan.as_ref().is_some_and(can_execute_safe),
63
+ "executed": executed,
64
+ "requiresUserApproval": requires_user_approval || plan.as_ref().is_some_and(|item| item.requires_user_approval),
65
+ "plan": plan,
66
+ "steps": steps,
67
+ "agentInstruction": if executed { "Executed only NAOME safe check/proof repair steps." } else { "Review this output; unsafe repair plans require human approval." }
68
+ }),
69
+ session.as_deref(),
70
+ )
71
+ }
72
+
73
+ fn can_execute_safe(item: &naome_core::RepairPlanItem) -> bool {
74
+ item.safe_to_execute
75
+ && !item.requires_user_approval
76
+ && matches!(item.kind.as_str(), "rerun_check" | "record_proof")
30
77
  }
@@ -1,6 +1,10 @@
1
1
  use std::path::Path;
2
2
 
3
+ mod can_edit;
4
+ mod check_run;
3
5
  mod common;
6
+ mod complete;
7
+ mod loop_control;
4
8
  mod readiness;
5
9
  mod record;
6
10
  mod repair;
@@ -19,9 +23,13 @@ pub fn run_task_command(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
19
23
  Some("migrate-ledger") => migrate_ledger(root, args),
20
24
  Some("status") => task_status(root, args),
21
25
  Some("proof-plan") => proof_plan(root, args),
26
+ Some("can-edit") => can_edit::can_edit(root, args),
27
+ Some("run-check") => check_run::run_check_command(root, args),
22
28
  Some("can-transition") => can_transition(root, args),
23
29
  Some("repair") => repair::repair_preview(root, args),
24
30
  Some("record-proof") => record::record_proof(root, args),
31
+ Some("complete") => complete::complete_task(root, args),
32
+ Some("loop") => loop_control::task_loop(root, args),
25
33
  Some("request-scope") => scope_request::request_scope(root, args),
26
34
  Some("can-commit") => readiness::can_commit(root, args),
27
35
  Some("timeline") => timeline::timeline(root, args),
@@ -31,9 +39,10 @@ pub fn run_task_command(root: &Path, args: &[String]) -> Result<(), Box<dyn std:
31
39
  }
32
40
 
33
41
  fn task_status(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
42
+ let session = common::agent_session(args)?;
34
43
  let report = task_status_report(root)?;
35
44
  if args.iter().any(|arg| arg == "--json") {
36
- println!("{}", serde_json::to_string_pretty(&report)?);
45
+ common::print_json_with_session(serde_json::to_value(&report)?, session.as_deref())?;
37
46
  } else {
38
47
  print!("{}", format_task_status(&report));
39
48
  }
@@ -42,9 +51,10 @@ fn task_status(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::E
42
51
  }
43
52
 
44
53
  fn proof_plan(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
54
+ let session = common::agent_session(args)?;
45
55
  let report = task_proof_plan(root)?;
46
56
  if args.iter().any(|arg| arg == "--json") {
47
- println!("{}", serde_json::to_string_pretty(&report)?);
57
+ common::print_json_with_session(serde_json::to_value(&report)?, session.as_deref())?;
48
58
  } else {
49
59
  print!("{}", format_task_proof_plan(&report));
50
60
  }
@@ -53,6 +63,7 @@ fn proof_plan(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Er
53
63
  }
54
64
 
55
65
  fn can_transition(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
66
+ let session = common::agent_session(args)?;
56
67
  let Some(target) = args
57
68
  .windows(2)
58
69
  .find(|window| window[0] == "--to")
@@ -62,7 +73,7 @@ fn can_transition(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error
62
73
  };
63
74
  let report = task_transition_readiness(root, target)?;
64
75
  if args.iter().any(|arg| arg == "--json") {
65
- println!("{}", serde_json::to_string_pretty(&report)?);
76
+ common::print_json_with_session(serde_json::to_value(&report)?, session.as_deref())?;
66
77
  } else {
67
78
  println!(
68
79
  "NAOME task transition {target}: {}",
@@ -1,11 +1,10 @@
1
1
  use std::fs;
2
- use std::process::Command;
3
2
 
4
3
  use serde_json::{json, Value};
5
4
 
6
5
  mod task_cli_support;
7
6
 
8
- use task_cli_support::{fixture_root, init_git, task_state, write_fixture_file};
7
+ use task_cli_support::{fixture_root, init_git, run_json, task_state, write_fixture_file};
9
8
 
10
9
  #[test]
11
10
  fn status_json_exposes_policy_hints_and_recovery_guidance() {
@@ -83,6 +82,14 @@ fn record_proof_from_plan_writes_compact_batch() {
83
82
  init_git(&root);
84
83
  write_fixture_file(&root, "README.md", "changed\n");
85
84
 
85
+ let check = run_json(
86
+ &root,
87
+ ["task", "run-check", "--check", "diff-check", "--json"],
88
+ );
89
+ assert_eq!(check["schema"], "naome.task.run-check.v1");
90
+ assert_eq!(check["executed"], true);
91
+ assert_eq!(check["recordedProof"], false);
92
+
86
93
  let recorded = run_json(
87
94
  &root,
88
95
  ["task", "record-proof", "--from-proof-plan", "--json"],
@@ -201,17 +208,3 @@ fn review_fix_mode_is_structured_not_inferred_from_prompt_text() {
201
208
  .unwrap()
202
209
  .contains("explicit allowedPaths"));
203
210
  }
204
-
205
- fn run_json<const N: usize>(root: &std::path::Path, args: [&str; N]) -> Value {
206
- let output = Command::new(env!("CARGO_BIN_EXE_naome"))
207
- .args(args)
208
- .current_dir(root)
209
- .output()
210
- .unwrap();
211
- assert!(
212
- output.status.success(),
213
- "{}",
214
- String::from_utf8_lossy(&output.stderr)
215
- );
216
- serde_json::from_slice(&output.stdout).unwrap()
217
- }
@@ -0,0 +1,383 @@
1
+ use std::fs;
2
+ use std::process::Command;
3
+
4
+ use serde_json::{json, Value};
5
+
6
+ mod task_cli_support;
7
+
8
+ use task_cli_support::{
9
+ active_task, fixture_root, git, init_git, run_json, task_state, task_state_with_active_task,
10
+ write_fixture_file, write_verification_checks,
11
+ };
12
+
13
+ #[test]
14
+ fn run_check_rejects_unknown_and_records_successful_safe_checks() {
15
+ let root = fixture_root(task_state());
16
+ init_git(&root);
17
+ write_fixture_file(&root, "README.md", "changed\n");
18
+
19
+ let unknown = run_json(&root, ["task", "run-check", "--check", "missing", "--json"]);
20
+ assert_eq!(unknown["schema"], "naome.task.run-check.v1");
21
+ assert_eq!(unknown["executed"], false);
22
+ assert_eq!(unknown["findings"][0]["id"], "task.check.unknown");
23
+
24
+ let result = run_json(
25
+ &root,
26
+ [
27
+ "task",
28
+ "run-check",
29
+ "--check",
30
+ "diff-check",
31
+ "--record-proof",
32
+ "--json",
33
+ "--agent-session",
34
+ "loop-a",
35
+ ],
36
+ );
37
+ assert_eq!(result["executed"], true);
38
+ assert_eq!(result["exitCode"], 0);
39
+ assert_eq!(result["recordedProof"], true);
40
+ assert_eq!(result["agentSession"], "loop-a");
41
+
42
+ let task_state: Value =
43
+ serde_json::from_str(&fs::read_to_string(root.join(".naome/task-state.json")).unwrap())
44
+ .unwrap();
45
+ assert_eq!(
46
+ task_state["activeTask"]["proofBatches"][0]["proofs"][0]["agentSession"],
47
+ "loop-a"
48
+ );
49
+ assert_eq!(
50
+ task_state["activeTask"]["proofBatches"][0]["proofs"][0]["command"],
51
+ "git diff --check"
52
+ );
53
+ assert_eq!(
54
+ task_state["activeTask"]["proofBatches"][0]["proofs"][0]["cwd"],
55
+ "."
56
+ );
57
+ }
58
+
59
+ #[test]
60
+ fn record_proof_requires_recent_success_evidence() {
61
+ let root = fixture_root(task_state());
62
+ init_git(&root);
63
+ write_fixture_file(&root, "README.md", "changed\n");
64
+
65
+ let recorded = run_json(
66
+ &root,
67
+ ["task", "record-proof", "--from-proof-plan", "--json"],
68
+ );
69
+
70
+ assert_eq!(recorded["recorded"], false);
71
+ assert_eq!(
72
+ recorded["findings"][0]["id"],
73
+ "task.proof.no_recent_success"
74
+ );
75
+ }
76
+
77
+ #[test]
78
+ fn record_proof_rejects_receipts_from_older_same_path_content() {
79
+ let root = fixture_root(task_state());
80
+ init_git(&root);
81
+ write_fixture_file(&root, "README.md", "changed\n");
82
+
83
+ let checked = run_json(
84
+ &root,
85
+ ["task", "run-check", "--check", "diff-check", "--json"],
86
+ );
87
+ assert_eq!(checked["executed"], true);
88
+ assert_eq!(checked["exitCode"], 0);
89
+
90
+ write_fixture_file(&root, "README.md", "changed with trailing whitespace \n");
91
+ let recorded = run_json(
92
+ &root,
93
+ ["task", "record-proof", "--from-proof-plan", "--json"],
94
+ );
95
+
96
+ assert_eq!(recorded["recorded"], false);
97
+ assert_eq!(
98
+ recorded["findings"][0]["id"],
99
+ "task.proof.no_recent_success"
100
+ );
101
+ }
102
+
103
+ #[test]
104
+ fn record_proof_rejects_receipts_from_old_check_metadata() {
105
+ let root = fixture_root(task_state());
106
+ init_git(&root);
107
+ write_fixture_file(&root, "README.md", "changed\n");
108
+
109
+ let checked = run_json(
110
+ &root,
111
+ ["task", "run-check", "--check", "diff-check", "--json"],
112
+ );
113
+ assert_eq!(checked["executed"], true);
114
+ assert_eq!(checked["exitCode"], 0);
115
+
116
+ write_verification_checks(
117
+ &root,
118
+ json!([{
119
+ "id": "diff-check",
120
+ "command": "node .naome/bin/naome.js quality check --changed",
121
+ "cwd": ".",
122
+ "purpose": "Updated check command.",
123
+ "cost": "fast",
124
+ "source": "test",
125
+ "evidence": ["README.md"],
126
+ "lastVerified": null
127
+ }]),
128
+ );
129
+ git(&root, ["add", ".naome/verification.json"]);
130
+ git(&root, ["commit", "-m", "update verification metadata"]);
131
+
132
+ let recorded = run_json(
133
+ &root,
134
+ ["task", "record-proof", "--from-proof-plan", "--json"],
135
+ );
136
+
137
+ assert_eq!(recorded["recorded"], false);
138
+ assert_eq!(
139
+ recorded["findings"][0]["id"],
140
+ "task.proof.no_recent_success"
141
+ );
142
+ }
143
+
144
+ #[test]
145
+ fn run_check_diff_check_covers_staged_diff() {
146
+ let root = fixture_root(task_state());
147
+ init_git(&root);
148
+ write_fixture_file(&root, "README.md", "staged trailing whitespace \n");
149
+ git(&root, ["add", "README.md"]);
150
+
151
+ let result = run_json(
152
+ &root,
153
+ [
154
+ "task",
155
+ "run-check",
156
+ "--check",
157
+ "diff-check",
158
+ "--record-proof",
159
+ "--json",
160
+ ],
161
+ );
162
+
163
+ assert_eq!(result["executed"], true);
164
+ assert_ne!(result["exitCode"], 0);
165
+ assert_eq!(result["recordedProof"], false);
166
+ assert!(result["stdoutSummary"]
167
+ .as_str()
168
+ .is_some_and(|summary| summary.contains("trailing whitespace")));
169
+ }
170
+
171
+ #[test]
172
+ fn repair_execute_safe_runs_checks_but_refuses_scope_repairs() {
173
+ let root = fixture_root(task_state());
174
+ init_git(&root);
175
+ write_fixture_file(&root, "README.md", "changed\n");
176
+ write_fixture_file(&root, "src/lib.rs", "outside\n");
177
+
178
+ let scope = run_json(
179
+ &root,
180
+ [
181
+ "task",
182
+ "repair",
183
+ "--plan",
184
+ "remove_out_of_scope_change_src_lib_rs",
185
+ "--execute-safe",
186
+ "--json",
187
+ ],
188
+ );
189
+ assert_eq!(scope["executed"], false);
190
+ assert_eq!(scope["requiresUserApproval"], true);
191
+
192
+ fs::remove_file(root.join("src/lib.rs")).unwrap();
193
+ let check = run_json(
194
+ &root,
195
+ [
196
+ "task",
197
+ "repair",
198
+ "--plan",
199
+ "rerun_diff-check",
200
+ "--execute-safe",
201
+ "--json",
202
+ ],
203
+ );
204
+ assert_eq!(check["executed"], true);
205
+ assert_eq!(check["steps"][0]["schema"], "naome.task.run-check.v1");
206
+ }
207
+
208
+ #[test]
209
+ fn repair_execute_safe_rejects_dry_run_combo_and_unsafe_check_plans() {
210
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
211
+ "requiredCheckIds": ["unsafe-check"],
212
+ "proofResults": []
213
+ }))));
214
+ write_verification_checks(
215
+ &root,
216
+ json!([{
217
+ "id": "unsafe-check",
218
+ "command": "sh -c 'echo unsafe'",
219
+ "cwd": ".",
220
+ "purpose": "Unsafe check for repair planning tests.",
221
+ "cost": "fast",
222
+ "source": "test",
223
+ "evidence": ["README.md"],
224
+ "lastVerified": null
225
+ }]),
226
+ );
227
+ init_git(&root);
228
+ write_fixture_file(&root, "README.md", "changed\n");
229
+
230
+ let combo = Command::new(env!("CARGO_BIN_EXE_naome"))
231
+ .args([
232
+ "task",
233
+ "repair",
234
+ "--plan",
235
+ "rerun_unsafe-check",
236
+ "--dry-run",
237
+ "--execute-safe",
238
+ "--json",
239
+ ])
240
+ .current_dir(&root)
241
+ .output()
242
+ .unwrap();
243
+ assert!(!combo.status.success());
244
+ assert!(String::from_utf8_lossy(&combo.stderr).contains("--dry-run"));
245
+
246
+ let unsafe_repair = run_json(
247
+ &root,
248
+ [
249
+ "task",
250
+ "repair",
251
+ "--plan",
252
+ "rerun_unsafe-check",
253
+ "--execute-safe",
254
+ "--json",
255
+ ],
256
+ );
257
+ assert_eq!(unsafe_repair["executed"], false);
258
+ assert_eq!(unsafe_repair["requiresUserApproval"], true);
259
+ assert!(unsafe_repair["steps"].as_array().unwrap().is_empty());
260
+ }
261
+
262
+ #[test]
263
+ fn run_check_rejects_changed_npm_scripts_from_safe_execution() {
264
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
265
+ "requiredCheckIds": ["task-state-tests"],
266
+ "allowedPaths": ["package.json"],
267
+ "proofResults": []
268
+ }))));
269
+ write_verification_checks(
270
+ &root,
271
+ json!([{
272
+ "id": "task-state-tests",
273
+ "command": "npm run test:task-state",
274
+ "cwd": ".",
275
+ "purpose": "Task-state regression tests.",
276
+ "cost": "medium",
277
+ "source": "test",
278
+ "evidence": ["package.json"],
279
+ "lastVerified": null
280
+ }]),
281
+ );
282
+ init_git(&root);
283
+ write_fixture_file(
284
+ &root,
285
+ "package.json",
286
+ r#"{"scripts":{"test:task-state":"node -e \"process.exit(0)\"}}"#,
287
+ );
288
+
289
+ let result = run_json(
290
+ &root,
291
+ ["task", "run-check", "--check", "task-state-tests", "--json"],
292
+ );
293
+
294
+ assert_eq!(result["executed"], false);
295
+ assert_eq!(result["findings"][0]["id"], "task.check.unsafe_command");
296
+ }
297
+
298
+ #[test]
299
+ fn run_check_rejects_pack_dry_run_as_not_read_only() {
300
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
301
+ "requiredCheckIds": ["package-dry-run"],
302
+ "proofResults": []
303
+ }))));
304
+ write_verification_checks(
305
+ &root,
306
+ json!([{
307
+ "id": "package-dry-run",
308
+ "command": "npm run pack:dry-run",
309
+ "cwd": ".",
310
+ "purpose": "Package dry run.",
311
+ "cost": "medium",
312
+ "source": "test",
313
+ "evidence": ["README.md"],
314
+ "lastVerified": null
315
+ }]),
316
+ );
317
+ init_git(&root);
318
+ write_fixture_file(&root, "README.md", "changed\n");
319
+
320
+ let result = run_json(
321
+ &root,
322
+ ["task", "run-check", "--check", "package-dry-run", "--json"],
323
+ );
324
+
325
+ assert_eq!(result["executed"], false);
326
+ assert_eq!(result["findings"][0]["id"], "task.check.unsafe_command");
327
+ }
328
+
329
+ #[test]
330
+ fn task_loop_read_only_and_execute_safe_drive_proof_to_completion() {
331
+ let root = fixture_root(task_state());
332
+ init_git(&root);
333
+ write_fixture_file(&root, "README.md", "changed\n");
334
+
335
+ let read_only = run_json(&root, ["task", "loop", "--json"]);
336
+ assert_eq!(read_only["schema"], "naome.task.loop.v1");
337
+ assert_eq!(read_only["mode"], "read_only");
338
+ assert!(read_only["executedSteps"].as_array().unwrap().is_empty());
339
+ assert_eq!(
340
+ read_only["status"]["proof"]["missingChecks"],
341
+ json!(["diff-check"])
342
+ );
343
+
344
+ let executed = run_json(&root, ["task", "loop", "--execute-safe", "--json"]);
345
+ assert_eq!(executed["mode"], "execute_safe");
346
+ assert_eq!(
347
+ executed["executedSteps"][0]["schema"],
348
+ "naome.task.run-check.v1"
349
+ );
350
+ assert_eq!(executed["executedSteps"][0]["recordedProof"], true);
351
+
352
+ let completed = run_json(
353
+ &root,
354
+ ["task", "complete", "--from-can-transition", "--json"],
355
+ );
356
+ assert_eq!(completed["schema"], "naome.task.complete.v1");
357
+ assert_eq!(completed["completed"], true);
358
+
359
+ let state: Value =
360
+ serde_json::from_str(&fs::read_to_string(root.join(".naome/task-state.json")).unwrap())
361
+ .unwrap();
362
+ assert_eq!(state["status"], "complete");
363
+ }
364
+
365
+ #[test]
366
+ fn complete_blocks_when_transition_is_not_allowed() {
367
+ let root = fixture_root(task_state_with_active_task(active_task(json!({
368
+ "proofResults": []
369
+ }))));
370
+ init_git(&root);
371
+ write_fixture_file(&root, "README.md", "changed\n");
372
+
373
+ let completed = run_json(
374
+ &root,
375
+ ["task", "complete", "--from-can-transition", "--json"],
376
+ );
377
+
378
+ assert_eq!(completed["completed"], false);
379
+ assert_eq!(
380
+ completed["blockingFindings"][0]["id"],
381
+ "task.proof.missing_check"
382
+ );
383
+ }