@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.
- package/Cargo.lock +2 -2
- package/README.md +17 -122
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/main.rs +13 -0
- package/crates/naome-cli/src/task_commands/can_edit.rs +116 -0
- package/crates/naome-cli/src/task_commands/check_run/output.rs +34 -0
- package/crates/naome-cli/src/task_commands/check_run/receipts.rs +155 -0
- package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
- package/crates/naome-cli/src/task_commands/check_run.rs +192 -0
- package/crates/naome-cli/src/task_commands/common.rs +70 -0
- package/crates/naome-cli/src/task_commands/complete.rs +43 -0
- package/crates/naome-cli/src/task_commands/loop_control.rs +55 -0
- package/crates/naome-cli/src/task_commands/readiness.rs +44 -0
- package/crates/naome-cli/src/task_commands/record.rs +236 -0
- package/crates/naome-cli/src/task_commands/repair.rs +77 -0
- package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
- package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
- package/crates/naome-cli/src/task_commands.rs +80 -1
- package/crates/naome-cli/tests/task_cli.rs +58 -0
- package/crates/naome-cli/tests/task_cli_agent_controls.rs +210 -0
- package/crates/naome-cli/tests/task_cli_control.rs +126 -0
- package/crates/naome-cli/tests/task_cli_loop.rs +383 -0
- package/crates/naome-cli/tests/task_cli_loop_edit.rs +144 -0
- package/crates/naome-cli/tests/task_cli_support/mod.rs +178 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/lib.rs +7 -2
- package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
- package/crates/naome-core/src/task_state/mod.rs +12 -0
- package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
- package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
- package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
- package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
- package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
- package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
- package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
- package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
- package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
- package/crates/naome-core/src/task_state/status/control.rs +16 -0
- package/crates/naome-core/src/task_state/status/git.rs +133 -0
- package/crates/naome-core/src/task_state/status/model.rs +152 -0
- package/crates/naome-core/src/task_state/status/proof.rs +217 -0
- package/crates/naome-core/src/task_state/status/proof_read.rs +164 -0
- package/crates/naome-core/src/task_state/status/report.rs +148 -0
- package/crates/naome-core/src/task_state/status/report_context.rs +148 -0
- package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
- package/crates/naome-core/src/task_state/status/scope.rs +111 -0
- package/crates/naome-core/src/task_state/status/transition.rs +101 -0
- package/crates/naome-core/src/task_state/status.rs +23 -0
- package/crates/naome-core/src/task_state/status_output.rs +103 -0
- package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
- package/crates/naome-core/tests/task_state_support/states.rs +4 -0
- package/crates/naome-core/tests/task_status.rs +423 -0
- package/crates/naome-core/tests/task_status_git.rs +141 -0
- package/installer/context.js +1 -1
- package/installer/harness-verification.js +2 -6
- package/installer/manifest-state.js +2 -2
- package/installer/native.js +3 -31
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
- package/templates/naome-root/.naome/bin/check-task-state.js +4 -39
- package/templates/naome-root/.naome/bin/naome.js +2 -30
- 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};
|