@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.
- package/Cargo.lock +2 -2
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/main.rs +9 -0
- package/crates/naome-cli/src/task_commands/common.rs +32 -0
- package/crates/naome-cli/src/task_commands/readiness.rs +40 -0
- package/crates/naome-cli/src/task_commands/record.rs +134 -0
- package/crates/naome-cli/src/task_commands/repair.rs +30 -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 +69 -1
- package/crates/naome-cli/tests/task_cli.rs +58 -0
- package/crates/naome-cli/tests/task_cli_agent_controls.rs +217 -0
- package/crates/naome-cli/tests/task_cli_control.rs +126 -0
- package/crates/naome-cli/tests/task_cli_support/mod.rs +150 -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/mod.rs +10 -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 +150 -0
- package/crates/naome-core/src/task_state/status/proof.rs +167 -0
- package/crates/naome-core/src/task_state/status/proof_read.rs +150 -0
- package/crates/naome-core/src/task_state/status/report.rs +148 -0
- package/crates/naome-core/src/task_state/status/report_context.rs +126 -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 +73 -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 +301 -0
- package/crates/naome-core/tests/task_status_git.rs +141 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- 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"
|