@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,150 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
|
|
3
|
+
use super::agent_model::{
|
|
4
|
+
AgentLoop, NextActionV2, PolicyHints, ProofRecording, RecoveryGuidance, RepairPlanItem,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
pub(super) const STATUS_SCHEMA: &str = "naome.task.status.v1";
|
|
8
|
+
pub(super) const PROOF_PLAN_SCHEMA: &str = "naome.task.proof-plan.v1";
|
|
9
|
+
pub(super) const TASK_STATE_PATH: &str = ".naome/task-state.json";
|
|
10
|
+
|
|
11
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
12
|
+
#[serde(rename_all = "camelCase")]
|
|
13
|
+
pub struct TaskStatusReportV1 {
|
|
14
|
+
pub schema: String,
|
|
15
|
+
pub state: String,
|
|
16
|
+
pub task_id: Option<String>,
|
|
17
|
+
pub request: Option<String>,
|
|
18
|
+
pub task_mode: TaskModeStatus,
|
|
19
|
+
pub git: TaskGitStatus,
|
|
20
|
+
pub scope: TaskScopeStatus,
|
|
21
|
+
pub proof: TaskProofStatus,
|
|
22
|
+
pub blocked: bool,
|
|
23
|
+
pub findings: Vec<TaskStatusFinding>,
|
|
24
|
+
pub next_action: String,
|
|
25
|
+
pub next_action_v2: NextActionV2,
|
|
26
|
+
pub agent_loop: AgentLoop,
|
|
27
|
+
pub repair_plan: Vec<RepairPlanItem>,
|
|
28
|
+
pub policy_hints: PolicyHints,
|
|
29
|
+
pub recovery_guidance: Vec<RecoveryGuidance>,
|
|
30
|
+
pub task_feedback: Vec<TaskFeedback>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
34
|
+
#[serde(rename_all = "camelCase")]
|
|
35
|
+
pub struct TaskProofPlanReport {
|
|
36
|
+
pub schema: String,
|
|
37
|
+
pub state: String,
|
|
38
|
+
pub task_id: Option<String>,
|
|
39
|
+
pub task_mode: TaskModeStatus,
|
|
40
|
+
pub proof: TaskProofStatus,
|
|
41
|
+
pub recommended_commands: Vec<TaskRecommendedCommand>,
|
|
42
|
+
pub blocked: bool,
|
|
43
|
+
pub findings: Vec<TaskStatusFinding>,
|
|
44
|
+
pub next_action: String,
|
|
45
|
+
pub next_action_v2: NextActionV2,
|
|
46
|
+
pub agent_loop: AgentLoop,
|
|
47
|
+
pub repair_plan: Vec<RepairPlanItem>,
|
|
48
|
+
pub proof_recording: ProofRecording,
|
|
49
|
+
pub policy_hints: PolicyHints,
|
|
50
|
+
pub recovery_guidance: Vec<RecoveryGuidance>,
|
|
51
|
+
pub task_feedback: Vec<TaskFeedback>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
55
|
+
#[serde(rename_all = "camelCase")]
|
|
56
|
+
pub struct TransitionReadinessReport {
|
|
57
|
+
pub schema: String,
|
|
58
|
+
pub target_state: String,
|
|
59
|
+
pub allowed: bool,
|
|
60
|
+
pub blocking_findings: Vec<TaskStatusFinding>,
|
|
61
|
+
pub required_before_transition: Vec<String>,
|
|
62
|
+
pub agent_loop: AgentLoop,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
66
|
+
#[serde(rename_all = "camelCase")]
|
|
67
|
+
pub struct TaskGitStatus {
|
|
68
|
+
pub head: Option<String>,
|
|
69
|
+
pub admission_head: Option<String>,
|
|
70
|
+
pub admission_head_reachable: bool,
|
|
71
|
+
pub upstream: Option<String>,
|
|
72
|
+
pub ahead: usize,
|
|
73
|
+
pub behind: usize,
|
|
74
|
+
pub operation_in_progress: Option<String>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
78
|
+
#[serde(rename_all = "camelCase")]
|
|
79
|
+
pub struct TaskModeStatus {
|
|
80
|
+
pub kind: String,
|
|
81
|
+
pub review_fix: bool,
|
|
82
|
+
pub scope_policy: String,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
86
|
+
#[serde(rename_all = "camelCase")]
|
|
87
|
+
pub struct TaskScopeStatus {
|
|
88
|
+
pub allowed_paths: Vec<String>,
|
|
89
|
+
pub changed_paths: Vec<String>,
|
|
90
|
+
pub in_scope_changed_paths: Vec<String>,
|
|
91
|
+
pub out_of_scope_changed_paths: Vec<String>,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
95
|
+
#[serde(rename_all = "camelCase")]
|
|
96
|
+
pub struct TaskProofStatus {
|
|
97
|
+
pub required_checks: Vec<String>,
|
|
98
|
+
pub passed_checks: Vec<String>,
|
|
99
|
+
pub missing_checks: Vec<String>,
|
|
100
|
+
pub stale_checks: Vec<String>,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
104
|
+
#[serde(rename_all = "camelCase")]
|
|
105
|
+
pub struct TaskStatusFinding {
|
|
106
|
+
pub id: String,
|
|
107
|
+
pub severity: String,
|
|
108
|
+
pub message: String,
|
|
109
|
+
pub path: Option<String>,
|
|
110
|
+
pub suggested_fix: String,
|
|
111
|
+
pub agent_instruction: String,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
115
|
+
#[serde(rename_all = "camelCase")]
|
|
116
|
+
pub struct TaskFeedback {
|
|
117
|
+
pub problem: String,
|
|
118
|
+
pub repair: String,
|
|
119
|
+
pub files: Vec<String>,
|
|
120
|
+
pub must_not_do: Vec<String>,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
124
|
+
#[serde(rename_all = "camelCase")]
|
|
125
|
+
pub struct TaskRecommendedCommand {
|
|
126
|
+
pub check_id: String,
|
|
127
|
+
pub command: String,
|
|
128
|
+
pub cwd: String,
|
|
129
|
+
pub reason: String,
|
|
130
|
+
pub selection_reason: String,
|
|
131
|
+
pub impacted_paths: Vec<String>,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
pub(super) fn finding(
|
|
135
|
+
id: &str,
|
|
136
|
+
severity: &str,
|
|
137
|
+
message: impl Into<String>,
|
|
138
|
+
path: Option<String>,
|
|
139
|
+
suggested_fix: &str,
|
|
140
|
+
agent_instruction: &str,
|
|
141
|
+
) -> TaskStatusFinding {
|
|
142
|
+
TaskStatusFinding {
|
|
143
|
+
id: id.to_string(),
|
|
144
|
+
severity: severity.to_string(),
|
|
145
|
+
message: message.into(),
|
|
146
|
+
path,
|
|
147
|
+
suggested_fix: suggested_fix.to_string(),
|
|
148
|
+
agent_instruction: agent_instruction.to_string(),
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use crate::task_state::util::string_array;
|
|
6
|
+
|
|
7
|
+
use super::model::{finding, TaskProofStatus, TaskRecommendedCommand, TaskStatusFinding};
|
|
8
|
+
use super::proof_read::{ProofRecord, VerificationCheck};
|
|
9
|
+
|
|
10
|
+
pub(super) fn proof_status(
|
|
11
|
+
active_task: Option<&Value>,
|
|
12
|
+
proofs: &[ProofRecord],
|
|
13
|
+
current_task_paths: &[String],
|
|
14
|
+
) -> TaskProofStatus {
|
|
15
|
+
let required_checks = active_task
|
|
16
|
+
.and_then(|task| string_array(task.get("requiredCheckIds")))
|
|
17
|
+
.unwrap_or_default();
|
|
18
|
+
let mut passed_checks = Vec::new();
|
|
19
|
+
let mut missing_checks = Vec::new();
|
|
20
|
+
let mut stale_checks = Vec::new();
|
|
21
|
+
|
|
22
|
+
for check_id in &required_checks {
|
|
23
|
+
let successful = proofs
|
|
24
|
+
.iter()
|
|
25
|
+
.filter(|proof| proof.check_id == *check_id && proof.exit_code == 0)
|
|
26
|
+
.collect::<Vec<_>>();
|
|
27
|
+
if successful.is_empty() {
|
|
28
|
+
missing_checks.push(check_id.clone());
|
|
29
|
+
} else if successful
|
|
30
|
+
.iter()
|
|
31
|
+
.any(|proof| evidence_covers(&proof.evidence_paths, current_task_paths))
|
|
32
|
+
{
|
|
33
|
+
passed_checks.push(check_id.clone());
|
|
34
|
+
} else {
|
|
35
|
+
stale_checks.push(check_id.clone());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
TaskProofStatus {
|
|
40
|
+
required_checks,
|
|
41
|
+
passed_checks,
|
|
42
|
+
missing_checks,
|
|
43
|
+
stale_checks,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub(super) fn add_proof_findings(
|
|
48
|
+
proof: &TaskProofStatus,
|
|
49
|
+
current_task_paths: &[String],
|
|
50
|
+
proofs: &[ProofRecord],
|
|
51
|
+
findings: &mut Vec<TaskStatusFinding>,
|
|
52
|
+
) {
|
|
53
|
+
for check_id in &proof.missing_checks {
|
|
54
|
+
findings.push(finding(
|
|
55
|
+
"task.proof.missing_check",
|
|
56
|
+
"error",
|
|
57
|
+
format!("Required check has no passing proof: {check_id}."),
|
|
58
|
+
None,
|
|
59
|
+
"Run the required check and record passing proof before completing the task.",
|
|
60
|
+
"Do not mark a task complete while required checks are missing.",
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for check_id in &proof.stale_checks {
|
|
65
|
+
let files = stale_files_for_check(check_id, current_task_paths, proofs);
|
|
66
|
+
if files.is_empty() {
|
|
67
|
+
findings.push(finding(
|
|
68
|
+
"task.scope.missing_evidence",
|
|
69
|
+
"warning",
|
|
70
|
+
format!("Proof evidence is stale for required check: {check_id}."),
|
|
71
|
+
None,
|
|
72
|
+
"Rerun the check and record evidence for the current task-owned diff.",
|
|
73
|
+
"Do not reuse stale proof for new changed files.",
|
|
74
|
+
));
|
|
75
|
+
}
|
|
76
|
+
for file in files {
|
|
77
|
+
findings.push(finding(
|
|
78
|
+
"task.scope.missing_evidence",
|
|
79
|
+
"warning",
|
|
80
|
+
format!("Proof evidence for {check_id} does not cover changed file: {file}."),
|
|
81
|
+
Some(file),
|
|
82
|
+
"Rerun the check and record evidence for the current task-owned diff.",
|
|
83
|
+
"Do not reuse stale proof for new changed files.",
|
|
84
|
+
));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub(super) fn add_unknown_proof_findings(
|
|
90
|
+
proofs: &[ProofRecord],
|
|
91
|
+
verification: &BTreeMap<String, VerificationCheck>,
|
|
92
|
+
findings: &mut Vec<TaskStatusFinding>,
|
|
93
|
+
) {
|
|
94
|
+
let mut seen = BTreeSet::new();
|
|
95
|
+
for proof in proofs {
|
|
96
|
+
if verification.contains_key(&proof.check_id) || !seen.insert(proof.check_id.clone()) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
findings.push(finding(
|
|
100
|
+
"task.proof.unknown_check_metadata",
|
|
101
|
+
"info",
|
|
102
|
+
format!(
|
|
103
|
+
"Proof references a check not present in .naome/verification.json: {}.",
|
|
104
|
+
proof.check_id
|
|
105
|
+
),
|
|
106
|
+
None,
|
|
107
|
+
"Keep the proof readable; add verification metadata if agents should recommend it.",
|
|
108
|
+
"Treat unknown check metadata as advisory, not as proof failure by itself.",
|
|
109
|
+
));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pub(super) fn recommended_commands(
|
|
114
|
+
proof: &TaskProofStatus,
|
|
115
|
+
verification: &BTreeMap<String, VerificationCheck>,
|
|
116
|
+
current_task_paths: &[String],
|
|
117
|
+
) -> Vec<TaskRecommendedCommand> {
|
|
118
|
+
let check_ids = proof
|
|
119
|
+
.missing_checks
|
|
120
|
+
.iter()
|
|
121
|
+
.chain(proof.stale_checks.iter())
|
|
122
|
+
.cloned()
|
|
123
|
+
.collect::<BTreeSet<_>>();
|
|
124
|
+
check_ids
|
|
125
|
+
.iter()
|
|
126
|
+
.filter_map(|check_id| {
|
|
127
|
+
let check = verification.get(check_id)?;
|
|
128
|
+
Some(TaskRecommendedCommand {
|
|
129
|
+
check_id: check_id.clone(),
|
|
130
|
+
command: check.command.clone(),
|
|
131
|
+
cwd: check.cwd.clone(),
|
|
132
|
+
reason: if proof.stale_checks.contains(check_id) {
|
|
133
|
+
"stale-proof".to_string()
|
|
134
|
+
} else {
|
|
135
|
+
"missing-proof".to_string()
|
|
136
|
+
},
|
|
137
|
+
selection_reason: "Required by active task proof state.".to_string(),
|
|
138
|
+
impacted_paths: current_task_paths.to_vec(),
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
.collect()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn evidence_covers(evidence_paths: &[String], current_paths: &[String]) -> bool {
|
|
145
|
+
current_paths
|
|
146
|
+
.iter()
|
|
147
|
+
.all(|path| evidence_paths.iter().any(|evidence| evidence == path))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn stale_files_for_check(
|
|
151
|
+
check_id: &str,
|
|
152
|
+
current_task_paths: &[String],
|
|
153
|
+
proofs: &[ProofRecord],
|
|
154
|
+
) -> Vec<String> {
|
|
155
|
+
let mut covered = BTreeSet::new();
|
|
156
|
+
for proof in proofs
|
|
157
|
+
.iter()
|
|
158
|
+
.filter(|proof| proof.check_id == check_id && proof.exit_code == 0)
|
|
159
|
+
{
|
|
160
|
+
covered.extend(proof.evidence_paths.iter().cloned());
|
|
161
|
+
}
|
|
162
|
+
current_task_paths
|
|
163
|
+
.iter()
|
|
164
|
+
.filter(|path| !covered.contains(*path))
|
|
165
|
+
.cloned()
|
|
166
|
+
.collect()
|
|
167
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
use std::collections::BTreeMap;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
use serde_json::Value;
|
|
6
|
+
|
|
7
|
+
use crate::models::NaomeError;
|
|
8
|
+
use crate::task_state::evidence::evidence_entry_path;
|
|
9
|
+
use crate::task_state::util::normalize_path;
|
|
10
|
+
|
|
11
|
+
use super::model::{finding, TaskStatusFinding};
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Clone)]
|
|
14
|
+
pub(super) struct ProofRecord {
|
|
15
|
+
pub(super) check_id: String,
|
|
16
|
+
pub(super) exit_code: i64,
|
|
17
|
+
pub(super) evidence_paths: Vec<String>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone)]
|
|
21
|
+
pub(super) struct VerificationCheck {
|
|
22
|
+
pub(super) command: String,
|
|
23
|
+
pub(super) cwd: String,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub(super) fn read_proofs(active_task: &Value) -> Vec<ProofRecord> {
|
|
27
|
+
let path_sets = proof_path_sets(active_task);
|
|
28
|
+
let mut proofs = Vec::new();
|
|
29
|
+
if let Some(legacy) = active_task.get("proofResults").and_then(Value::as_array) {
|
|
30
|
+
proofs.extend(legacy.iter().filter_map(read_legacy_proof));
|
|
31
|
+
}
|
|
32
|
+
if let Some(batches) = active_task.get("proofBatches").and_then(Value::as_array) {
|
|
33
|
+
for batch in batches {
|
|
34
|
+
let batch_evidence = batch_evidence(batch, &path_sets);
|
|
35
|
+
let Some(batch_proofs) = batch.get("proofs").and_then(Value::as_array) else {
|
|
36
|
+
continue;
|
|
37
|
+
};
|
|
38
|
+
for proof in batch_proofs {
|
|
39
|
+
let Some(check_id) = proof.get("checkId").and_then(Value::as_str) else {
|
|
40
|
+
continue;
|
|
41
|
+
};
|
|
42
|
+
let Some(exit_code) = proof.get("exitCode").and_then(Value::as_i64) else {
|
|
43
|
+
continue;
|
|
44
|
+
};
|
|
45
|
+
proofs.push(ProofRecord {
|
|
46
|
+
check_id: check_id.to_string(),
|
|
47
|
+
exit_code,
|
|
48
|
+
evidence_paths: compact_evidence_paths(proof, &path_sets)
|
|
49
|
+
.unwrap_or_else(|| batch_evidence.clone()),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
proofs
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub(super) fn read_verification_checks(
|
|
58
|
+
root: &Path,
|
|
59
|
+
findings: &mut Vec<TaskStatusFinding>,
|
|
60
|
+
) -> Result<BTreeMap<String, VerificationCheck>, NaomeError> {
|
|
61
|
+
let content = match fs::read_to_string(root.join(".naome/verification.json")) {
|
|
62
|
+
Ok(content) => content,
|
|
63
|
+
Err(_) => return Ok(BTreeMap::new()),
|
|
64
|
+
};
|
|
65
|
+
let verification: Value = match serde_json::from_str(&content) {
|
|
66
|
+
Ok(value) => value,
|
|
67
|
+
Err(error) => {
|
|
68
|
+
findings.push(finding(
|
|
69
|
+
"task.proof.verification_metadata_unreadable",
|
|
70
|
+
"warning",
|
|
71
|
+
format!(".naome/verification.json is not valid JSON: {error}."),
|
|
72
|
+
Some(".naome/verification.json".to_string()),
|
|
73
|
+
"Fix verification metadata so proof-plan can recommend commands.",
|
|
74
|
+
"Do not invent check commands when verification metadata is unreadable.",
|
|
75
|
+
));
|
|
76
|
+
return Ok(BTreeMap::new());
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
Ok(verification
|
|
80
|
+
.get("checks")
|
|
81
|
+
.and_then(Value::as_array)
|
|
82
|
+
.into_iter()
|
|
83
|
+
.flatten()
|
|
84
|
+
.filter_map(|check| {
|
|
85
|
+
Some((
|
|
86
|
+
check.get("id")?.as_str()?.to_string(),
|
|
87
|
+
VerificationCheck {
|
|
88
|
+
command: check.get("command")?.as_str()?.to_string(),
|
|
89
|
+
cwd: check.get("cwd")?.as_str()?.to_string(),
|
|
90
|
+
},
|
|
91
|
+
))
|
|
92
|
+
})
|
|
93
|
+
.collect())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn batch_evidence(batch: &Value, path_sets: &BTreeMap<String, Vec<String>>) -> Vec<String> {
|
|
97
|
+
batch
|
|
98
|
+
.get("evidencePathSet")
|
|
99
|
+
.and_then(Value::as_str)
|
|
100
|
+
.and_then(|name| path_sets.get(name))
|
|
101
|
+
.cloned()
|
|
102
|
+
.unwrap_or_default()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn compact_evidence_paths(
|
|
106
|
+
proof: &Value,
|
|
107
|
+
path_sets: &BTreeMap<String, Vec<String>>,
|
|
108
|
+
) -> Option<Vec<String>> {
|
|
109
|
+
proof
|
|
110
|
+
.get("evidence")
|
|
111
|
+
.and_then(Value::as_array)
|
|
112
|
+
.map(|entries| evidence_paths(entries))
|
|
113
|
+
.or_else(|| {
|
|
114
|
+
proof
|
|
115
|
+
.get("evidencePathSet")
|
|
116
|
+
.and_then(Value::as_str)
|
|
117
|
+
.and_then(|name| path_sets.get(name))
|
|
118
|
+
.cloned()
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn read_legacy_proof(proof: &Value) -> Option<ProofRecord> {
|
|
123
|
+
Some(ProofRecord {
|
|
124
|
+
check_id: proof.get("checkId")?.as_str()?.to_string(),
|
|
125
|
+
exit_code: proof.get("exitCode")?.as_i64()?,
|
|
126
|
+
evidence_paths: proof
|
|
127
|
+
.get("evidence")
|
|
128
|
+
.and_then(Value::as_array)
|
|
129
|
+
.map(|entries| evidence_paths(entries))
|
|
130
|
+
.unwrap_or_default(),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn proof_path_sets(active_task: &Value) -> BTreeMap<String, Vec<String>> {
|
|
135
|
+
active_task
|
|
136
|
+
.get("proofPathSets")
|
|
137
|
+
.and_then(Value::as_object)
|
|
138
|
+
.into_iter()
|
|
139
|
+
.flat_map(|sets| sets.iter())
|
|
140
|
+
.filter_map(|(name, value)| Some((name.clone(), evidence_paths(value.as_array()?))))
|
|
141
|
+
.collect()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn evidence_paths(entries: &[Value]) -> Vec<String> {
|
|
145
|
+
entries
|
|
146
|
+
.iter()
|
|
147
|
+
.filter_map(evidence_entry_path)
|
|
148
|
+
.map(normalize_path)
|
|
149
|
+
.collect()
|
|
150
|
+
}
|
|
@@ -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
|
+
}
|