@lamentis/naome 1.4.0 → 1.4.1

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 (45) hide show
  1. package/Cargo.lock +2 -2
  2. package/crates/naome-cli/Cargo.toml +1 -1
  3. package/crates/naome-cli/src/main.rs +9 -0
  4. package/crates/naome-cli/src/task_commands/common.rs +32 -0
  5. package/crates/naome-cli/src/task_commands/readiness.rs +40 -0
  6. package/crates/naome-cli/src/task_commands/record.rs +134 -0
  7. package/crates/naome-cli/src/task_commands/repair.rs +30 -0
  8. package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
  9. package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
  10. package/crates/naome-cli/src/task_commands.rs +69 -1
  11. package/crates/naome-cli/tests/task_cli.rs +58 -0
  12. package/crates/naome-cli/tests/task_cli_agent_controls.rs +217 -0
  13. package/crates/naome-cli/tests/task_cli_control.rs +126 -0
  14. package/crates/naome-cli/tests/task_cli_support/mod.rs +150 -0
  15. package/crates/naome-core/Cargo.toml +1 -1
  16. package/crates/naome-core/src/lib.rs +7 -2
  17. package/crates/naome-core/src/task_state/mod.rs +10 -0
  18. package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
  19. package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
  20. package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
  21. package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
  22. package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
  23. package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
  24. package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
  25. package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
  26. package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
  27. package/crates/naome-core/src/task_state/status/control.rs +16 -0
  28. package/crates/naome-core/src/task_state/status/git.rs +133 -0
  29. package/crates/naome-core/src/task_state/status/model.rs +150 -0
  30. package/crates/naome-core/src/task_state/status/proof.rs +167 -0
  31. package/crates/naome-core/src/task_state/status/proof_read.rs +150 -0
  32. package/crates/naome-core/src/task_state/status/report.rs +148 -0
  33. package/crates/naome-core/src/task_state/status/report_context.rs +126 -0
  34. package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
  35. package/crates/naome-core/src/task_state/status/scope.rs +111 -0
  36. package/crates/naome-core/src/task_state/status/transition.rs +73 -0
  37. package/crates/naome-core/src/task_state/status.rs +23 -0
  38. package/crates/naome-core/src/task_state/status_output.rs +103 -0
  39. package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
  40. package/crates/naome-core/tests/task_state_support/states.rs +4 -0
  41. package/crates/naome-core/tests/task_status.rs +301 -0
  42. package/crates/naome-core/tests/task_status_git.rs +141 -0
  43. package/native/darwin-arm64/naome +0 -0
  44. package/native/linux-x64/naome +0 -0
  45. package/package.json +1 -1
