@lamentis/naome 1.4.0 → 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 (64) 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 +13 -0
  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 +70 -0
  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 +44 -0
  14. package/crates/naome-cli/src/task_commands/record.rs +236 -0
  15. package/crates/naome-cli/src/task_commands/repair.rs +77 -0
  16. package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
  17. package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
  18. package/crates/naome-cli/src/task_commands.rs +80 -1
  19. package/crates/naome-cli/tests/task_cli.rs +58 -0
  20. package/crates/naome-cli/tests/task_cli_agent_controls.rs +210 -0
  21. package/crates/naome-cli/tests/task_cli_control.rs +126 -0
  22. package/crates/naome-cli/tests/task_cli_loop.rs +383 -0
  23. package/crates/naome-cli/tests/task_cli_loop_edit.rs +144 -0
  24. package/crates/naome-cli/tests/task_cli_support/mod.rs +178 -0
  25. package/crates/naome-core/Cargo.toml +1 -1
  26. package/crates/naome-core/src/lib.rs +7 -2
  27. package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
  28. package/crates/naome-core/src/task_state/mod.rs +12 -0
  29. package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
  30. package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
  31. package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
  32. package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
  33. package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
  34. package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
  35. package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
  36. package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
  37. package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
  38. package/crates/naome-core/src/task_state/status/control.rs +16 -0
  39. package/crates/naome-core/src/task_state/status/git.rs +133 -0
  40. package/crates/naome-core/src/task_state/status/model.rs +152 -0
  41. package/crates/naome-core/src/task_state/status/proof.rs +217 -0
  42. package/crates/naome-core/src/task_state/status/proof_read.rs +164 -0
  43. package/crates/naome-core/src/task_state/status/report.rs +148 -0
  44. package/crates/naome-core/src/task_state/status/report_context.rs +148 -0
  45. package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
  46. package/crates/naome-core/src/task_state/status/scope.rs +111 -0
  47. package/crates/naome-core/src/task_state/status/transition.rs +101 -0
  48. package/crates/naome-core/src/task_state/status.rs +23 -0
  49. package/crates/naome-core/src/task_state/status_output.rs +103 -0
  50. package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
  51. package/crates/naome-core/tests/task_state_support/states.rs +4 -0
  52. package/crates/naome-core/tests/task_status.rs +423 -0
  53. package/crates/naome-core/tests/task_status_git.rs +141 -0
  54. package/installer/context.js +1 -1
  55. package/installer/harness-verification.js +2 -6
  56. package/installer/manifest-state.js +2 -2
  57. package/installer/native.js +3 -31
  58. package/native/darwin-arm64/naome +0 -0
  59. package/native/linux-x64/naome +0 -0
  60. package/package.json +1 -1
  61. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  62. package/templates/naome-root/.naome/bin/check-task-state.js +4 -39
  63. package/templates/naome-root/.naome/bin/naome.js +2 -30
  64. package/templates/naome-root/.naome/manifest.json +2 -2
