@lamentis/naome 1.1.2 → 1.2.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/Cargo.toml +1 -1
- package/LICENSE +180 -21
- package/README.md +49 -6
- package/bin/naome-node.js +2 -1579
- package/bin/naome.js +68 -16
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/check_commands.rs +135 -0
- package/crates/naome-cli/src/cli_args.rs +5 -0
- package/crates/naome-cli/src/dispatcher.rs +37 -0
- package/crates/naome-cli/src/install_bridge.rs +83 -0
- package/crates/naome-cli/src/main.rs +60 -341
- package/crates/naome-cli/src/prompt_commands.rs +68 -0
- package/crates/naome-cli/src/quality_commands.rs +229 -0
- package/crates/naome-cli/src/simple_commands.rs +53 -0
- package/crates/naome-cli/src/workflow_commands.rs +153 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/decision/checks.rs +64 -0
- package/crates/naome-core/src/decision/idle.rs +67 -0
- package/crates/naome-core/src/decision/json.rs +36 -0
- package/crates/naome-core/src/decision/states.rs +165 -0
- package/crates/naome-core/src/decision.rs +131 -353
- package/crates/naome-core/src/harness_health/integrity.rs +96 -0
- package/crates/naome-core/src/harness_health.rs +14 -126
- package/crates/naome-core/src/install_plan.rs +5 -0
- package/crates/naome-core/src/intent/classifier.rs +171 -0
- package/crates/naome-core/src/intent/envelope.rs +108 -0
- package/crates/naome-core/src/intent/legacy.rs +138 -0
- package/crates/naome-core/src/intent/legacy_response.rs +76 -0
- package/crates/naome-core/src/intent/model.rs +71 -0
- package/crates/naome-core/src/intent/patterns.rs +170 -0
- package/crates/naome-core/src/intent/resolver.rs +162 -0
- package/crates/naome-core/src/intent/resolver_active.rs +17 -0
- package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
- package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
- package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
- package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
- package/crates/naome-core/src/intent/risk.rs +40 -0
- package/crates/naome-core/src/intent/segment.rs +170 -0
- package/crates/naome-core/src/intent.rs +64 -879
- package/crates/naome-core/src/journal.rs +9 -20
- package/crates/naome-core/src/lib.rs +15 -0
- package/crates/naome-core/src/paths.rs +3 -1
- package/crates/naome-core/src/quality/adapter_support.rs +89 -0
- package/crates/naome-core/src/quality/adapters.rs +131 -0
- package/crates/naome-core/src/quality/baseline.rs +75 -0
- package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
- package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
- package/crates/naome-core/src/quality/checks.rs +228 -0
- package/crates/naome-core/src/quality/cleanup.rs +84 -0
- package/crates/naome-core/src/quality/config.rs +102 -0
- package/crates/naome-core/src/quality/config_support.rs +24 -0
- package/crates/naome-core/src/quality/mod.rs +108 -0
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
- package/crates/naome-core/src/quality/scanner.rs +379 -0
- package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
- package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
- package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
- package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
- package/crates/naome-core/src/quality/structure/checks.rs +124 -0
- package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
- package/crates/naome-core/src/quality/structure/classify.rs +94 -0
- package/crates/naome-core/src/quality/structure/config.rs +89 -0
- package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
- package/crates/naome-core/src/quality/structure/mod.rs +77 -0
- package/crates/naome-core/src/quality/structure/model.rs +124 -0
- package/crates/naome-core/src/quality/types.rs +292 -0
- package/crates/naome-core/src/route/builtin_checks.rs +155 -0
- package/crates/naome-core/src/route/builtin_context.rs +73 -0
- package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
- package/crates/naome-core/src/route/builtin_require.rs +40 -0
- package/crates/naome-core/src/route/context.rs +180 -0
- package/crates/naome-core/src/route/execution.rs +96 -0
- package/crates/naome-core/src/route/execution_baselines.rs +146 -0
- package/crates/naome-core/src/route/execution_support.rs +57 -0
- package/crates/naome-core/src/route/execution_tasks.rs +71 -0
- package/crates/naome-core/src/route/git_ops.rs +72 -0
- package/crates/naome-core/src/route/quality_gate.rs +73 -0
- package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
- package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
- package/crates/naome-core/src/route/worktree.rs +75 -0
- package/crates/naome-core/src/route/worktree_files.rs +32 -0
- package/crates/naome-core/src/route/worktree_plan.rs +131 -0
- package/crates/naome-core/src/route.rs +44 -1155
- package/crates/naome-core/src/task_state/admission.rs +63 -0
- package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
- package/crates/naome-core/src/task_state/api.rs +130 -0
- package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
- package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
- package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
- package/crates/naome-core/src/task_state/completion.rs +72 -0
- package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
- package/crates/naome-core/src/task_state/diff.rs +95 -0
- package/crates/naome-core/src/task_state/evidence.rs +154 -0
- package/crates/naome-core/src/task_state/git_io.rs +86 -0
- package/crates/naome-core/src/task_state/git_parse.rs +86 -0
- package/crates/naome-core/src/task_state/git_refs.rs +37 -0
- package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
- package/crates/naome-core/src/task_state/mod.rs +38 -0
- package/crates/naome-core/src/task_state/process_guard.rs +40 -0
- package/crates/naome-core/src/task_state/progress.rs +123 -0
- package/crates/naome-core/src/task_state/proof.rs +139 -0
- package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
- package/crates/naome-core/src/task_state/proof_model.rs +70 -0
- package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
- package/crates/naome-core/src/task_state/push_gate.rs +49 -0
- package/crates/naome-core/src/task_state/reconcile.rs +7 -0
- package/crates/naome-core/src/task_state/repair.rs +168 -0
- package/crates/naome-core/src/task_state/shape.rs +117 -0
- package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
- package/crates/naome-core/src/task_state/task_records.rs +131 -0
- package/crates/naome-core/src/task_state/task_references.rs +126 -0
- package/crates/naome-core/src/task_state/types.rs +87 -0
- package/crates/naome-core/src/task_state/util.rs +137 -0
- package/crates/naome-core/src/verification/render.rs +122 -0
- package/crates/naome-core/src/verification.rs +177 -58
- package/crates/naome-core/src/verification_contract.rs +49 -21
- package/crates/naome-core/src/workflow/integrity.rs +123 -0
- package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
- package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
- package/crates/naome-core/src/workflow/mod.rs +18 -0
- package/crates/naome-core/src/workflow/mutation.rs +68 -0
- package/crates/naome-core/src/workflow/output.rs +111 -0
- package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
- package/crates/naome-core/src/workflow/phases.rs +169 -0
- package/crates/naome-core/src/workflow/policy.rs +156 -0
- package/crates/naome-core/src/workflow/processes.rs +91 -0
- package/crates/naome-core/src/workflow/types.rs +42 -0
- package/crates/naome-core/tests/decision.rs +24 -118
- package/crates/naome-core/tests/harness_health.rs +5 -0
- package/crates/naome-core/tests/intent.rs +97 -792
- package/crates/naome-core/tests/intent_support/mod.rs +133 -0
- package/crates/naome-core/tests/intent_v2.rs +90 -0
- package/crates/naome-core/tests/quality.rs +319 -0
- package/crates/naome-core/tests/quality_structure.rs +116 -0
- package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
- package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
- package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
- package/crates/naome-core/tests/repo_support/mod.rs +16 -0
- package/crates/naome-core/tests/repo_support/repo.rs +113 -0
- package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
- package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
- package/crates/naome-core/tests/repo_support/routes.rs +81 -0
- package/crates/naome-core/tests/repo_support/verification.rs +168 -0
- package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
- package/crates/naome-core/tests/route.rs +1 -1476
- package/crates/naome-core/tests/route_baseline.rs +86 -0
- package/crates/naome-core/tests/route_completion.rs +141 -0
- package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
- package/crates/naome-core/tests/route_user_diff.rs +198 -0
- package/crates/naome-core/tests/route_worktree.rs +54 -0
- package/crates/naome-core/tests/task_state.rs +60 -429
- package/crates/naome-core/tests/task_state_compact.rs +110 -0
- package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
- package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
- package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
- package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
- package/crates/naome-core/tests/task_state_support/states.rs +84 -0
- package/crates/naome-core/tests/verification.rs +4 -45
- package/crates/naome-core/tests/verification_contract.rs +22 -78
- package/crates/naome-core/tests/workflow_integrity.rs +85 -0
- package/crates/naome-core/tests/workflow_policy.rs +139 -0
- package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
- package/installer/agents.js +90 -0
- package/installer/context.js +67 -0
- package/installer/filesystem.js +166 -0
- package/installer/flows.js +84 -0
- package/installer/git-boundary.js +170 -0
- package/installer/git-hook-content.js +36 -0
- package/installer/git-hooks.js +134 -0
- package/installer/git-local.js +2 -0
- package/installer/git-shared.js +35 -0
- package/installer/harness-file-ops.js +140 -0
- package/installer/harness-files.js +56 -0
- package/installer/harness-verification.js +123 -0
- package/installer/install-plan.js +66 -0
- package/installer/main.js +25 -0
- package/installer/manifest-state.js +167 -0
- package/installer/native-build.js +24 -0
- package/installer/native-format.js +6 -0
- package/installer/native.js +162 -0
- package/installer/output.js +131 -0
- package/installer/version.js +32 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +3 -2
- package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
- package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
- package/templates/naome-root/.naome/bin/naome.js +51 -76
- package/templates/naome-root/.naome/manifest.json +22 -18
- package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
- package/templates/naome-root/.naome/repository-quality.json +24 -0
- package/templates/naome-root/.naome/repository-structure.json +90 -0
- package/templates/naome-root/.naome/task-contract.schema.json +93 -11
- package/templates/naome-root/.naome/upgrade-state.json +1 -1
- package/templates/naome-root/.naome/verification.json +38 -0
- package/templates/naome-root/AGENTS.md +3 -0
- package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
- package/templates/naome-root/docs/naome/execution.md +25 -21
- package/templates/naome-root/docs/naome/index.md +5 -3
- package/templates/naome-root/docs/naome/repository-quality.md +46 -0
- package/templates/naome-root/docs/naome/repository-structure.md +51 -0
- package/templates/naome-root/docs/naome/testing.md +13 -0
- package/crates/naome-core/src/task_state.rs +0 -2210
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use crate::models::NaomeError;
|
|
6
|
+
|
|
7
|
+
use super::completion::validate_complete_task;
|
|
8
|
+
use super::human_review_state::validate_human_review_state;
|
|
9
|
+
use super::progress::{checked_status, validate_clean_git_diff};
|
|
10
|
+
use super::shape::{
|
|
11
|
+
format_blocker, validate_active_task, validate_active_task_references, validate_blocker,
|
|
12
|
+
validate_idle_state,
|
|
13
|
+
};
|
|
14
|
+
pub(super) fn validate_admission(
|
|
15
|
+
task_state: &Value,
|
|
16
|
+
root: &Path,
|
|
17
|
+
errors: &mut Vec<String>,
|
|
18
|
+
) -> Result<(), NaomeError> {
|
|
19
|
+
let status = checked_status(task_state, root, errors)?;
|
|
20
|
+
match status {
|
|
21
|
+
"idle" => validate_idle_state(task_state, errors),
|
|
22
|
+
"complete" => {
|
|
23
|
+
validate_active_task(task_state.get("activeTask"), errors);
|
|
24
|
+
validate_active_task_references(
|
|
25
|
+
task_state.get("activeTask"),
|
|
26
|
+
root,
|
|
27
|
+
errors,
|
|
28
|
+
Some(status),
|
|
29
|
+
)?;
|
|
30
|
+
validate_complete_task(
|
|
31
|
+
task_state.get("activeTask"),
|
|
32
|
+
task_state.get("blocker"),
|
|
33
|
+
root,
|
|
34
|
+
errors,
|
|
35
|
+
&mut Vec::new(),
|
|
36
|
+
)?;
|
|
37
|
+
}
|
|
38
|
+
"needs_human_review" => {
|
|
39
|
+
let start = errors.len();
|
|
40
|
+
validate_human_review_state(task_state, root, errors)?;
|
|
41
|
+
if errors.len() > start {
|
|
42
|
+
errors.push("Task admission is blocked because needs_human_review state is invalid; fix blocker paths and proof evidence before asking for a human decision.".to_string());
|
|
43
|
+
} else {
|
|
44
|
+
errors.push(format_blocker(
|
|
45
|
+
"Task admission is blocked",
|
|
46
|
+
task_state.get("blocker"),
|
|
47
|
+
));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
"blocked" => {
|
|
51
|
+
validate_blocker(task_state.get("blocker"), errors);
|
|
52
|
+
errors.push(format_blocker(
|
|
53
|
+
"Task admission is blocked",
|
|
54
|
+
task_state.get("blocker"),
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
other => errors.push(format!(
|
|
58
|
+
"Task admission is blocked because task state is {other}."
|
|
59
|
+
)),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
validate_clean_git_diff(task_state, root, errors)
|
|
63
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use crate::models::NaomeError;
|
|
6
|
+
|
|
7
|
+
use super::git_io::git_commit_exists;
|
|
8
|
+
use super::util::{is_iso_datetime, require_string, require_string_array_allow_empty};
|
|
9
|
+
|
|
10
|
+
pub(super) fn validate_admission_proof(
|
|
11
|
+
admission: Option<&Value>,
|
|
12
|
+
root: &Path,
|
|
13
|
+
errors: &mut Vec<String>,
|
|
14
|
+
) -> Result<(), NaomeError> {
|
|
15
|
+
let Some(object) = admission.and_then(Value::as_object) else {
|
|
16
|
+
errors.push(
|
|
17
|
+
"activeTask.admission must be an object recorded from a passed admission check."
|
|
18
|
+
.to_string(),
|
|
19
|
+
);
|
|
20
|
+
return Ok(());
|
|
21
|
+
};
|
|
22
|
+
let prefix = "activeTask.admission";
|
|
23
|
+
|
|
24
|
+
require_string(object.get("command"), &format!("{prefix}.command"), errors);
|
|
25
|
+
require_string(object.get("cwd"), &format!("{prefix}.cwd"), errors);
|
|
26
|
+
require_string_array_allow_empty(
|
|
27
|
+
object.get("changedPaths"),
|
|
28
|
+
&format!("{prefix}.changedPaths"),
|
|
29
|
+
errors,
|
|
30
|
+
);
|
|
31
|
+
require_string(object.get("gitHead"), &format!("{prefix}.gitHead"), errors);
|
|
32
|
+
|
|
33
|
+
if object.get("command").and_then(Value::as_str)
|
|
34
|
+
!= Some("node .naome/bin/check-task-state.js --admission")
|
|
35
|
+
{
|
|
36
|
+
errors.push(format!(
|
|
37
|
+
"{prefix}.command must be node .naome/bin/check-task-state.js --admission."
|
|
38
|
+
));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if object.get("cwd").and_then(Value::as_str) != Some(".") {
|
|
42
|
+
errors.push(format!("{prefix}.cwd must be \".\"."));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
match object.get("exitCode").and_then(Value::as_i64) {
|
|
46
|
+
Some(0) => {}
|
|
47
|
+
Some(_) => errors.push(format!("{prefix}.exitCode must be 0.")),
|
|
48
|
+
None => errors.push(format!("{prefix}.exitCode must be an integer.")),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if !object
|
|
52
|
+
.get("checkedAt")
|
|
53
|
+
.and_then(Value::as_str)
|
|
54
|
+
.is_some_and(is_iso_datetime)
|
|
55
|
+
{
|
|
56
|
+
errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if let Some(changed_paths) = object.get("changedPaths").and_then(Value::as_array) {
|
|
60
|
+
if !changed_paths.is_empty() {
|
|
61
|
+
errors.push(format!("{prefix}.changedPaths must be empty because task admission requires a clean git diff."));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if let Some(git_head) = object.get("gitHead").and_then(Value::as_str) {
|
|
66
|
+
if !git_head.trim().is_empty() && !git_commit_exists(root, git_head)? {
|
|
67
|
+
errors.push(format!("{prefix}.gitHead must be an existing git commit."));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Ok(())
|
|
72
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use crate::models::NaomeError;
|
|
6
|
+
|
|
7
|
+
use super::completion::{
|
|
8
|
+
validate_admission, validate_commit_gate, validate_complete_task, validate_progress,
|
|
9
|
+
};
|
|
10
|
+
use super::diff::validate_human_review_blocker_paths;
|
|
11
|
+
use super::proof::validate_proof_evidence_covers_changed_paths;
|
|
12
|
+
use super::reconcile::validate_push_gate;
|
|
13
|
+
use super::shape::{
|
|
14
|
+
format_blocker, validate_active_task, validate_active_task_references, validate_blocker,
|
|
15
|
+
validate_idle_state, validate_pending_upgrade, validate_task_state_shape,
|
|
16
|
+
};
|
|
17
|
+
use super::task_diff_api::validate_harness_health_gate;
|
|
18
|
+
use super::types::*;
|
|
19
|
+
use super::util::read_json;
|
|
20
|
+
pub fn validate_task_state(
|
|
21
|
+
root: &Path,
|
|
22
|
+
options: TaskStateOptions,
|
|
23
|
+
) -> Result<TaskStateReport, NaomeError> {
|
|
24
|
+
let mut report = TaskStateReport {
|
|
25
|
+
errors: Vec::new(),
|
|
26
|
+
notices: Vec::new(),
|
|
27
|
+
};
|
|
28
|
+
let Some(task_state) = read_json(root, ".naome/task-state.json", &mut report.errors)? else {
|
|
29
|
+
return Ok(report);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
validate_task_state_shape(&task_state, &mut report.errors);
|
|
33
|
+
let status = task_state
|
|
34
|
+
.get("status")
|
|
35
|
+
.and_then(Value::as_str)
|
|
36
|
+
.unwrap_or("invalid");
|
|
37
|
+
if !ALLOWED_STATUS.contains(&status) {
|
|
38
|
+
return Ok(report);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
validate_harness_health_gate(root, &options, &mut report.errors)?;
|
|
42
|
+
if !report.errors.is_empty() {
|
|
43
|
+
return Ok(report);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if validate_requested_mode(&task_state, root, options.mode, &mut report)? {
|
|
47
|
+
return Ok(report);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if status == "idle" {
|
|
51
|
+
validate_idle_state(&task_state, &mut report.errors);
|
|
52
|
+
return Ok(report);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let active_error_start = report.errors.len();
|
|
56
|
+
validate_active_task(task_state.get("activeTask"), &mut report.errors);
|
|
57
|
+
validate_pending_upgrade(&task_state, root, &mut report.errors)?;
|
|
58
|
+
validate_active_task_references(
|
|
59
|
+
task_state.get("activeTask"),
|
|
60
|
+
root,
|
|
61
|
+
&mut report.errors,
|
|
62
|
+
Some(status),
|
|
63
|
+
)?;
|
|
64
|
+
|
|
65
|
+
if status == "needs_human_review" {
|
|
66
|
+
validate_blocker(task_state.get("blocker"), &mut report.errors);
|
|
67
|
+
validate_human_review_blocker_paths(
|
|
68
|
+
task_state.get("activeTask"),
|
|
69
|
+
task_state.get("blocker"),
|
|
70
|
+
root,
|
|
71
|
+
&mut report.errors,
|
|
72
|
+
)?;
|
|
73
|
+
validate_proof_evidence_covers_changed_paths(
|
|
74
|
+
task_state.get("activeTask"),
|
|
75
|
+
root,
|
|
76
|
+
&mut report.errors,
|
|
77
|
+
)?;
|
|
78
|
+
if report.errors.len() > active_error_start {
|
|
79
|
+
report.errors.push("needs_human_review task state is invalid; fix blocker paths and proof evidence before asking for a human decision.".to_string());
|
|
80
|
+
} else {
|
|
81
|
+
report.errors.push(format_blocker(
|
|
82
|
+
"Human review required",
|
|
83
|
+
task_state.get("blocker"),
|
|
84
|
+
));
|
|
85
|
+
}
|
|
86
|
+
return Ok(report);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if status == "blocked" {
|
|
90
|
+
validate_blocker(task_state.get("blocker"), &mut report.errors);
|
|
91
|
+
report
|
|
92
|
+
.errors
|
|
93
|
+
.push(format_blocker("Task is blocked", task_state.get("blocker")));
|
|
94
|
+
return Ok(report);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if BLOCKING_STATUS.contains(&status) {
|
|
98
|
+
report.errors.push(format!(
|
|
99
|
+
"Task is still {status}; new work must wait until the active task is complete or resolved."
|
|
100
|
+
));
|
|
101
|
+
return Ok(report);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
validate_complete_task(
|
|
105
|
+
task_state.get("activeTask"),
|
|
106
|
+
task_state.get("blocker"),
|
|
107
|
+
root,
|
|
108
|
+
&mut report.errors,
|
|
109
|
+
&mut report.notices,
|
|
110
|
+
)?;
|
|
111
|
+
Ok(report)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn validate_requested_mode(
|
|
115
|
+
task_state: &Value,
|
|
116
|
+
root: &Path,
|
|
117
|
+
mode: TaskStateMode,
|
|
118
|
+
report: &mut TaskStateReport,
|
|
119
|
+
) -> Result<bool, NaomeError> {
|
|
120
|
+
match mode {
|
|
121
|
+
TaskStateMode::Admission => validate_admission(task_state, root, &mut report.errors)?,
|
|
122
|
+
TaskStateMode::Progress => validate_progress(task_state, root, &mut report.errors)?,
|
|
123
|
+
TaskStateMode::CommitGate => {
|
|
124
|
+
validate_commit_gate(task_state, root, &mut report.errors, &mut report.notices)?;
|
|
125
|
+
}
|
|
126
|
+
TaskStateMode::PushGate => validate_push_gate(task_state, &mut report.errors),
|
|
127
|
+
TaskStateMode::State => return Ok(false),
|
|
128
|
+
}
|
|
129
|
+
Ok(true)
|
|
130
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use super::completion::validate_complete_task_against_entries;
|
|
6
|
+
use super::diff::task_diff_from_entries;
|
|
7
|
+
use super::git_io::read_git_staged_changed_entries;
|
|
8
|
+
use super::process_guard::{validate_no_active_processes, ProcessGate};
|
|
9
|
+
use super::progress::{validate_init_complete, validate_upgrade_complete};
|
|
10
|
+
use super::reconcile::{
|
|
11
|
+
is_deterministic_harness_refresh_diff, is_harness_repair_diff,
|
|
12
|
+
is_install_or_upgrade_baseline_diff,
|
|
13
|
+
};
|
|
14
|
+
use super::shape::{
|
|
15
|
+
read_verification_check_ids, validate_active_task, validate_active_task_references,
|
|
16
|
+
validate_blocker, validate_pending_upgrade, validate_required_check_ids,
|
|
17
|
+
};
|
|
18
|
+
use super::types::ChangedEntry;
|
|
19
|
+
use crate::models::NaomeError;
|
|
20
|
+
pub(super) fn validate_commit_gate(
|
|
21
|
+
task_state: &Value,
|
|
22
|
+
root: &Path,
|
|
23
|
+
errors: &mut Vec<String>,
|
|
24
|
+
notices: &mut Vec<String>,
|
|
25
|
+
) -> Result<(), NaomeError> {
|
|
26
|
+
let staged_entries = read_git_staged_changed_entries(root)?;
|
|
27
|
+
let changed_paths: Vec<String> = staged_entries
|
|
28
|
+
.iter()
|
|
29
|
+
.map(|entry| entry.path.clone())
|
|
30
|
+
.collect();
|
|
31
|
+
if changed_paths.is_empty() {
|
|
32
|
+
return Ok(());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let status = task_state
|
|
36
|
+
.get("status")
|
|
37
|
+
.and_then(Value::as_str)
|
|
38
|
+
.unwrap_or("invalid");
|
|
39
|
+
if status == "complete" && is_deterministic_harness_refresh_diff(&changed_paths) {
|
|
40
|
+
validate_pending_upgrade(task_state, root, errors)?;
|
|
41
|
+
validate_completed_task_for_harness_refresh(task_state, root, &staged_entries, errors)?;
|
|
42
|
+
return Ok(());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if status == "complete" {
|
|
46
|
+
validate_active_task(task_state.get("activeTask"), errors);
|
|
47
|
+
validate_active_task_references(task_state.get("activeTask"), root, errors, Some(status))?;
|
|
48
|
+
if !task_state.get("blocker").is_some_and(Value::is_null) {
|
|
49
|
+
errors.push("complete task state must have blocker set to null.".to_string());
|
|
50
|
+
}
|
|
51
|
+
if let Some(active_task) = task_state.get("activeTask") {
|
|
52
|
+
let check_ids = read_verification_check_ids(root, errors)?;
|
|
53
|
+
validate_required_check_ids(active_task, &check_ids, errors);
|
|
54
|
+
validate_no_active_processes(root, errors, ProcessGate::Commit)?;
|
|
55
|
+
validate_complete_task_against_entries(
|
|
56
|
+
active_task,
|
|
57
|
+
root,
|
|
58
|
+
&check_ids,
|
|
59
|
+
&staged_entries,
|
|
60
|
+
errors,
|
|
61
|
+
)?;
|
|
62
|
+
if errors.is_empty() {
|
|
63
|
+
notices.push(format!(
|
|
64
|
+
"Commit gate accepted task-owned staged paths: {}.",
|
|
65
|
+
changed_paths.join(", ")
|
|
66
|
+
));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return Ok(());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if status == "idle" && is_install_or_upgrade_baseline_diff(root, &changed_paths)? {
|
|
73
|
+
return Ok(());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if status == "idle" && is_harness_repair_diff(root, &changed_paths)? {
|
|
77
|
+
return Ok(());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
validate_init_complete(root, errors)?;
|
|
81
|
+
validate_upgrade_complete(root, errors)?;
|
|
82
|
+
|
|
83
|
+
if status == "idle" {
|
|
84
|
+
errors.push(format!("NAOME commit gate blocked: changed paths are not owned by a completed task state. Changed paths: {}. Finish a NAOME task and use naome commit, or reconcile the diff before committing.", changed_paths.join(", ")));
|
|
85
|
+
return Ok(());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if status == "blocked" || status == "needs_human_review" {
|
|
89
|
+
validate_blocker(task_state.get("blocker"), errors);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
errors.push(format!("NAOME commit gate blocked because task state is {status}. Finish or revise the active task, set it to complete with fresh proof, then use naome commit. Human options: continue_current_task, request_task_changes, mark_task_blocked, cancel_task_state."));
|
|
93
|
+
Ok(())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pub(super) fn validate_completed_task_for_harness_refresh(
|
|
97
|
+
task_state: &Value,
|
|
98
|
+
root: &Path,
|
|
99
|
+
staged_entries: &[ChangedEntry],
|
|
100
|
+
errors: &mut Vec<String>,
|
|
101
|
+
) -> Result<(), NaomeError> {
|
|
102
|
+
validate_active_task(task_state.get("activeTask"), errors);
|
|
103
|
+
validate_active_task_references(task_state.get("activeTask"), root, errors, Some("complete"))?;
|
|
104
|
+
if !task_state.get("blocker").is_some_and(Value::is_null) {
|
|
105
|
+
errors.push("complete task state must have blocker set to null.".to_string());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let Some(active_task) = task_state.get("activeTask") else {
|
|
109
|
+
return Ok(());
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
let check_ids = read_verification_check_ids(root, errors)?;
|
|
113
|
+
validate_required_check_ids(active_task, &check_ids, errors);
|
|
114
|
+
|
|
115
|
+
let mut validation_errors = Vec::new();
|
|
116
|
+
validate_no_active_processes(root, &mut validation_errors, ProcessGate::Commit)?;
|
|
117
|
+
validate_complete_task_against_entries(
|
|
118
|
+
active_task,
|
|
119
|
+
root,
|
|
120
|
+
&check_ids,
|
|
121
|
+
staged_entries,
|
|
122
|
+
&mut validation_errors,
|
|
123
|
+
)?;
|
|
124
|
+
|
|
125
|
+
let staged_harness_paths = task_diff_from_entries(active_task, staged_entries).outside_paths;
|
|
126
|
+
let allowed_scope_error = format!(
|
|
127
|
+
"Changed files outside allowedPaths: {}. Human options: request_scope_change, move_changes_to_new_task, revert_out_of_scope_changes.",
|
|
128
|
+
staged_harness_paths.join(", ")
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
errors.extend(
|
|
132
|
+
validation_errors
|
|
133
|
+
.into_iter()
|
|
134
|
+
.filter(|error| error != &allowed_scope_error),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
Ok(())
|
|
138
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use serde_json::Value;
|
|
5
|
+
|
|
6
|
+
use crate::models::NaomeError;
|
|
7
|
+
|
|
8
|
+
use super::proof::{
|
|
9
|
+
validate_control_state_paths, validate_evidence_array, validate_evidence_paths,
|
|
10
|
+
};
|
|
11
|
+
use super::proof_model::{CanonicalProof, VerificationDefaults};
|
|
12
|
+
use super::proof_sources::read_path_sets;
|
|
13
|
+
use super::util::{is_iso_datetime, require_string};
|
|
14
|
+
|
|
15
|
+
pub(super) fn compact_proofs(
|
|
16
|
+
active_task: &Value,
|
|
17
|
+
root: &Path,
|
|
18
|
+
errors: &mut Vec<String>,
|
|
19
|
+
defaults: &HashMap<String, VerificationDefaults>,
|
|
20
|
+
) -> Result<Vec<CanonicalProof>, NaomeError> {
|
|
21
|
+
let path_sets = read_path_sets(active_task, root, errors)?;
|
|
22
|
+
let Some(batches) = active_task.get("proofBatches") else {
|
|
23
|
+
return Ok(Vec::new());
|
|
24
|
+
};
|
|
25
|
+
let Some(batches) = batches.as_array() else {
|
|
26
|
+
errors.push("activeTask.proofBatches must be an array when present.".to_string());
|
|
27
|
+
return Ok(Vec::new());
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let mut proofs = Vec::new();
|
|
31
|
+
for (batch_index, batch) in batches.iter().enumerate() {
|
|
32
|
+
let prefix = format!("activeTask.proofBatches[{batch_index}]");
|
|
33
|
+
let Some(batch_object) = batch.as_object() else {
|
|
34
|
+
errors.push(format!("{prefix} must be an object."));
|
|
35
|
+
continue;
|
|
36
|
+
};
|
|
37
|
+
let batch_checked_at = batch_object.get("checkedAt").and_then(Value::as_str);
|
|
38
|
+
if batch_checked_at.is_some_and(|value| !is_iso_datetime(value)) {
|
|
39
|
+
errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
|
|
40
|
+
}
|
|
41
|
+
let batch_ref = batch_object.get("evidencePathSet").and_then(Value::as_str);
|
|
42
|
+
let Some(batch_proofs) = batch_object.get("proofs").and_then(Value::as_array) else {
|
|
43
|
+
errors.push(format!("{prefix}.proofs must be an array."));
|
|
44
|
+
continue;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (proof_index, proof) in batch_proofs.iter().enumerate() {
|
|
48
|
+
let proof_prefix = format!("{prefix}.proofs[{proof_index}]");
|
|
49
|
+
if let Some(proof) = compact_proof(
|
|
50
|
+
proof,
|
|
51
|
+
&proof_prefix,
|
|
52
|
+
batch,
|
|
53
|
+
batch_checked_at,
|
|
54
|
+
batch_ref,
|
|
55
|
+
&path_sets,
|
|
56
|
+
defaults,
|
|
57
|
+
errors,
|
|
58
|
+
) {
|
|
59
|
+
validate_evidence_paths(
|
|
60
|
+
Some(&Value::Array(proof.evidence.clone())),
|
|
61
|
+
&format!("{proof_prefix}.evidence"),
|
|
62
|
+
root,
|
|
63
|
+
errors,
|
|
64
|
+
active_task,
|
|
65
|
+
)?;
|
|
66
|
+
proofs.push(proof);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
Ok(proofs)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn compact_proof(
|
|
74
|
+
proof: &Value,
|
|
75
|
+
prefix: &str,
|
|
76
|
+
batch: &Value,
|
|
77
|
+
batch_checked_at: Option<&str>,
|
|
78
|
+
batch_ref: Option<&str>,
|
|
79
|
+
path_sets: &HashMap<String, Vec<Value>>,
|
|
80
|
+
defaults: &HashMap<String, VerificationDefaults>,
|
|
81
|
+
errors: &mut Vec<String>,
|
|
82
|
+
) -> Option<CanonicalProof> {
|
|
83
|
+
let object = proof.as_object()?;
|
|
84
|
+
let check_id = object.get("checkId").and_then(Value::as_str)?;
|
|
85
|
+
let defaults = defaults.get(check_id);
|
|
86
|
+
let command = resolved_text(proof, batch, "command")
|
|
87
|
+
.or_else(|| defaults.map(|check| check.command.as_str()));
|
|
88
|
+
let cwd =
|
|
89
|
+
resolved_text(proof, batch, "cwd").or_else(|| defaults.map(|check| check.cwd.as_str()));
|
|
90
|
+
let checked_at = object
|
|
91
|
+
.get("checkedAt")
|
|
92
|
+
.and_then(Value::as_str)
|
|
93
|
+
.or(batch_checked_at);
|
|
94
|
+
let evidence = resolved_evidence(proof, batch_ref, path_sets, prefix, errors);
|
|
95
|
+
|
|
96
|
+
require_string(object.get("checkId"), &format!("{prefix}.checkId"), errors);
|
|
97
|
+
if command.is_none() {
|
|
98
|
+
errors.push(format!(
|
|
99
|
+
"{prefix}.command must be explicit or resolvable from .naome/verification.json."
|
|
100
|
+
));
|
|
101
|
+
}
|
|
102
|
+
if cwd.is_none() {
|
|
103
|
+
errors.push(format!(
|
|
104
|
+
"{prefix}.cwd must be explicit or resolvable from .naome/verification.json."
|
|
105
|
+
));
|
|
106
|
+
}
|
|
107
|
+
let Some(exit_code) = object.get("exitCode").and_then(Value::as_i64) else {
|
|
108
|
+
errors.push(format!("{prefix}.exitCode must be an integer."));
|
|
109
|
+
return None;
|
|
110
|
+
};
|
|
111
|
+
let Some(checked_at) = checked_at else {
|
|
112
|
+
errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
|
|
113
|
+
return None;
|
|
114
|
+
};
|
|
115
|
+
if !is_iso_datetime(checked_at) {
|
|
116
|
+
errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
|
|
117
|
+
}
|
|
118
|
+
Some(CanonicalProof {
|
|
119
|
+
check_id: check_id.to_string(),
|
|
120
|
+
command: command?.to_string(),
|
|
121
|
+
cwd: cwd?.to_string(),
|
|
122
|
+
exit_code,
|
|
123
|
+
checked_at: checked_at.to_string(),
|
|
124
|
+
evidence: evidence?,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn resolved_text<'a>(proof: &'a Value, batch: &'a Value, field: &str) -> Option<&'a str> {
|
|
129
|
+
proof
|
|
130
|
+
.get(field)
|
|
131
|
+
.and_then(Value::as_str)
|
|
132
|
+
.or_else(|| batch.get(field).and_then(Value::as_str))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn resolved_evidence(
|
|
136
|
+
proof: &Value,
|
|
137
|
+
batch_ref: Option<&str>,
|
|
138
|
+
path_sets: &HashMap<String, Vec<Value>>,
|
|
139
|
+
prefix: &str,
|
|
140
|
+
errors: &mut Vec<String>,
|
|
141
|
+
) -> Option<Vec<Value>> {
|
|
142
|
+
if let Some(evidence) = proof.get("evidence") {
|
|
143
|
+
validate_evidence_array(Some(evidence), &format!("{prefix}.evidence"), errors);
|
|
144
|
+
validate_control_state_paths(Some(evidence), &format!("{prefix}.evidence"), errors);
|
|
145
|
+
return evidence.as_array().cloned();
|
|
146
|
+
}
|
|
147
|
+
let path_set = proof
|
|
148
|
+
.get("evidencePathSet")
|
|
149
|
+
.and_then(Value::as_str)
|
|
150
|
+
.or(batch_ref);
|
|
151
|
+
match path_set.and_then(|name| path_sets.get(name)) {
|
|
152
|
+
Some(paths) => Some(paths.clone()),
|
|
153
|
+
None => {
|
|
154
|
+
errors.push(format!(
|
|
155
|
+
"{prefix}.evidence must be an evidence array or path-set reference."
|
|
156
|
+
));
|
|
157
|
+
None
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use crate::models::NaomeError;
|
|
6
|
+
|
|
7
|
+
use super::api::validate_task_state;
|
|
8
|
+
use super::git_io::read_git_changed_paths;
|
|
9
|
+
use super::repair::is_safe_harness_refresh_path;
|
|
10
|
+
use super::types::{
|
|
11
|
+
CompletedTaskHarnessRefreshDiff, TaskStateMode, TaskStateOptions, CONTROL_STATE_PATH,
|
|
12
|
+
};
|
|
13
|
+
use super::util::{matches_any_pattern, read_json, string_array};
|
|
14
|
+
pub(super) fn add_completed_task_diff_notice(
|
|
15
|
+
root: &Path,
|
|
16
|
+
notices: &mut Vec<String>,
|
|
17
|
+
) -> Result<(), NaomeError> {
|
|
18
|
+
let changed_paths = read_git_changed_paths(root)?;
|
|
19
|
+
if changed_paths.is_empty() {
|
|
20
|
+
return Ok(());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
notices.push(format!("Task is complete and verified. Changed paths: {}. NAOME intent can baseline it automatically before the next distinct task; only surface human choices when intent blocks or the user explicitly asks to review, revise, cancel, or commit.", changed_paths.join(", ")));
|
|
24
|
+
Ok(())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn completed_task_harness_refresh_diff(
|
|
28
|
+
root: &Path,
|
|
29
|
+
) -> Result<Option<CompletedTaskHarnessRefreshDiff>, NaomeError> {
|
|
30
|
+
let mut read_errors = Vec::new();
|
|
31
|
+
let Some(task_state) = read_json(root, ".naome/task-state.json", &mut read_errors)? else {
|
|
32
|
+
return Ok(None);
|
|
33
|
+
};
|
|
34
|
+
if !read_errors.is_empty() {
|
|
35
|
+
return Ok(None);
|
|
36
|
+
}
|
|
37
|
+
if task_state.get("status").and_then(Value::as_str) != Some("complete") {
|
|
38
|
+
return Ok(None);
|
|
39
|
+
}
|
|
40
|
+
let Some(active_task) = task_state.get("activeTask") else {
|
|
41
|
+
return Ok(None);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
|
|
45
|
+
let mut harness_paths = Vec::new();
|
|
46
|
+
let mut task_paths = Vec::new();
|
|
47
|
+
let mut other_paths = Vec::new();
|
|
48
|
+
|
|
49
|
+
for path in read_git_changed_paths(root)? {
|
|
50
|
+
if path == CONTROL_STATE_PATH {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if matches_any_pattern(&path, &allowed_paths) {
|
|
54
|
+
task_paths.push(path);
|
|
55
|
+
} else if is_safe_harness_refresh_path(&path) {
|
|
56
|
+
harness_paths.push(path);
|
|
57
|
+
} else {
|
|
58
|
+
other_paths.push(path);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if task_paths.is_empty() || harness_paths.is_empty() || !other_paths.is_empty() {
|
|
63
|
+
return Ok(None);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let report = validate_task_state(
|
|
67
|
+
root,
|
|
68
|
+
TaskStateOptions {
|
|
69
|
+
mode: TaskStateMode::State,
|
|
70
|
+
harness_health: None,
|
|
71
|
+
},
|
|
72
|
+
)?;
|
|
73
|
+
let allowed_scope_error = format!(
|
|
74
|
+
"Changed files outside allowedPaths: {}. Human options: request_scope_change, move_changes_to_new_task, revert_out_of_scope_changes.",
|
|
75
|
+
harness_paths.join(", ")
|
|
76
|
+
);
|
|
77
|
+
if report
|
|
78
|
+
.errors
|
|
79
|
+
.iter()
|
|
80
|
+
.all(|error| error == &allowed_scope_error)
|
|
81
|
+
{
|
|
82
|
+
Ok(Some(CompletedTaskHarnessRefreshDiff {
|
|
83
|
+
harness_paths,
|
|
84
|
+
task_paths,
|
|
85
|
+
}))
|
|
86
|
+
} else {
|
|
87
|
+
Ok(None)
|
|
88
|
+
}
|
|
89
|
+
}
|