@lamentis/naome 1.4.1 → 1.4.3
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 +14 -5
- package/crates/naome-cli/src/task_commands/agent_snapshot.rs +173 -0
- package/crates/naome-cli/src/task_commands/can_edit.rs +64 -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 +163 -0
- package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
- package/crates/naome-cli/src/task_commands/check_run.rs +196 -0
- package/crates/naome-cli/src/task_commands/commit_preflight.rs +89 -0
- package/crates/naome-cli/src/task_commands/common.rs +39 -1
- package/crates/naome-cli/src/task_commands/compact_proof.rs +69 -0
- package/crates/naome-cli/src/task_commands/complete.rs +43 -0
- package/crates/naome-cli/src/task_commands/loop_control.rs +73 -0
- package/crates/naome-cli/src/task_commands/path_policy.rs +57 -0
- package/crates/naome-cli/src/task_commands/planner/checks.rs +166 -0
- package/crates/naome-cli/src/task_commands/planner/impact.rs +35 -0
- package/crates/naome-cli/src/task_commands/planner/mod.rs +24 -0
- package/crates/naome-cli/src/task_commands/preflight.rs +208 -0
- package/crates/naome-cli/src/task_commands/readiness.rs +14 -10
- package/crates/naome-cli/src/task_commands/record.rs +176 -37
- package/crates/naome-cli/src/task_commands/repair.rs +58 -11
- package/crates/naome-cli/src/task_commands/scope_suggestions.rs +109 -0
- package/crates/naome-cli/src/task_commands.rs +26 -3
- package/crates/naome-cli/tests/task_cli_agent_controls.rs +9 -16
- package/crates/naome-cli/tests/task_cli_fast_flow.rs +290 -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 +28 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/lib.rs +7 -7
- package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
- package/crates/naome-core/src/task_state/mod.rs +2 -0
- package/crates/naome-core/src/task_state/status/control/repair.rs +2 -2
- package/crates/naome-core/src/task_state/status/model.rs +2 -0
- package/crates/naome-core/src/task_state/status/proof.rs +59 -9
- package/crates/naome-core/src/task_state/status/proof_read.rs +14 -0
- package/crates/naome-core/src/task_state/status/report_context.rs +23 -1
- package/crates/naome-core/src/task_state/status/transition.rs +29 -1
- package/crates/naome-core/tests/task_status.rs +122 -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,144 @@
|
|
|
1
|
+
use std::process::Command;
|
|
2
|
+
|
|
3
|
+
mod task_cli_support;
|
|
4
|
+
|
|
5
|
+
use serde_json::json;
|
|
6
|
+
|
|
7
|
+
use task_cli_support::{
|
|
8
|
+
active_task, fixture_root, init_git, run_json, task_state, task_state_with_active_task,
|
|
9
|
+
write_fixture_file,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn can_edit_allows_in_scope_and_blocks_unsafe_paths() {
|
|
14
|
+
let root = fixture_root(task_state());
|
|
15
|
+
init_git(&root);
|
|
16
|
+
|
|
17
|
+
let allowed = run_json(&root, ["task", "can-edit", "--path", "README.md", "--json"]);
|
|
18
|
+
assert_eq!(allowed["schema"], "naome.task.can-edit.v1");
|
|
19
|
+
assert_eq!(allowed["path"], "README.md");
|
|
20
|
+
assert_eq!(allowed["allowed"], true);
|
|
21
|
+
|
|
22
|
+
let outside = run_json(
|
|
23
|
+
&root,
|
|
24
|
+
["task", "can-edit", "--path", "src/lib.rs", "--json"],
|
|
25
|
+
);
|
|
26
|
+
assert_eq!(outside["allowed"], false);
|
|
27
|
+
assert_eq!(outside["findings"][0]["id"], "task.edit.out_of_scope");
|
|
28
|
+
|
|
29
|
+
let traversal = run_json(
|
|
30
|
+
&root,
|
|
31
|
+
["task", "can-edit", "--path", "../README.md", "--json"],
|
|
32
|
+
);
|
|
33
|
+
assert_eq!(traversal["allowed"], false);
|
|
34
|
+
assert_eq!(traversal["findings"][0]["id"], "task.edit.unsafe_path");
|
|
35
|
+
|
|
36
|
+
let backslash_traversal = run_json(
|
|
37
|
+
&root,
|
|
38
|
+
["task", "can-edit", "--path", "..\\README.md", "--json"],
|
|
39
|
+
);
|
|
40
|
+
assert_eq!(backslash_traversal["allowed"], false);
|
|
41
|
+
assert_eq!(
|
|
42
|
+
backslash_traversal["findings"][0]["id"],
|
|
43
|
+
"task.edit.unsafe_path"
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
let absolute = run_json(
|
|
47
|
+
&root,
|
|
48
|
+
["task", "can-edit", "--path", "/tmp/file.rs", "--json"],
|
|
49
|
+
);
|
|
50
|
+
assert_eq!(absolute["allowed"], false);
|
|
51
|
+
assert_eq!(absolute["findings"][0]["id"], "task.edit.unsafe_path");
|
|
52
|
+
|
|
53
|
+
let control = run_json(
|
|
54
|
+
&root,
|
|
55
|
+
[
|
|
56
|
+
"task",
|
|
57
|
+
"can-edit",
|
|
58
|
+
"--path",
|
|
59
|
+
".naome/task-state.json",
|
|
60
|
+
"--json",
|
|
61
|
+
],
|
|
62
|
+
);
|
|
63
|
+
assert_eq!(control["allowed"], false);
|
|
64
|
+
assert_eq!(control["findings"][0]["id"], "task.edit.control_path");
|
|
65
|
+
|
|
66
|
+
let ignore_control = run_json(
|
|
67
|
+
&root,
|
|
68
|
+
["task", "can-edit", "--path", ".naomeignore", "--json"],
|
|
69
|
+
);
|
|
70
|
+
assert_eq!(ignore_control["allowed"], false);
|
|
71
|
+
assert_eq!(
|
|
72
|
+
ignore_control["findings"][0]["id"],
|
|
73
|
+
"task.edit.control_path"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn can_edit_honors_ignore_directories_wildcards_and_blocked_states() {
|
|
79
|
+
let root = fixture_root(task_state_with_active_task(active_task(json!({
|
|
80
|
+
"allowedPaths": ["**"]
|
|
81
|
+
}))));
|
|
82
|
+
std::fs::write(
|
|
83
|
+
root.join(".naomeignore"),
|
|
84
|
+
".naome/archive/\n.naome/tasks/\ndist/\n",
|
|
85
|
+
)
|
|
86
|
+
.unwrap();
|
|
87
|
+
init_git(&root);
|
|
88
|
+
write_fixture_file(&root, "dist/bundle.js", "generated\n");
|
|
89
|
+
|
|
90
|
+
let ignored = run_json(
|
|
91
|
+
&root,
|
|
92
|
+
["task", "can-edit", "--path", "dist/bundle.js", "--json"],
|
|
93
|
+
);
|
|
94
|
+
assert_eq!(ignored["allowed"], false);
|
|
95
|
+
assert_eq!(ignored["findings"][0]["id"], "task.edit.ignored_path");
|
|
96
|
+
|
|
97
|
+
let wildcard_root = fixture_root(task_state_with_active_task(active_task(json!({
|
|
98
|
+
"allowedPaths": ["scripts/*.js"]
|
|
99
|
+
}))));
|
|
100
|
+
init_git(&wildcard_root);
|
|
101
|
+
let wildcard = run_json(
|
|
102
|
+
&wildcard_root,
|
|
103
|
+
["task", "can-edit", "--path", "scripts/check.js", "--json"],
|
|
104
|
+
);
|
|
105
|
+
assert_eq!(wildcard["allowed"], true);
|
|
106
|
+
|
|
107
|
+
let blocked_root = fixture_root(task_state_with_active_task(active_task(json!({
|
|
108
|
+
"allowedPaths": ["README.md"]
|
|
109
|
+
}))));
|
|
110
|
+
let mut state: serde_json::Value = serde_json::from_str(
|
|
111
|
+
&std::fs::read_to_string(blocked_root.join(".naome/task-state.json")).unwrap(),
|
|
112
|
+
)
|
|
113
|
+
.unwrap();
|
|
114
|
+
state["status"] = json!("blocked");
|
|
115
|
+
state["activeTask"]["status"] = json!("blocked");
|
|
116
|
+
task_cli_support::write_json(&blocked_root, ".naome/task-state.json", &state);
|
|
117
|
+
init_git(&blocked_root);
|
|
118
|
+
let blocked = run_json(
|
|
119
|
+
&blocked_root,
|
|
120
|
+
["task", "can-edit", "--path", "README.md", "--json"],
|
|
121
|
+
);
|
|
122
|
+
assert_eq!(blocked["allowed"], false);
|
|
123
|
+
assert_eq!(blocked["findings"][0]["id"], "task.edit.no_active_task");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#[test]
|
|
127
|
+
fn agent_session_is_validated_and_reflected_in_json() {
|
|
128
|
+
let root = fixture_root(task_state());
|
|
129
|
+
init_git(&root);
|
|
130
|
+
|
|
131
|
+
let status = run_json(
|
|
132
|
+
&root,
|
|
133
|
+
["task", "status", "--json", "--agent-session", "agent-42"],
|
|
134
|
+
);
|
|
135
|
+
assert_eq!(status["agentSession"], "agent-42");
|
|
136
|
+
|
|
137
|
+
let rejected = Command::new(env!("CARGO_BIN_EXE_naome"))
|
|
138
|
+
.args(["task", "status", "--json", "--agent-session", "../bad"])
|
|
139
|
+
.current_dir(root)
|
|
140
|
+
.output()
|
|
141
|
+
.unwrap();
|
|
142
|
+
assert!(!rejected.status.success());
|
|
143
|
+
assert!(String::from_utf8_lossy(&rejected.stderr).contains("agent-session"));
|
|
144
|
+
}
|
|
@@ -131,6 +131,34 @@ pub fn write_json(root: &std::path::Path, path: &str, value: &Value) {
|
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
pub fn write_verification_checks(root: &std::path::Path, checks: Value) {
|
|
135
|
+
write_json(
|
|
136
|
+
root,
|
|
137
|
+
".naome/verification.json",
|
|
138
|
+
&json!({
|
|
139
|
+
"schema": "naome.verification.v1",
|
|
140
|
+
"version": 1,
|
|
141
|
+
"status": "ready",
|
|
142
|
+
"checks": checks
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pub fn run_json<const N: usize>(root: &std::path::Path, args: [&str; N]) -> Value {
|
|
148
|
+
let output = Command::new(env!("CARGO_BIN_EXE_naome"))
|
|
149
|
+
.args(args)
|
|
150
|
+
.current_dir(root)
|
|
151
|
+
.output()
|
|
152
|
+
.unwrap();
|
|
153
|
+
assert!(
|
|
154
|
+
output.status.success(),
|
|
155
|
+
"{}{}",
|
|
156
|
+
String::from_utf8_lossy(&output.stdout),
|
|
157
|
+
String::from_utf8_lossy(&output.stderr)
|
|
158
|
+
);
|
|
159
|
+
serde_json::from_slice(&output.stdout).unwrap()
|
|
160
|
+
}
|
|
161
|
+
|
|
134
162
|
fn verification() -> Value {
|
|
135
163
|
json!({
|
|
136
164
|
"schema": "naome.verification.v1",
|
|
@@ -68,13 +68,13 @@ pub use task_ledger::{
|
|
|
68
68
|
TaskLedgerProjection, TaskLedgerStatus,
|
|
69
69
|
};
|
|
70
70
|
pub use task_state::{
|
|
71
|
-
completed_task_commit_paths, format_task_proof_plan, format_task_status,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
TaskStateOptions, TaskStateReport, TaskStatusFinding,
|
|
77
|
-
TransitionReadinessReport,
|
|
71
|
+
completed_task_commit_paths, format_task_proof_plan, format_task_status,
|
|
72
|
+
task_evidence_fingerprint, task_proof_plan, task_status_exit_code, task_status_report,
|
|
73
|
+
task_transition_readiness, validate_task_state, AgentLoop, NextActionV2, PolicyHints,
|
|
74
|
+
ProofRecording, ProofRecordingAfterSuccess, RecoveryGuidance, RepairPlanItem, TaskFeedback,
|
|
75
|
+
TaskGitStatus, TaskModeStatus, TaskProofPlanReport, TaskProofStatus, TaskRecommendedCommand,
|
|
76
|
+
TaskScopeStatus, TaskStateMode, TaskStateOptions, TaskStateReport, TaskStatusFinding,
|
|
77
|
+
TaskStatusReportV1, TransitionReadinessReport,
|
|
78
78
|
};
|
|
79
79
|
pub use verification::seed_builtin_verification_checks;
|
|
80
80
|
pub use verification_contract::validate_verification_contract;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use crate::models::NaomeError;
|
|
5
|
+
|
|
6
|
+
pub fn task_evidence_fingerprint(root: &Path, paths: &[String]) -> Result<String, NaomeError> {
|
|
7
|
+
let mut paths = paths.to_vec();
|
|
8
|
+
paths.sort();
|
|
9
|
+
let mut hash = Fnv64::new();
|
|
10
|
+
for path in paths {
|
|
11
|
+
hash.update(path.as_bytes());
|
|
12
|
+
hash.update(b"\0");
|
|
13
|
+
match fs::read(root.join(&path)) {
|
|
14
|
+
Ok(content) => {
|
|
15
|
+
hash.update(b"file:");
|
|
16
|
+
hash.update(content.len().to_string().as_bytes());
|
|
17
|
+
hash.update(b":");
|
|
18
|
+
hash.update(&content);
|
|
19
|
+
}
|
|
20
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
21
|
+
hash.update(b"missing");
|
|
22
|
+
}
|
|
23
|
+
Err(error) => return Err(NaomeError::from(error)),
|
|
24
|
+
}
|
|
25
|
+
hash.update(b"\0");
|
|
26
|
+
}
|
|
27
|
+
Ok(format!("fnv64:{:016x}", hash.finish()))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
struct Fnv64(u64);
|
|
31
|
+
|
|
32
|
+
impl Fnv64 {
|
|
33
|
+
fn new() -> Self {
|
|
34
|
+
Self(0xcbf29ce484222325)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn update(&mut self, bytes: &[u8]) {
|
|
38
|
+
for byte in bytes {
|
|
39
|
+
self.0 ^= u64::from(*byte);
|
|
40
|
+
self.0 = self.0.wrapping_mul(0x100000001b3);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn finish(self) -> u64 {
|
|
45
|
+
self.0
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -8,6 +8,7 @@ mod completion;
|
|
|
8
8
|
mod deleted_paths;
|
|
9
9
|
mod diff;
|
|
10
10
|
mod evidence;
|
|
11
|
+
mod evidence_fingerprint;
|
|
11
12
|
mod git_io;
|
|
12
13
|
mod git_parse;
|
|
13
14
|
mod git_refs;
|
|
@@ -32,6 +33,7 @@ mod util;
|
|
|
32
33
|
|
|
33
34
|
pub use api::validate_task_state;
|
|
34
35
|
pub use completed_refresh::completed_task_harness_refresh_diff;
|
|
36
|
+
pub use evidence_fingerprint::task_evidence_fingerprint;
|
|
35
37
|
pub(crate) use proof_model::{canonical_proof_check_ids, canonical_proofs};
|
|
36
38
|
pub use status::{
|
|
37
39
|
task_proof_plan, task_status_exit_code, task_status_report, task_transition_readiness,
|
|
@@ -42,8 +42,8 @@ fn check_repairs(commands: &[TaskRecommendedCommand]) -> Vec<RepairPlanItem> {
|
|
|
42
42
|
check_ids: vec![command.check_id.clone()],
|
|
43
43
|
commands: vec![command.command.clone()],
|
|
44
44
|
cwd: Some(command.cwd.clone()),
|
|
45
|
-
safe_to_execute:
|
|
46
|
-
requires_user_approval:
|
|
45
|
+
safe_to_execute: command.safe_to_execute,
|
|
46
|
+
requires_user_approval: !command.safe_to_execute,
|
|
47
47
|
})
|
|
48
48
|
.collect()
|
|
49
49
|
}
|
|
@@ -127,8 +127,10 @@ pub struct TaskRecommendedCommand {
|
|
|
127
127
|
pub command: String,
|
|
128
128
|
pub cwd: String,
|
|
129
129
|
pub reason: String,
|
|
130
|
+
pub proof_reason: String,
|
|
130
131
|
pub selection_reason: String,
|
|
131
132
|
pub impacted_paths: Vec<String>,
|
|
133
|
+
pub safe_to_execute: bool,
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
pub(super) fn finding(
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::Path;
|
|
2
3
|
|
|
3
4
|
use serde_json::Value;
|
|
4
5
|
|
|
6
|
+
use crate::models::NaomeError;
|
|
7
|
+
use crate::task_state::evidence_fingerprint::task_evidence_fingerprint;
|
|
5
8
|
use crate::task_state::util::string_array;
|
|
6
9
|
|
|
7
10
|
use super::model::{finding, TaskProofStatus, TaskRecommendedCommand, TaskStatusFinding};
|
|
8
11
|
use super::proof_read::{ProofRecord, VerificationCheck};
|
|
9
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
|
+
|
|
10
25
|
pub(super) fn proof_status(
|
|
26
|
+
root: &Path,
|
|
11
27
|
active_task: Option<&Value>,
|
|
12
28
|
proofs: &[ProofRecord],
|
|
13
29
|
current_task_paths: &[String],
|
|
14
|
-
) -> TaskProofStatus {
|
|
30
|
+
) -> Result<TaskProofStatus, NaomeError> {
|
|
15
31
|
let required_checks = active_task
|
|
16
32
|
.and_then(|task| string_array(task.get("requiredCheckIds")))
|
|
17
33
|
.unwrap_or_default();
|
|
34
|
+
let current_fingerprint = task_evidence_fingerprint(root, current_task_paths)?;
|
|
18
35
|
let mut passed_checks = Vec::new();
|
|
19
36
|
let mut missing_checks = Vec::new();
|
|
20
37
|
let mut stale_checks = Vec::new();
|
|
@@ -28,7 +45,7 @@ pub(super) fn proof_status(
|
|
|
28
45
|
missing_checks.push(check_id.clone());
|
|
29
46
|
} else if successful
|
|
30
47
|
.iter()
|
|
31
|
-
.any(|proof|
|
|
48
|
+
.any(|proof| proof_is_fresh(proof, current_task_paths, ¤t_fingerprint))
|
|
32
49
|
{
|
|
33
50
|
passed_checks.push(check_id.clone());
|
|
34
51
|
} else {
|
|
@@ -36,12 +53,12 @@ pub(super) fn proof_status(
|
|
|
36
53
|
}
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
TaskProofStatus {
|
|
56
|
+
Ok(TaskProofStatus {
|
|
40
57
|
required_checks,
|
|
41
58
|
passed_checks,
|
|
42
59
|
missing_checks,
|
|
43
60
|
stale_checks,
|
|
44
|
-
}
|
|
61
|
+
})
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
pub(super) fn add_proof_findings(
|
|
@@ -125,28 +142,61 @@ pub(super) fn recommended_commands(
|
|
|
125
142
|
.iter()
|
|
126
143
|
.filter_map(|check_id| {
|
|
127
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
|
+
};
|
|
128
150
|
Some(TaskRecommendedCommand {
|
|
129
151
|
check_id: check_id.clone(),
|
|
130
152
|
command: check.command.clone(),
|
|
131
153
|
cwd: check.cwd.clone(),
|
|
132
|
-
reason:
|
|
133
|
-
|
|
134
|
-
} else {
|
|
135
|
-
"missing-proof".to_string()
|
|
136
|
-
},
|
|
154
|
+
reason: reason.clone(),
|
|
155
|
+
proof_reason: reason,
|
|
137
156
|
selection_reason: "Required by active task proof state.".to_string(),
|
|
138
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
|
+
),
|
|
139
163
|
})
|
|
140
164
|
})
|
|
141
165
|
.collect()
|
|
142
166
|
}
|
|
143
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
|
+
|
|
144
182
|
fn evidence_covers(evidence_paths: &[String], current_paths: &[String]) -> bool {
|
|
145
183
|
current_paths
|
|
146
184
|
.iter()
|
|
147
185
|
.all(|path| evidence_paths.iter().any(|evidence| evidence == path))
|
|
148
186
|
}
|
|
149
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
|
+
|
|
150
200
|
fn stale_files_for_check(
|
|
151
201
|
check_id: &str,
|
|
152
202
|
current_task_paths: &[String],
|
|
@@ -15,6 +15,7 @@ pub(super) struct ProofRecord {
|
|
|
15
15
|
pub(super) check_id: String,
|
|
16
16
|
pub(super) exit_code: i64,
|
|
17
17
|
pub(super) evidence_paths: Vec<String>,
|
|
18
|
+
pub(super) evidence_fingerprint: Option<String>,
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
#[derive(Debug, Clone)]
|
|
@@ -32,6 +33,10 @@ pub(super) fn read_proofs(active_task: &Value) -> Vec<ProofRecord> {
|
|
|
32
33
|
if let Some(batches) = active_task.get("proofBatches").and_then(Value::as_array) {
|
|
33
34
|
for batch in batches {
|
|
34
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);
|
|
35
40
|
let Some(batch_proofs) = batch.get("proofs").and_then(Value::as_array) else {
|
|
36
41
|
continue;
|
|
37
42
|
};
|
|
@@ -47,6 +52,11 @@ pub(super) fn read_proofs(active_task: &Value) -> Vec<ProofRecord> {
|
|
|
47
52
|
exit_code,
|
|
48
53
|
evidence_paths: compact_evidence_paths(proof, &path_sets)
|
|
49
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()),
|
|
50
60
|
});
|
|
51
61
|
}
|
|
52
62
|
}
|
|
@@ -123,6 +133,10 @@ fn read_legacy_proof(proof: &Value) -> Option<ProofRecord> {
|
|
|
123
133
|
Some(ProofRecord {
|
|
124
134
|
check_id: proof.get("checkId")?.as_str()?.to_string(),
|
|
125
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),
|
|
126
140
|
evidence_paths: proof
|
|
127
141
|
.get("evidence")
|
|
128
142
|
.and_then(Value::as_array)
|
|
@@ -27,6 +27,8 @@ pub(super) struct TaskStatusContext {
|
|
|
27
27
|
pub(super) proof: TaskProofStatus,
|
|
28
28
|
pub(super) recommended_commands: Vec<TaskRecommendedCommand>,
|
|
29
29
|
pub(super) findings: Vec<TaskStatusFinding>,
|
|
30
|
+
pub(super) human_review_pending: bool,
|
|
31
|
+
pub(super) blocker_present: bool,
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
impl TaskStatusContext {
|
|
@@ -57,7 +59,7 @@ impl TaskStatusContext {
|
|
|
57
59
|
let verification = read_verification_checks(root, &mut findings)?;
|
|
58
60
|
let proofs = active_task.map(read_proofs).unwrap_or_default();
|
|
59
61
|
add_unknown_proof_findings(&proofs, &verification, &mut findings);
|
|
60
|
-
let proof = proof_status(active_task, &proofs, &scope.in_scope_changed_paths)
|
|
62
|
+
let proof = proof_status(root, active_task, &proofs, &scope.in_scope_changed_paths)?;
|
|
61
63
|
add_proof_findings(
|
|
62
64
|
&proof,
|
|
63
65
|
&scope.in_scope_changed_paths,
|
|
@@ -77,6 +79,10 @@ impl TaskStatusContext {
|
|
|
77
79
|
proof,
|
|
78
80
|
recommended_commands,
|
|
79
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()),
|
|
80
86
|
})
|
|
81
87
|
}
|
|
82
88
|
}
|
|
@@ -105,6 +111,22 @@ fn add_active_task_shape_finding(
|
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
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
|
+
|
|
108
130
|
fn task_mode(active_task: Option<&Value>) -> TaskModeStatus {
|
|
109
131
|
let kind = task_text(active_task, "kind").unwrap_or_else(|| "standard".to_string());
|
|
110
132
|
let declared_review_fix = active_task
|
|
@@ -12,7 +12,13 @@ pub(super) fn transition_report(
|
|
|
12
12
|
&context.findings,
|
|
13
13
|
&context.scope,
|
|
14
14
|
);
|
|
15
|
-
let blocking_findings = transition_blockers(
|
|
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
|
+
);
|
|
16
22
|
let required_before_transition = blocking_findings
|
|
17
23
|
.iter()
|
|
18
24
|
.map(|finding| finding.suggested_fix.clone())
|
|
@@ -31,6 +37,8 @@ fn transition_blockers(
|
|
|
31
37
|
state: &str,
|
|
32
38
|
findings: &[TaskStatusFinding],
|
|
33
39
|
proof: &TaskProofStatus,
|
|
40
|
+
human_review_pending: bool,
|
|
41
|
+
blocker_present: bool,
|
|
34
42
|
) -> Vec<TaskStatusFinding> {
|
|
35
43
|
let mut blockers = findings
|
|
36
44
|
.iter()
|
|
@@ -59,6 +67,26 @@ fn transition_blockers(
|
|
|
59
67
|
"Do not complete a blocked task state.",
|
|
60
68
|
));
|
|
61
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
|
+
}
|
|
62
90
|
if !proof.stale_checks.is_empty() {
|
|
63
91
|
blockers.push(finding(
|
|
64
92
|
"task.transition.stale_proof",
|