@@ -0,0 +1,148 @@
1
+ use std::path::Path;
2
+
3
+ use crate::models::NaomeError;
4
+
5
+ use super::agent_model::{AgentLoop, NextActionV2, RepairPlanItem};
6
+ use super::control::{
7
+ agent_loop, next_action_v2, policy_hints, proof_recording, recovery_guidance, repair_plan,
8
+ };
9
+ use super::model::{
10
+ TaskProofPlanReport, TaskStatusReportV1, TransitionReadinessReport, PROOF_PLAN_SCHEMA,
11
+ STATUS_SCHEMA,
12
+ };
13
+ use super::report_context::TaskStatusContext;
14
+ use super::report_support::{next_action, read_task_state_for_status, task_feedback};
15
+ use super::transition::transition_report;
16
+
17
+ pub fn task_status_report(root: &Path) -> Result<TaskStatusReportV1, NaomeError> {
18
+ let mut findings = Vec::new();
19
+ let task_state = read_task_state_for_status(root, &mut findings)?;
20
+ let context = TaskStatusContext::new(root, task_state.as_ref(), findings)?;
21
+ Ok(status_report(context))
22
+ }
23
+
24
+ pub fn task_proof_plan(root: &Path) -> Result<TaskProofPlanReport, NaomeError> {
25
+ let mut findings = Vec::new();
26
+ let task_state = read_task_state_for_status(root, &mut findings)?;
27
+ let context = TaskStatusContext::new(root, task_state.as_ref(), findings)?;
28
+ Ok(proof_plan_report(context))
29
+ }
30
+
31
+ pub fn task_transition_readiness(
32
+ root: &Path,
33
+ target_state: &str,
34
+ ) -> Result<TransitionReadinessReport, NaomeError> {
35
+ if target_state != "complete" {
36
+ return Err(NaomeError::new(format!(
37
+ "unsupported task transition target: {target_state}; v1.4.1 supports only complete"
38
+ )));
39
+ }
40
+ let mut findings = Vec::new();
41
+ let task_state = read_task_state_for_status(root, &mut findings)?;
42
+ let context = TaskStatusContext::new(root, task_state.as_ref(), findings)?;
43
+ Ok(transition_report(context, target_state))
44
+ }
45
+
46
+ fn status_report(context: TaskStatusContext) -> TaskStatusReportV1 {
47
+ let blocked = context
48
+ .findings
49
+ .iter()
50
+ .any(|finding| finding.severity == "error");
51
+ let next_action = next_action(&context.state, &context.proof, &context.findings);
52
+ let agent = agent_control(&context);
53
+ let policy_hints = policy_hints(&context.scope, &context.proof, &context.findings);
54
+ let recovery_guidance = recovery_guidance(&context.findings);
55
+ TaskStatusReportV1 {
56
+ schema: STATUS_SCHEMA.to_string(),
57
+ state: context.state,
58
+ task_id: context.task_id,
59
+ request: context.request,
60
+ task_mode: context.task_mode,
61
+ git: context.git,
62
+ scope: context.scope,
63
+ proof: context.proof,
64
+ blocked,
65
+ task_feedback: task_feedback(&context.findings),
66
+ findings: context.findings,
67
+ next_action,
68
+ next_action_v2: agent.next_action,
69
+ agent_loop: agent.loop_state,
70
+ repair_plan: agent.repair_plan,
71
+ policy_hints,
72
+ recovery_guidance,
73
+ }
74
+ }
75
+
76
+ fn proof_plan_report(context: TaskStatusContext) -> TaskProofPlanReport {
77
+ let next_action = proof_plan_next_action(&context);
78
+ let agent = agent_control(&context);
79
+ let proof_recording =
80
+ proof_recording(context.task_id.as_deref(), &context.scope, &context.proof);
81
+ let policy_hints = policy_hints(&context.scope, &context.proof, &context.findings);
82
+ let recovery_guidance = recovery_guidance(&context.findings);
83
+ let blocked = context
84
+ .findings
85
+ .iter()
86
+ .any(|finding| finding.id == "task.scope.out_of_scope_change");
87
+ TaskProofPlanReport {
88
+ schema: PROOF_PLAN_SCHEMA.to_string(),
89
+ state: context.state,
90
+ task_id: context.task_id,
91
+ task_mode: context.task_mode,
92
+ proof: context.proof,
93
+ recommended_commands: context.recommended_commands,
94
+ blocked,
95
+ task_feedback: task_feedback(&context.findings),
96
+ findings: context.findings,
97
+ next_action,
98
+ next_action_v2: agent.next_action,
99
+ agent_loop: agent.loop_state,
100
+ repair_plan: agent.repair_plan,
101
+ proof_recording,
102
+ policy_hints,
103
+ recovery_guidance,
104
+ }
105
+ }
106
+
107
+ struct AgentControl {
108
+ next_action: NextActionV2,
109
+ loop_state: AgentLoop,
110
+ repair_plan: Vec<RepairPlanItem>,
111
+ }
112
+
113
+ fn agent_control(context: &TaskStatusContext) -> AgentControl {
114
+ AgentControl {
115
+ next_action: next_action_v2(
116
+ &context.state,
117
+ &context.proof,
118
+ &context.findings,
119
+ &context.recommended_commands,
120
+ &context.scope,
121
+ ),
122
+ loop_state: agent_loop(
123
+ &context.state,
124
+ &context.proof,
125
+ &context.findings,
126
+ &context.scope,
127
+ ),
128
+ repair_plan: repair_plan(
129
+ &context.proof,
130
+ &context.findings,
131
+ &context.recommended_commands,
132
+ &context.scope,
133
+ ),
134
+ }
135
+ }
136
+
137
+ fn proof_plan_next_action(context: &TaskStatusContext) -> String {
138
+ if !context.proof.missing_checks.is_empty() || !context.proof.stale_checks.is_empty() {
139
+ if context.recommended_commands.is_empty() {
140
+ "Recover verification metadata or run the missing/stale checks manually, then record the proof in task-state.".to_string()
141
+ } else {
142
+ "Run the recommended missing or stale checks, then record the proof in task-state."
143
+ .to_string()
144
+ }
145
+ } else {
146
+ "Proof evidence is current for the active task.".to_string()
147
+ }
148
+ }
@@ -0,0 +1,148 @@
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
+ pub(super) human_review_pending: bool,
31
+ pub(super) blocker_present: bool,
32
+ }
33
+
34
+ impl TaskStatusContext {
35
+ pub(super) fn new(
36
+ root: &Path,
37
+ task_state: Option<&Value>,
38
+ mut findings: Vec<TaskStatusFinding>,
39
+ ) -> Result<Self, NaomeError> {
40
+ let active_task = task_state
41
+ .and_then(|state| state.get("activeTask"))
42
+ .filter(|task| task.is_object());
43
+ let state = state_name(task_state);
44
+ add_active_task_shape_finding(&state, task_state, active_task, &mut findings);
45
+ let task_id = task_text(active_task, "id");
46
+ let request = task_text(active_task, "request");
47
+ let task_mode = task_mode(active_task);
48
+ let admission_head = active_task
49
+ .and_then(|task| task.get("admission"))
50
+ .and_then(|admission| admission.get("gitHead"))
51
+ .and_then(Value::as_str)
52
+ .map(ToString::to_string);
53
+
54
+ let changed_entries = changed_entries(root, &mut findings);
55
+ let git = git_status(root, admission_head, &mut findings)?;
56
+ let scope = scope_status(active_task, &changed_entries);
57
+ add_scope_findings(&state, active_task, &scope, &mut findings);
58
+
59
+ let verification = read_verification_checks(root, &mut findings)?;
60
+ let proofs = active_task.map(read_proofs).unwrap_or_default();
61
+ add_unknown_proof_findings(&proofs, &verification, &mut findings);
62
+ let proof = proof_status(root, active_task, &proofs, &scope.in_scope_changed_paths)?;
63
+ add_proof_findings(
64
+ &proof,
65
+ &scope.in_scope_changed_paths,
66
+ &proofs,
67
+ &mut findings,
68
+ );
69
+ let recommended_commands =
70
+ recommended_commands(&proof, &verification, &scope.in_scope_changed_paths);
71
+
72
+ Ok(Self {
73
+ state,
74
+ task_id,
75
+ request,
76
+ task_mode,
77
+ git,
78
+ scope,
79
+ proof,
80
+ recommended_commands,
81
+ findings,
82
+ human_review_pending: human_review_pending(active_task),
83
+ blocker_present: task_state
84
+ .and_then(|state| state.get("blocker"))
85
+ .is_some_and(|blocker| !blocker.is_null()),
86
+ })
87
+ }
88
+ }
89
+
90
+ fn add_active_task_shape_finding(
91
+ state: &str,
92
+ task_state: Option<&Value>,
93
+ active_task: Option<&Value>,
94
+ findings: &mut Vec<TaskStatusFinding>,
95
+ ) {
96
+ if !matches!(
97
+ state,
98
+ "implementing" | "blocked" | "needs_human_review" | "revising"
99
+ ) {
100
+ return;
101
+ }
102
+ if task_state.is_some() && active_task.is_none() {
103
+ findings.push(super::model::finding(
104
+ "task.state.active_task_missing",
105
+ "error",
106
+ format!("Task state is {state} but activeTask is missing or not an object."),
107
+ Some(".naome/task-state.json".to_string()),
108
+ "Restore a valid activeTask object or reset the task state before continuing.",
109
+ "Do not complete or commit a task while activeTask is missing.",
110
+ ));
111
+ }
112
+ }
113
+
114
+ fn human_review_pending(active_task: Option<&Value>) -> bool {
115
+ active_task
116
+ .and_then(|task| task.get("humanReview"))
117
+ .and_then(Value::as_object)
118
+ .is_some_and(|review| {
119
+ review
120
+ .get("required")
121
+ .and_then(Value::as_bool)
122
+ .unwrap_or(false)
123
+ && !review
124
+ .get("approved")
125
+ .and_then(Value::as_bool)
126
+ .unwrap_or(false)
127
+ })
128
+ }
129
+
130
+ fn task_mode(active_task: Option<&Value>) -> TaskModeStatus {
131
+ let kind = task_text(active_task, "kind").unwrap_or_else(|| "standard".to_string());
132
+ let declared_review_fix = active_task
133
+ .and_then(|task| string_array(task.get("declaredChangeTypes")))
134
+ .unwrap_or_default()
135
+ .iter()
136
+ .any(|value| value == "review-fix");
137
+ let review_fix = kind == "review_fix" || declared_review_fix;
138
+ TaskModeStatus {
139
+ kind,
140
+ review_fix,
141
+ scope_policy: if review_fix {
142
+ "Review-fix tasks must stay inside explicit allowedPaths."
143
+ } else {
144
+ "Task changes must stay inside explicit allowedPaths."
145
+ }
146
+ .to_string(),
147
+ }
148
+ }
@@ -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,101 @@
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(
16
+ &context.state,
17
+ &context.findings,
18
+ &context.proof,
19
+ context.human_review_pending,
20
+ context.blocker_present,
21
+ );
22
+ let required_before_transition = blocking_findings
23
+ .iter()
24
+ .map(|finding| finding.suggested_fix.clone())
25
+ .collect::<Vec<_>>();
26
+ TransitionReadinessReport {
27
+ schema: "naome.task.transition-readiness.v1".to_string(),
28
+ target_state: target_state.to_string(),
29
+ allowed: blocking_findings.is_empty(),
30
+ blocking_findings,
31
+ required_before_transition,
32
+ agent_loop,
33
+ }
34
+ }
35
+
36
+ fn transition_blockers(
37
+ state: &str,
38
+ findings: &[TaskStatusFinding],
39
+ proof: &TaskProofStatus,
40
+ human_review_pending: bool,
41
+ blocker_present: bool,
42
+ ) -> Vec<TaskStatusFinding> {
43
+ let mut blockers = findings
44
+ .iter()
45
+ .filter(|finding| {
46
+ finding.severity == "error" || finding.id == "task.state.completed_task_has_diff"
47
+ })
48
+ .cloned()
49
+ .collect::<Vec<_>>();
50
+ if matches!(state, "idle" | "missing") {
51
+ blockers.push(finding(
52
+ "task.transition.no_active_task",
53
+ "error",
54
+ "No active task can transition to complete.",
55
+ None,
56
+ "Create or recover an active task before completing.",
57
+ "Do not complete an idle or missing task.",
58
+ ));
59
+ }
60
+ if matches!(state, "blocked" | "needs_human_review") {
61
+ blockers.push(finding(
62
+ "task.transition.blocked_state",
63
+ "error",
64
+ format!("Task state {state} cannot transition to complete."),
65
+ None,
66
+ "Resolve the blocker or required human review before completing the task.",
67
+ "Do not complete a blocked task state.",
68
+ ));
69
+ }
70
+ if human_review_pending {
71
+ blockers.push(finding(
72
+ "task.transition.human_review_required",
73
+ "error",
74
+ "Task requires human review approval before completion.",
75
+ Some(".naome/task-state.json".to_string()),
76
+ "Wait for explicit human review approval before completing the task.",
77
+ "Do not complete a task while humanReview.required is true and approved is false.",
78
+ ));
79
+ }
80
+ if blocker_present && !matches!(state, "blocked" | "needs_human_review") {
81
+ blockers.push(finding(
82
+ "task.transition.blocker_present",
83
+ "error",
84
+ "Task-state has a blocker object that must be resolved before completion.",
85
+ Some(".naome/task-state.json".to_string()),
86
+ "Resolve or clear the blocker through an existing safe task-state path before completing.",
87
+ "Do not complete a task while .naome/task-state.json blocker is still present.",
88
+ ));
89
+ }
90
+ if !proof.stale_checks.is_empty() {
91
+ blockers.push(finding(
92
+ "task.transition.stale_proof",
93
+ "error",
94
+ "Task has stale proof.",
95
+ None,
96
+ "Rerun stale checks and record fresh proof.",
97
+ "Do not complete a task with stale proof.",
98
+ ));
99
+ }
100
+ blockers
101
+ }
@@ -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};