@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,133 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
use std::process::Command;
|
|
3
|
+
|
|
4
|
+
use crate::models::NaomeError;
|
|
5
|
+
|
|
6
|
+
use super::model::{finding, TaskGitStatus, TaskStatusFinding};
|
|
7
|
+
use crate::task_state::git_io::{command_output, git_commit_exists, read_git_head};
|
|
8
|
+
|
|
9
|
+
pub(super) fn git_status(
|
|
10
|
+
root: &Path,
|
|
11
|
+
admission_head: Option<String>,
|
|
12
|
+
findings: &mut Vec<TaskStatusFinding>,
|
|
13
|
+
) -> Result<TaskGitStatus, NaomeError> {
|
|
14
|
+
let head = read_git_head(root)?;
|
|
15
|
+
let mut admission_head_reachable = false;
|
|
16
|
+
if let Some(admission_head) = admission_head.as_deref() {
|
|
17
|
+
if !git_commit_exists(root, admission_head)? {
|
|
18
|
+
findings.push(finding(
|
|
19
|
+
"task.git.admission_head_missing",
|
|
20
|
+
"error",
|
|
21
|
+
format!("Admission git head does not exist: {admission_head}."),
|
|
22
|
+
None,
|
|
23
|
+
"Recover the task-state from a reachable baseline or restart the task after sync.",
|
|
24
|
+
"Do not continue normal task work until the admission head is reachable.",
|
|
25
|
+
));
|
|
26
|
+
} else if !is_ancestor(root, admission_head, "HEAD")? {
|
|
27
|
+
findings.push(finding(
|
|
28
|
+
"task.git.admission_head_not_reachable",
|
|
29
|
+
"error",
|
|
30
|
+
"Admission git head is not an ancestor of the current HEAD.",
|
|
31
|
+
None,
|
|
32
|
+
"Rebase or recreate the task-state against the current branch before continuing.",
|
|
33
|
+
"Do not commit task work against a branch that lost its admission baseline.",
|
|
34
|
+
));
|
|
35
|
+
} else {
|
|
36
|
+
admission_head_reachable = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let (upstream, ahead, behind) = branch_divergence(root)?;
|
|
41
|
+
if upstream.is_some() && ahead > 0 && behind > 0 {
|
|
42
|
+
findings.push(finding(
|
|
43
|
+
"task.git.branch_diverged",
|
|
44
|
+
"warning",
|
|
45
|
+
format!("Current branch diverged from upstream ({ahead} ahead, {behind} behind)."),
|
|
46
|
+
None,
|
|
47
|
+
"Pull with rebase or coordinate the branch before recording final task proof.",
|
|
48
|
+
"Do not ignore branch divergence when interpreting task proof.",
|
|
49
|
+
));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let operation_in_progress = operation_in_progress(root)?;
|
|
53
|
+
if let Some(operation) = &operation_in_progress {
|
|
54
|
+
findings.push(finding(
|
|
55
|
+
"task.git.operation_in_progress",
|
|
56
|
+
"error",
|
|
57
|
+
format!("Git operation in progress: {operation}."),
|
|
58
|
+
None,
|
|
59
|
+
"Finish or abort the git operation, then rerun naome task status.",
|
|
60
|
+
"Do not commit or mutate task-state while a git operation is unresolved.",
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Ok(TaskGitStatus {
|
|
65
|
+
head,
|
|
66
|
+
admission_head,
|
|
67
|
+
admission_head_reachable,
|
|
68
|
+
upstream,
|
|
69
|
+
ahead,
|
|
70
|
+
behind,
|
|
71
|
+
operation_in_progress,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn is_ancestor(root: &Path, ancestor: &str, descendant: &str) -> Result<bool, NaomeError> {
|
|
76
|
+
Ok(Command::new("git")
|
|
77
|
+
.args(["merge-base", "--is-ancestor", ancestor, descendant])
|
|
78
|
+
.current_dir(root)
|
|
79
|
+
.status()?
|
|
80
|
+
.success())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn branch_divergence(root: &Path) -> Result<(Option<String>, usize, usize), NaomeError> {
|
|
84
|
+
let upstream = Command::new("git")
|
|
85
|
+
.args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
|
|
86
|
+
.current_dir(root)
|
|
87
|
+
.output()?;
|
|
88
|
+
if !upstream.status.success() {
|
|
89
|
+
return Ok((None, 0, 0));
|
|
90
|
+
}
|
|
91
|
+
let upstream_name = command_output(&upstream);
|
|
92
|
+
let counts = Command::new("git")
|
|
93
|
+
.args(["rev-list", "--left-right", "--count", "HEAD...@{u}"])
|
|
94
|
+
.current_dir(root)
|
|
95
|
+
.output()?;
|
|
96
|
+
if !counts.status.success() {
|
|
97
|
+
return Ok((Some(upstream_name), 0, 0));
|
|
98
|
+
}
|
|
99
|
+
let text = String::from_utf8_lossy(&counts.stdout);
|
|
100
|
+
let mut parts = text.split_whitespace();
|
|
101
|
+
let ahead = parts
|
|
102
|
+
.next()
|
|
103
|
+
.and_then(|value| value.parse::<usize>().ok())
|
|
104
|
+
.unwrap_or(0);
|
|
105
|
+
let behind = parts
|
|
106
|
+
.next()
|
|
107
|
+
.and_then(|value| value.parse::<usize>().ok())
|
|
108
|
+
.unwrap_or(0);
|
|
109
|
+
Ok((Some(upstream_name), ahead, behind))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn operation_in_progress(root: &Path) -> Result<Option<String>, NaomeError> {
|
|
113
|
+
for (name, marker) in [
|
|
114
|
+
("merge", "MERGE_HEAD"),
|
|
115
|
+
("cherry-pick", "CHERRY_PICK_HEAD"),
|
|
116
|
+
("revert", "REVERT_HEAD"),
|
|
117
|
+
("rebase", "rebase-merge"),
|
|
118
|
+
("rebase", "rebase-apply"),
|
|
119
|
+
] {
|
|
120
|
+
let output = Command::new("git")
|
|
121
|
+
.args(["rev-parse", "--git-path", marker])
|
|
122
|
+
.current_dir(root)
|
|
123
|
+
.output()?;
|
|
124
|
+
if !output.status.success() {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
let path = root.join(String::from_utf8_lossy(&output.stdout).trim());
|
|
128
|
+
if path.exists() {
|
|
129
|
+
return Ok(Some(name.to_string()));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
Ok(None)
|
|
133
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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 proof_reason: String,
|
|
131
|
+
pub selection_reason: String,
|
|
132
|
+
pub impacted_paths: Vec<String>,
|
|
133
|
+
pub safe_to_execute: bool,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pub(super) fn finding(
|
|
137
|
+
id: &str,
|
|
138
|
+
severity: &str,
|
|
139
|
+
message: impl Into<String>,
|
|
140
|
+
path: Option<String>,
|
|
141
|
+
suggested_fix: &str,
|
|
142
|
+
agent_instruction: &str,
|
|
143
|
+
) -> TaskStatusFinding {
|
|
144
|
+
TaskStatusFinding {
|
|
145
|
+
id: id.to_string(),
|
|
146
|
+
severity: severity.to_string(),
|
|
147
|
+
message: message.into(),
|
|
148
|
+
path,
|
|
149
|
+
suggested_fix: suggested_fix.to_string(),
|
|
150
|
+
agent_instruction: agent_instruction.to_string(),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use serde_json::Value;
|
|
5
|
+
|
|
6
|
+
use crate::models::NaomeError;
|
|
7
|
+
use crate::task_state::evidence_fingerprint::task_evidence_fingerprint;
|
|
8
|
+
use crate::task_state::util::string_array;
|
|
9
|
+
|
|
10
|
+
use super::model::{finding, TaskProofStatus, TaskRecommendedCommand, TaskStatusFinding};
|
|
11
|
+
use super::proof_read::{ProofRecord, VerificationCheck};
|
|
12
|
+
|
|
13
|
+
const AUTONOMOUS_SAFE_CHECKS: &[&str] = &[
|
|
14
|
+
"git diff --check",
|
|
15
|
+
"node .naome/bin/check-harness-health.js",
|
|
16
|
+
"node .naome/bin/check-task-state.js",
|
|
17
|
+
"node .naome/bin/naome.js quality check --changed",
|
|
18
|
+
"node .naome/bin/naome.js semantic check --changed",
|
|
19
|
+
"node .naome/bin/naome.js arch validate --changed-only",
|
|
20
|
+
"npm run check:task-state",
|
|
21
|
+
"npm run test:task-state",
|
|
22
|
+
"npm run test:decision-engine",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
pub(super) fn proof_status(
|
|
26
|
+
root: &Path,
|
|
27
|
+
active_task: Option<&Value>,
|
|
28
|
+
proofs: &[ProofRecord],
|
|
29
|
+
current_task_paths: &[String],
|
|
30
|
+
) -> Result<TaskProofStatus, NaomeError> {
|
|
31
|
+
let required_checks = active_task
|
|
32
|
+
.and_then(|task| string_array(task.get("requiredCheckIds")))
|
|
33
|
+
.unwrap_or_default();
|
|
34
|
+
let current_fingerprint = task_evidence_fingerprint(root, current_task_paths)?;
|
|
35
|
+
let mut passed_checks = Vec::new();
|
|
36
|
+
let mut missing_checks = Vec::new();
|
|
37
|
+
let mut stale_checks = Vec::new();
|
|
38
|
+
|
|
39
|
+
for check_id in &required_checks {
|
|
40
|
+
let successful = proofs
|
|
41
|
+
.iter()
|
|
42
|
+
.filter(|proof| proof.check_id == *check_id && proof.exit_code == 0)
|
|
43
|
+
.collect::<Vec<_>>();
|
|
44
|
+
if successful.is_empty() {
|
|
45
|
+
missing_checks.push(check_id.clone());
|
|
46
|
+
} else if successful
|
|
47
|
+
.iter()
|
|
48
|
+
.any(|proof| proof_is_fresh(proof, current_task_paths, ¤t_fingerprint))
|
|
49
|
+
{
|
|
50
|
+
passed_checks.push(check_id.clone());
|
|
51
|
+
} else {
|
|
52
|
+
stale_checks.push(check_id.clone());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Ok(TaskProofStatus {
|
|
57
|
+
required_checks,
|
|
58
|
+
passed_checks,
|
|
59
|
+
missing_checks,
|
|
60
|
+
stale_checks,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub(super) fn add_proof_findings(
|
|
65
|
+
proof: &TaskProofStatus,
|
|
66
|
+
current_task_paths: &[String],
|
|
67
|
+
proofs: &[ProofRecord],
|
|
68
|
+
findings: &mut Vec<TaskStatusFinding>,
|
|
69
|
+
) {
|
|
70
|
+
for check_id in &proof.missing_checks {
|
|
71
|
+
findings.push(finding(
|
|
72
|
+
"task.proof.missing_check",
|
|
73
|
+
"error",
|
|
74
|
+
format!("Required check has no passing proof: {check_id}."),
|
|
75
|
+
None,
|
|
76
|
+
"Run the required check and record passing proof before completing the task.",
|
|
77
|
+
"Do not mark a task complete while required checks are missing.",
|
|
78
|
+
));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for check_id in &proof.stale_checks {
|
|
82
|
+
let files = stale_files_for_check(check_id, current_task_paths, proofs);
|
|
83
|
+
if files.is_empty() {
|
|
84
|
+
findings.push(finding(
|
|
85
|
+
"task.scope.missing_evidence",
|
|
86
|
+
"warning",
|
|
87
|
+
format!("Proof evidence is stale for required check: {check_id}."),
|
|
88
|
+
None,
|
|
89
|
+
"Rerun the check and record evidence for the current task-owned diff.",
|
|
90
|
+
"Do not reuse stale proof for new changed files.",
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
for file in files {
|
|
94
|
+
findings.push(finding(
|
|
95
|
+
"task.scope.missing_evidence",
|
|
96
|
+
"warning",
|
|
97
|
+
format!("Proof evidence for {check_id} does not cover changed file: {file}."),
|
|
98
|
+
Some(file),
|
|
99
|
+
"Rerun the check and record evidence for the current task-owned diff.",
|
|
100
|
+
"Do not reuse stale proof for new changed files.",
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub(super) fn add_unknown_proof_findings(
|
|
107
|
+
proofs: &[ProofRecord],
|
|
108
|
+
verification: &BTreeMap<String, VerificationCheck>,
|
|
109
|
+
findings: &mut Vec<TaskStatusFinding>,
|
|
110
|
+
) {
|
|
111
|
+
let mut seen = BTreeSet::new();
|
|
112
|
+
for proof in proofs {
|
|
113
|
+
if verification.contains_key(&proof.check_id) || !seen.insert(proof.check_id.clone()) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
findings.push(finding(
|
|
117
|
+
"task.proof.unknown_check_metadata",
|
|
118
|
+
"info",
|
|
119
|
+
format!(
|
|
120
|
+
"Proof references a check not present in .naome/verification.json: {}.",
|
|
121
|
+
proof.check_id
|
|
122
|
+
),
|
|
123
|
+
None,
|
|
124
|
+
"Keep the proof readable; add verification metadata if agents should recommend it.",
|
|
125
|
+
"Treat unknown check metadata as advisory, not as proof failure by itself.",
|
|
126
|
+
));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pub(super) fn recommended_commands(
|
|
131
|
+
proof: &TaskProofStatus,
|
|
132
|
+
verification: &BTreeMap<String, VerificationCheck>,
|
|
133
|
+
current_task_paths: &[String],
|
|
134
|
+
) -> Vec<TaskRecommendedCommand> {
|
|
135
|
+
let check_ids = proof
|
|
136
|
+
.missing_checks
|
|
137
|
+
.iter()
|
|
138
|
+
.chain(proof.stale_checks.iter())
|
|
139
|
+
.cloned()
|
|
140
|
+
.collect::<BTreeSet<_>>();
|
|
141
|
+
check_ids
|
|
142
|
+
.iter()
|
|
143
|
+
.filter_map(|check_id| {
|
|
144
|
+
let check = verification.get(check_id)?;
|
|
145
|
+
let reason = if proof.stale_checks.contains(check_id) {
|
|
146
|
+
"stale-proof".to_string()
|
|
147
|
+
} else {
|
|
148
|
+
"missing-proof".to_string()
|
|
149
|
+
};
|
|
150
|
+
Some(TaskRecommendedCommand {
|
|
151
|
+
check_id: check_id.clone(),
|
|
152
|
+
command: check.command.clone(),
|
|
153
|
+
cwd: check.cwd.clone(),
|
|
154
|
+
reason: reason.clone(),
|
|
155
|
+
proof_reason: reason,
|
|
156
|
+
selection_reason: "Required by active task proof state.".to_string(),
|
|
157
|
+
impacted_paths: current_task_paths.to_vec(),
|
|
158
|
+
safe_to_execute: is_safe_autonomous_command(
|
|
159
|
+
&check.command,
|
|
160
|
+
&check.cwd,
|
|
161
|
+
current_task_paths,
|
|
162
|
+
),
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
.collect()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn is_safe_autonomous_command(command: &str, cwd: &str, current_task_paths: &[String]) -> bool {
|
|
169
|
+
if cwd != "." || !AUTONOMOUS_SAFE_CHECKS.contains(&command) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if command.starts_with("npm run ")
|
|
173
|
+
&& current_task_paths
|
|
174
|
+
.iter()
|
|
175
|
+
.any(|path| path == "package.json" || path == "packages/naome/package.json")
|
|
176
|
+
{
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn evidence_covers(evidence_paths: &[String], current_paths: &[String]) -> bool {
|
|
183
|
+
current_paths
|
|
184
|
+
.iter()
|
|
185
|
+
.all(|path| evidence_paths.iter().any(|evidence| evidence == path))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn proof_is_fresh(
|
|
189
|
+
proof: &ProofRecord,
|
|
190
|
+
current_paths: &[String],
|
|
191
|
+
current_fingerprint: &str,
|
|
192
|
+
) -> bool {
|
|
193
|
+
evidence_covers(&proof.evidence_paths, current_paths)
|
|
194
|
+
&& proof
|
|
195
|
+
.evidence_fingerprint
|
|
196
|
+
.as_deref()
|
|
197
|
+
.is_none_or(|fingerprint| fingerprint == current_fingerprint)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn stale_files_for_check(
|
|
201
|
+
check_id: &str,
|
|
202
|
+
current_task_paths: &[String],
|
|
203
|
+
proofs: &[ProofRecord],
|
|
204
|
+
) -> Vec<String> {
|
|
205
|
+
let mut covered = BTreeSet::new();
|
|
206
|
+
for proof in proofs
|
|
207
|
+
.iter()
|
|
208
|
+
.filter(|proof| proof.check_id == check_id && proof.exit_code == 0)
|
|
209
|
+
{
|
|
210
|
+
covered.extend(proof.evidence_paths.iter().cloned());
|
|
211
|
+
}
|
|
212
|
+
current_task_paths
|
|
213
|
+
.iter()
|
|
214
|
+
.filter(|path| !covered.contains(*path))
|
|
215
|
+
.cloned()
|
|
216
|
+
.collect()
|
|
217
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
pub(super) evidence_fingerprint: Option<String>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[derive(Debug, Clone)]
|
|
22
|
+
pub(super) struct VerificationCheck {
|
|
23
|
+
pub(super) command: String,
|
|
24
|
+
pub(super) cwd: String,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub(super) fn read_proofs(active_task: &Value) -> Vec<ProofRecord> {
|
|
28
|
+
let path_sets = proof_path_sets(active_task);
|
|
29
|
+
let mut proofs = Vec::new();
|
|
30
|
+
if let Some(legacy) = active_task.get("proofResults").and_then(Value::as_array) {
|
|
31
|
+
proofs.extend(legacy.iter().filter_map(read_legacy_proof));
|
|
32
|
+
}
|
|
33
|
+
if let Some(batches) = active_task.get("proofBatches").and_then(Value::as_array) {
|
|
34
|
+
for batch in batches {
|
|
35
|
+
let batch_evidence = batch_evidence(batch, &path_sets);
|
|
36
|
+
let batch_fingerprint = batch
|
|
37
|
+
.get("evidenceFingerprint")
|
|
38
|
+
.and_then(Value::as_str)
|
|
39
|
+
.map(ToString::to_string);
|
|
40
|
+
let Some(batch_proofs) = batch.get("proofs").and_then(Value::as_array) else {
|
|
41
|
+
continue;
|
|
42
|
+
};
|
|
43
|
+
for proof in batch_proofs {
|
|
44
|
+
let Some(check_id) = proof.get("checkId").and_then(Value::as_str) else {
|
|
45
|
+
continue;
|
|
46
|
+
};
|
|
47
|
+
let Some(exit_code) = proof.get("exitCode").and_then(Value::as_i64) else {
|
|
48
|
+
continue;
|
|
49
|
+
};
|
|
50
|
+
proofs.push(ProofRecord {
|
|
51
|
+
check_id: check_id.to_string(),
|
|
52
|
+
exit_code,
|
|
53
|
+
evidence_paths: compact_evidence_paths(proof, &path_sets)
|
|
54
|
+
.unwrap_or_else(|| batch_evidence.clone()),
|
|
55
|
+
evidence_fingerprint: proof
|
|
56
|
+
.get("evidenceFingerprint")
|
|
57
|
+
.and_then(Value::as_str)
|
|
58
|
+
.map(ToString::to_string)
|
|
59
|
+
.or_else(|| batch_fingerprint.clone()),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
proofs
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub(super) fn read_verification_checks(
|
|
68
|
+
root: &Path,
|
|
69
|
+
findings: &mut Vec<TaskStatusFinding>,
|
|
70
|
+
) -> Result<BTreeMap<String, VerificationCheck>, NaomeError> {
|
|
71
|
+
let content = match fs::read_to_string(root.join(".naome/verification.json")) {
|
|
72
|
+
Ok(content) => content,
|
|
73
|
+
Err(_) => return Ok(BTreeMap::new()),
|
|
74
|
+
};
|
|
75
|
+
let verification: Value = match serde_json::from_str(&content) {
|
|
76
|
+
Ok(value) => value,
|
|
77
|
+
Err(error) => {
|
|
78
|
+
findings.push(finding(
|
|
79
|
+
"task.proof.verification_metadata_unreadable",
|
|
80
|
+
"warning",
|
|
81
|
+
format!(".naome/verification.json is not valid JSON: {error}."),
|
|
82
|
+
Some(".naome/verification.json".to_string()),
|
|
83
|
+
"Fix verification metadata so proof-plan can recommend commands.",
|
|
84
|
+
"Do not invent check commands when verification metadata is unreadable.",
|
|
85
|
+
));
|
|
86
|
+
return Ok(BTreeMap::new());
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
Ok(verification
|
|
90
|
+
.get("checks")
|
|
91
|
+
.and_then(Value::as_array)
|
|
92
|
+
.into_iter()
|
|
93
|
+
.flatten()
|
|
94
|
+
.filter_map(|check| {
|
|
95
|
+
Some((
|
|
96
|
+
check.get("id")?.as_str()?.to_string(),
|
|
97
|
+
VerificationCheck {
|
|
98
|
+
command: check.get("command")?.as_str()?.to_string(),
|
|
99
|
+
cwd: check.get("cwd")?.as_str()?.to_string(),
|
|
100
|
+
},
|
|
101
|
+
))
|
|
102
|
+
})
|
|
103
|
+
.collect())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn batch_evidence(batch: &Value, path_sets: &BTreeMap<String, Vec<String>>) -> Vec<String> {
|
|
107
|
+
batch
|
|
108
|
+
.get("evidencePathSet")
|
|
109
|
+
.and_then(Value::as_str)
|
|
110
|
+
.and_then(|name| path_sets.get(name))
|
|
111
|
+
.cloned()
|
|
112
|
+
.unwrap_or_default()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn compact_evidence_paths(
|
|
116
|
+
proof: &Value,
|
|
117
|
+
path_sets: &BTreeMap<String, Vec<String>>,
|
|
118
|
+
) -> Option<Vec<String>> {
|
|
119
|
+
proof
|
|
120
|
+
.get("evidence")
|
|
121
|
+
.and_then(Value::as_array)
|
|
122
|
+
.map(|entries| evidence_paths(entries))
|
|
123
|
+
.or_else(|| {
|
|
124
|
+
proof
|
|
125
|
+
.get("evidencePathSet")
|
|
126
|
+
.and_then(Value::as_str)
|
|
127
|
+
.and_then(|name| path_sets.get(name))
|
|
128
|
+
.cloned()
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn read_legacy_proof(proof: &Value) -> Option<ProofRecord> {
|
|
133
|
+
Some(ProofRecord {
|
|
134
|
+
check_id: proof.get("checkId")?.as_str()?.to_string(),
|
|
135
|
+
exit_code: proof.get("exitCode")?.as_i64()?,
|
|
136
|
+
evidence_fingerprint: proof
|
|
137
|
+
.get("evidenceFingerprint")
|
|
138
|
+
.and_then(Value::as_str)
|
|
139
|
+
.map(ToString::to_string),
|
|
140
|
+
evidence_paths: proof
|
|
141
|
+
.get("evidence")
|
|
142
|
+
.and_then(Value::as_array)
|
|
143
|
+
.map(|entries| evidence_paths(entries))
|
|
144
|
+
.unwrap_or_default(),
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fn proof_path_sets(active_task: &Value) -> BTreeMap<String, Vec<String>> {
|
|
149
|
+
active_task
|
|
150
|
+
.get("proofPathSets")
|
|
151
|
+
.and_then(Value::as_object)
|
|
152
|
+
.into_iter()
|
|
153
|
+
.flat_map(|sets| sets.iter())
|
|
154
|
+
.filter_map(|(name, value)| Some((name.clone(), evidence_paths(value.as_array()?))))
|
|
155
|
+
.collect()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn evidence_paths(entries: &[Value]) -> Vec<String> {
|
|
159
|
+
entries
|
|
160
|
+
.iter()
|
|
161
|
+
.filter_map(evidence_entry_path)
|
|
162
|
+
.map(normalize_path)
|
|
163
|
+
.collect()
|
|
164
|
+
}
|