@@ -0,0 +1,126 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+ use crate::task_state::util::string_array;
7
+
8
+ use super::git::git_status;
9
+ use super::model::{
10
+ TaskGitStatus, TaskModeStatus, TaskProofStatus, TaskRecommendedCommand, TaskScopeStatus,
11
+ TaskStatusFinding,
12
+ };
13
+ use super::proof::{
14
+ add_proof_findings, add_unknown_proof_findings, proof_status, recommended_commands,
15
+ };
16
+ use super::proof_read::{read_proofs, read_verification_checks};
17
+ use super::report_support::{changed_entries, state_name, task_text};
18
+ use super::scope::{add_scope_findings, scope_status};
19
+
20
+ pub(super) struct TaskStatusContext {
21
+ pub(super) state: String,
22
+ pub(super) task_id: Option<String>,
23
+ pub(super) request: Option<String>,
24
+ pub(super) task_mode: TaskModeStatus,
25
+ pub(super) git: TaskGitStatus,
26
+ pub(super) scope: TaskScopeStatus,
27
+ pub(super) proof: TaskProofStatus,
28
+ pub(super) recommended_commands: Vec<TaskRecommendedCommand>,
29
+ pub(super) findings: Vec<TaskStatusFinding>,
30
+ }
31
+
32
+ impl TaskStatusContext {
33
+ pub(super) fn new(
34
+ root: &Path,
35
+ task_state: Option<&Value>,
36
+ mut findings: Vec<TaskStatusFinding>,
37
+ ) -> Result<Self, NaomeError> {
38
+ let active_task = task_state
39
+ .and_then(|state| state.get("activeTask"))
40
+ .filter(|task| task.is_object());
41
+ let state = state_name(task_state);
42
+ add_active_task_shape_finding(&state, task_state, active_task, &mut findings);
43
+ let task_id = task_text(active_task, "id");
44
+ let request = task_text(active_task, "request");
45
+ let task_mode = task_mode(active_task);
46
+ let admission_head = active_task
47
+ .and_then(|task| task.get("admission"))
48
+ .and_then(|admission| admission.get("gitHead"))
49
+ .and_then(Value::as_str)
50
+ .map(ToString::to_string);
51
+
52
+ let changed_entries = changed_entries(root, &mut findings);
53
+ let git = git_status(root, admission_head, &mut findings)?;
54
+ let scope = scope_status(active_task, &changed_entries);
55
+ add_scope_findings(&state, active_task, &scope, &mut findings);
56
+
57
+ let verification = read_verification_checks(root, &mut findings)?;
58
+ let proofs = active_task.map(read_proofs).unwrap_or_default();
59
+ add_unknown_proof_findings(&proofs, &verification, &mut findings);
60
+ let proof = proof_status(active_task, &proofs, &scope.in_scope_changed_paths);
61
+ add_proof_findings(
62
+ &proof,
63
+ &scope.in_scope_changed_paths,
64
+ &proofs,
65
+ &mut findings,
66
+ );
67
+ let recommended_commands =
68
+ recommended_commands(&proof, &verification, &scope.in_scope_changed_paths);
69
+
70
+ Ok(Self {
71
+ state,
72
+ task_id,
73
+ request,
74
+ task_mode,
75
+ git,
76
+ scope,
77
+ proof,
78
+ recommended_commands,
79
+ findings,
80
+ })
81
+ }
82
+ }
83
+
84
+ fn add_active_task_shape_finding(
85
+ state: &str,
86
+ task_state: Option<&Value>,
87
+ active_task: Option<&Value>,
88
+ findings: &mut Vec<TaskStatusFinding>,
89
+ ) {
90
+ if !matches!(
91
+ state,
92
+ "implementing" | "blocked" | "needs_human_review" | "revising"
93
+ ) {
94
+ return;
95
+ }
96
+ if task_state.is_some() && active_task.is_none() {
97
+ findings.push(super::model::finding(
98
+ "task.state.active_task_missing",
99
+ "error",
100
+ format!("Task state is {state} but activeTask is missing or not an object."),
101
+ Some(".naome/task-state.json".to_string()),
102
+ "Restore a valid activeTask object or reset the task state before continuing.",
103
+ "Do not complete or commit a task while activeTask is missing.",
104
+ ));
105
+ }
106
+ }
107
+
108
+ fn task_mode(active_task: Option<&Value>) -> TaskModeStatus {
109
+ let kind = task_text(active_task, "kind").unwrap_or_else(|| "standard".to_string());
110
+ let declared_review_fix = active_task
111
+ .and_then(|task| string_array(task.get("declaredChangeTypes")))
112
+ .unwrap_or_default()
113
+ .iter()
114
+ .any(|value| value == "review-fix");
115
+ let review_fix = kind == "review_fix" || declared_review_fix;
116
+ TaskModeStatus {
117
+ kind,
118
+ review_fix,
119
+ scope_policy: if review_fix {
120
+ "Review-fix tasks must stay inside explicit allowedPaths."
121
+ } else {
122
+ "Task changes must stay inside explicit allowedPaths."
123
+ }
124
+ .to_string(),
125
+ }
126
+ }
@@ -0,0 +1,117 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+ use crate::task_ledger::read_task_state_projection;
8
+ use crate::task_state::git_io::read_git_changed_entries;
9
+ use crate::task_state::types::ChangedEntry;
10
+
11
+ use super::model::{finding, TaskFeedback, TaskProofStatus, TaskStatusFinding, TASK_STATE_PATH};
12
+
13
+ pub(super) fn read_task_state_for_status(
14
+ root: &Path,
15
+ findings: &mut Vec<TaskStatusFinding>,
16
+ ) -> Result<Option<Value>, NaomeError> {
17
+ let path = root.join(TASK_STATE_PATH);
18
+ if let Ok(content) = fs::read_to_string(&path) {
19
+ if has_conflict_markers(&content) {
20
+ findings.push(finding(
21
+ "task.state.conflict_markers",
22
+ "error",
23
+ ".naome/task-state.json contains unresolved conflict markers.",
24
+ Some(TASK_STATE_PATH.to_string()),
25
+ "Resolve the conflict markers and rerun naome task status.",
26
+ "Do not edit or commit task-state conflict markers.",
27
+ ));
28
+ return Ok(None);
29
+ }
30
+ if let Err(error) = serde_json::from_str::<Value>(&content) {
31
+ findings.push(finding(
32
+ "task.state.invalid_json",
33
+ "error",
34
+ format!(".naome/task-state.json is not valid JSON: {error}."),
35
+ Some(TASK_STATE_PATH.to_string()),
36
+ "Repair .naome/task-state.json to valid JSON or restore it from a clean task baseline.",
37
+ "Do not continue task work while task-state JSON is malformed.",
38
+ ));
39
+ return Ok(None);
40
+ }
41
+ }
42
+ read_task_state_projection(root)
43
+ }
44
+
45
+ pub(super) fn changed_entries(
46
+ root: &Path,
47
+ findings: &mut Vec<TaskStatusFinding>,
48
+ ) -> Vec<ChangedEntry> {
49
+ match read_git_changed_entries(root) {
50
+ Ok(entries) => entries,
51
+ Err(error) => {
52
+ findings.push(finding(
53
+ "task.git.status_unavailable",
54
+ "warning",
55
+ format!("Git changed-file status is unavailable: {error}"),
56
+ None,
57
+ "Run task status from a valid git work tree.",
58
+ "Do not infer task scope while git status is unavailable.",
59
+ ));
60
+ Vec::new()
61
+ }
62
+ }
63
+ }
64
+
65
+ pub(super) fn state_name(task_state: Option<&Value>) -> String {
66
+ task_state
67
+ .and_then(|state| state.get("status"))
68
+ .and_then(Value::as_str)
69
+ .unwrap_or("missing")
70
+ .to_string()
71
+ }
72
+
73
+ pub(super) fn task_text(active_task: Option<&Value>, key: &str) -> Option<String> {
74
+ active_task
75
+ .and_then(|task| task.get(key))
76
+ .and_then(Value::as_str)
77
+ .map(ToString::to_string)
78
+ }
79
+
80
+ pub(super) fn task_feedback(findings: &[TaskStatusFinding]) -> Vec<TaskFeedback> {
81
+ findings
82
+ .iter()
83
+ .filter(|finding| finding.severity != "info")
84
+ .map(|finding| TaskFeedback {
85
+ problem: finding.message.clone(),
86
+ repair: finding.suggested_fix.clone(),
87
+ files: finding.path.iter().cloned().collect(),
88
+ must_not_do: vec![
89
+ "Do not commit out-of-scope changes.".to_string(),
90
+ "Do not bypass the NAOME commit gate.".to_string(),
91
+ ],
92
+ })
93
+ .collect()
94
+ }
95
+
96
+ pub(super) fn next_action(
97
+ state: &str,
98
+ proof: &TaskProofStatus,
99
+ findings: &[TaskStatusFinding],
100
+ ) -> String {
101
+ if let Some(finding) = findings.iter().find(|finding| finding.severity == "error") {
102
+ return finding.suggested_fix.clone();
103
+ }
104
+ if !proof.missing_checks.is_empty() || !proof.stale_checks.is_empty() {
105
+ return "Run missing or stale checks from naome task proof-plan before completion."
106
+ .to_string();
107
+ }
108
+ match state {
109
+ "idle" | "missing" => "Create a NAOME task before feature work.".to_string(),
110
+ "complete" => "Task is complete; baseline it before starting unrelated work.".to_string(),
111
+ _ => "Continue the active task and keep proof current.".to_string(),
112
+ }
113
+ }
114
+
115
+ fn has_conflict_markers(content: &str) -> bool {
116
+ content.contains("<<<<<<<") && content.contains("=======") && content.contains(">>>>>>>")
117
+ }
@@ -0,0 +1,111 @@
1
+ use serde_json::Value;
2
+
3
+ use super::model::{finding, TaskScopeStatus, TaskStatusFinding, TASK_STATE_PATH};
4
+ use crate::task_state::types::{is_control_state_path, ChangedEntry};
5
+ use crate::task_state::util::{matches_any_pattern, string_array};
6
+
7
+ pub(super) fn scope_status(
8
+ active_task: Option<&Value>,
9
+ changed_entries: &[ChangedEntry],
10
+ ) -> TaskScopeStatus {
11
+ let allowed_paths = active_task
12
+ .and_then(|task| string_array(task.get("allowedPaths")))
13
+ .unwrap_or_default();
14
+ let changed_paths = changed_entries
15
+ .iter()
16
+ .map(|entry| entry.path.clone())
17
+ .collect::<Vec<_>>();
18
+ let task_paths = changed_paths
19
+ .iter()
20
+ .filter(|path| !is_control_state_path(path))
21
+ .cloned()
22
+ .collect::<Vec<_>>();
23
+ let in_scope_changed_paths = task_paths
24
+ .iter()
25
+ .filter(|path| matches_any_pattern(path, &allowed_paths))
26
+ .cloned()
27
+ .collect::<Vec<_>>();
28
+ let out_of_scope_changed_paths = if active_task.is_some() {
29
+ task_paths
30
+ .iter()
31
+ .filter(|path| !matches_any_pattern(path, &allowed_paths))
32
+ .cloned()
33
+ .collect::<Vec<_>>()
34
+ } else {
35
+ Vec::new()
36
+ };
37
+
38
+ TaskScopeStatus {
39
+ allowed_paths,
40
+ changed_paths,
41
+ in_scope_changed_paths,
42
+ out_of_scope_changed_paths,
43
+ }
44
+ }
45
+
46
+ pub(super) fn add_scope_findings(
47
+ state: &str,
48
+ active_task: Option<&Value>,
49
+ scope: &TaskScopeStatus,
50
+ findings: &mut Vec<TaskStatusFinding>,
51
+ ) {
52
+ for path in &scope.out_of_scope_changed_paths {
53
+ findings.push(finding(
54
+ "task.scope.out_of_scope_change",
55
+ "error",
56
+ format!("Changed file is outside the active task scope: {path}."),
57
+ Some(path.clone()),
58
+ "Either add the file through an explicit task scope revision or remove the unrelated change.",
59
+ "Do not commit out-of-scope changes or bypass the NAOME commit gate.",
60
+ ));
61
+ }
62
+
63
+ if !scope.changed_paths.is_empty()
64
+ && scope
65
+ .changed_paths
66
+ .iter()
67
+ .all(|path| is_control_state_path(path))
68
+ {
69
+ findings.push(finding(
70
+ "task.scope.control_state_only_change",
71
+ "warning",
72
+ "Only NAOME control-state files are changed.",
73
+ Some(TASK_STATE_PATH.to_string()),
74
+ "Continue only if the task-state transition is intentional and supported by proof.",
75
+ "Do not use control-state-only edits to bypass normal task admission.",
76
+ ));
77
+ }
78
+
79
+ if active_task.is_none()
80
+ && scope
81
+ .changed_paths
82
+ .iter()
83
+ .any(|path| !is_control_state_path(path))
84
+ {
85
+ findings.push(finding(
86
+ "task.state.no_active_task_with_diff",
87
+ "error",
88
+ "Changed files exist but no active task is available.",
89
+ None,
90
+ "Create or recover the active task before continuing feature work.",
91
+ "Do not commit repository changes without an admitted NAOME task.",
92
+ ));
93
+ }
94
+
95
+ if state == "complete" && !scope.changed_paths.is_empty() {
96
+ for path in scope
97
+ .changed_paths
98
+ .iter()
99
+ .filter(|path| !is_control_state_path(path))
100
+ {
101
+ findings.push(finding(
102
+ "task.state.completed_task_has_diff",
103
+ "warning",
104
+ format!("Completed task has new unbaselined diff: {path}."),
105
+ Some(path.clone()),
106
+ "Baseline the completed task or create a new task for the new diff.",
107
+ "Do not mix new work into a completed task without an explicit baseline.",
108
+ ));
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,73 @@
1
+ use super::control::agent_loop;
2
+ use super::model::{finding, TaskProofStatus, TaskStatusFinding, TransitionReadinessReport};
3
+ use super::report_context::TaskStatusContext;
4
+
5
+ pub(super) fn transition_report(
6
+ context: TaskStatusContext,
7
+ target_state: &str,
8
+ ) -> TransitionReadinessReport {
9
+ let agent_loop = agent_loop(
10
+ &context.state,
11
+ &context.proof,
12
+ &context.findings,
13
+ &context.scope,
14
+ );
15
+ let blocking_findings = transition_blockers(&context.state, &context.findings, &context.proof);
16
+ let required_before_transition = blocking_findings
17
+ .iter()
18
+ .map(|finding| finding.suggested_fix.clone())
19
+ .collect::<Vec<_>>();
20
+ TransitionReadinessReport {
21
+ schema: "naome.task.transition-readiness.v1".to_string(),
22
+ target_state: target_state.to_string(),
23
+ allowed: blocking_findings.is_empty(),
24
+ blocking_findings,
25
+ required_before_transition,
26
+ agent_loop,
27
+ }
28
+ }
29
+
30
+ fn transition_blockers(
31
+ state: &str,
32
+ findings: &[TaskStatusFinding],
33
+ proof: &TaskProofStatus,
34
+ ) -> Vec<TaskStatusFinding> {
35
+ let mut blockers = findings
36
+ .iter()
37
+ .filter(|finding| {
38
+ finding.severity == "error" || finding.id == "task.state.completed_task_has_diff"
39
+ })
40
+ .cloned()
41
+ .collect::<Vec<_>>();
42
+ if matches!(state, "idle" | "missing") {
43
+ blockers.push(finding(
44
+ "task.transition.no_active_task",
45
+ "error",
46
+ "No active task can transition to complete.",
47
+ None,
48
+ "Create or recover an active task before completing.",
49
+ "Do not complete an idle or missing task.",
50
+ ));
51
+ }
52
+ if matches!(state, "blocked" | "needs_human_review") {
53
+ blockers.push(finding(
54
+ "task.transition.blocked_state",
55
+ "error",
56
+ format!("Task state {state} cannot transition to complete."),
57
+ None,
58
+ "Resolve the blocker or required human review before completing the task.",
59
+ "Do not complete a blocked task state.",
60
+ ));
61
+ }
62
+ if !proof.stale_checks.is_empty() {
63
+ blockers.push(finding(
64
+ "task.transition.stale_proof",
65
+ "error",
66
+ "Task has stale proof.",
67
+ None,
68
+ "Rerun stale checks and record fresh proof.",
69
+ "Do not complete a task with stale proof.",
70
+ ));
71
+ }
72
+ blockers
73
+ }
@@ -0,0 +1,23 @@
1
+ mod agent_model;
2
+ mod control;
3
+ mod git;
4
+ mod model;
5
+ mod proof;
6
+ mod proof_read;
7
+ mod report;
8
+ mod report_context;
9
+ mod report_support;
10
+ mod scope;
11
+ mod transition;
12
+
13
+ pub use agent_model::{
14
+ AgentLoop, NextActionV2, PolicyHints, ProofRecording, ProofRecordingAfterSuccess,
15
+ RecoveryGuidance, RepairPlanItem,
16
+ };
17
+ pub use control::task_status_exit_code;
18
+ pub use model::{
19
+ TaskFeedback, TaskGitStatus, TaskModeStatus, TaskProofPlanReport, TaskProofStatus,
20
+ TaskRecommendedCommand, TaskScopeStatus, TaskStatusFinding, TaskStatusReportV1,
21
+ TransitionReadinessReport,
22
+ };
23
+ pub use report::{task_proof_plan, task_status_report, task_transition_readiness};
@@ -0,0 +1,103 @@
1
+ use super::status::{TaskProofPlanReport, TaskProofStatus, TaskStatusFinding, TaskStatusReportV1};
2
+
3
+ pub fn format_task_status(report: &TaskStatusReportV1) -> String {
4
+ let mut lines = vec![
5
+ format!("NAOME task status {}", report.state),
6
+ format!("agent loop: {}", report.agent_loop.state),
7
+ format!("next machine action: {}", report.next_action_v2.action_type),
8
+ format!("task: {}", report.task_id.as_deref().unwrap_or("<none>")),
9
+ format!("request: {}", report.request.as_deref().unwrap_or("<none>")),
10
+ format!(
11
+ "git head: {}",
12
+ report.git.head.as_deref().unwrap_or("<none>")
13
+ ),
14
+ format!(
15
+ "admission head: {}",
16
+ report.git.admission_head.as_deref().unwrap_or("<none>")
17
+ ),
18
+ format!(
19
+ "admission reachable: {}",
20
+ report.git.admission_head_reachable
21
+ ),
22
+ format!("changed files: {}", report.scope.changed_paths.len()),
23
+ format!(
24
+ "in scope: {}",
25
+ display_list(&report.scope.in_scope_changed_paths)
26
+ ),
27
+ format!(
28
+ "out of scope: {}",
29
+ display_list(&report.scope.out_of_scope_changed_paths)
30
+ ),
31
+ ];
32
+ append_proof_summary(&mut lines, &report.proof);
33
+ append_findings(&mut lines, &report.findings);
34
+ lines.push(format!("next action: {}", report.next_action));
35
+ lines.push(String::new());
36
+ lines.join("\n")
37
+ }
38
+
39
+ pub fn format_task_proof_plan(report: &TaskProofPlanReport) -> String {
40
+ let mut lines = vec![
41
+ format!(
42
+ "NAOME task proof plan {}",
43
+ report.task_id.as_deref().unwrap_or("<none>")
44
+ ),
45
+ format!("agent loop: {}", report.agent_loop.state),
46
+ format!("next machine action: {}", report.next_action_v2.action_type),
47
+ ];
48
+ append_proof_summary(&mut lines, &report.proof);
49
+ if !report.recommended_commands.is_empty() {
50
+ lines.push("recommended commands:".to_string());
51
+ for command in &report.recommended_commands {
52
+ lines.push(format!(
53
+ "- {}: {} (cwd {})",
54
+ command.check_id, command.command, command.cwd
55
+ ));
56
+ }
57
+ }
58
+ append_findings(&mut lines, &report.findings);
59
+ lines.push(format!("next action: {}", report.next_action));
60
+ lines.push(String::new());
61
+ lines.join("\n")
62
+ }
63
+
64
+ fn append_proof_summary(lines: &mut Vec<String>, proof: &TaskProofStatus) {
65
+ lines.push(format!(
66
+ "required checks: {}",
67
+ display_list(&proof.required_checks)
68
+ ));
69
+ lines.push(format!(
70
+ "passed checks: {}",
71
+ display_list(&proof.passed_checks)
72
+ ));
73
+ lines.push(format!(
74
+ "missing checks: {}",
75
+ display_list(&proof.missing_checks)
76
+ ));
77
+ lines.push(format!(
78
+ "stale checks: {}",
79
+ display_list(&proof.stale_checks)
80
+ ));
81
+ }
82
+
83
+ fn append_findings(lines: &mut Vec<String>, findings: &[TaskStatusFinding]) {
84
+ if findings.is_empty() {
85
+ return;
86
+ }
87
+ lines.push("findings:".to_string());
88
+ for finding in findings {
89
+ lines.push(format!(
90
+ "- {} {} {}",
91
+ finding.severity, finding.id, finding.message
92
+ ));
93
+ lines.push(format!(" fix: {}", finding.suggested_fix));
94
+ }
95
+ }
96
+
97
+ fn display_list(values: &[String]) -> String {
98
+ if values.is_empty() {
99
+ "<none>".to_string()
100
+ } else {
101
+ values.join(", ")
102
+ }
103
+ }
@@ -1,3 +1,5 @@
1
+ #![allow(dead_code, unused_imports)]
2
+
1
3
  mod states;
2
4
 
3
5
  use std::collections::HashMap;
@@ -12,7 +14,7 @@ use serde_json::{json, Value};
12
14
  use sha2::{Digest, Sha256};
13
15
  pub use states::{
14
16
  active_task, complete_task_state, idle_task_state, ready_empty_verification_fixture,
15
- successful_admission, successful_proof,
17
+ successful_admission, successful_proof, task_state_with_status,
16
18
  };
17
19
 
18
20
  static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
@@ -88,6 +90,18 @@ impl TaskFixture {
88
90
  self.refresh_admission_head();
89
91
  }
90
92
 
93
+ pub fn set_admission_head(&self, head: &str) {
94
+ let path = self.root.join(".naome/task-state.json");
95
+ let mut task_state: Value =
96
+ serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
97
+ task_state["activeTask"]["admission"]["gitHead"] = json!(head);
98
+ fs::write(
99
+ path,
100
+ format!("{}\n", serde_json::to_string_pretty(&task_state).unwrap()),
101
+ )
102
+ .unwrap();
103
+ }
104
+
91
105
  pub fn git<const N: usize>(&self, args: [&str; N]) -> String {
92
106
  let output = Command::new("git")
93
107
  .args(args)
@@ -12,6 +12,10 @@ pub fn complete_task_state(overrides: Value) -> Value {
12
12
  )
13
13
  }
14
14
 
15
+ pub fn task_state_with_status(status: &str, active_task: Value) -> Value {
16
+ task_state_record(status, active_task, json!("2026-05-04T12:00:00.000Z"))
17
+ }
18
+
15
19
  fn task_state_record(status: &str, active_task: Value, updated_at: Value) -> Value {
16
20
  let mut state: Value = serde_json::from_str(include_str!(
17
21
  "../../../../templates/naome-root/.naome/task-state.json"