diff --git a/.decapod/config.toml b/.decapod/config.toml index 22f95c1f..66ed9a5d 100644 --- a/.decapod/config.toml +++ b/.decapod/config.toml @@ -28,7 +28,7 @@ detected_surfaces = [ ] external_tracker = false container_workspaces = true -backend = "cloud" +backend = "local" declared_capabilities = [ "agent-helper", "authentication", diff --git a/.decapod/governance/claims.json b/.decapod/governance/claims.json index 5154357e..9d904907 100644 --- a/.decapod/governance/claims.json +++ b/.decapod/governance/claims.json @@ -11,7 +11,7 @@ "status": "active", "created_at": "2026-07-22", "updated_at": "2026-07-28", - "change_policy": "Claims are changed only through an issue-scoped review that preserves the baseline, Decapod condition, failure modes, measurements, proof gate, open questions, and evidence status. Governance artifact generation and publication inventory are tracked under issue #1025; the v0.85.0 cloud auto-login work for #1077 remains bounded to Decapod-owned backend composition, machine-local session reuse/refresh, repository identity, and CI surfaces while Propodus retains hosted authorization and persistence policy. One-time human GitHub approval, live protected todo behavior, and cross-system acceptance remain explicitly unproven follow-ups. The #1100/#1101/#1102 session lifecycle proof now covers local-session-first custody, machine-session reuse/refresh, pending-flow deduplication, one-time exchange persistence, and secret-free failure handling; deployment-provided live proof remains a protected follow-up." + "change_policy": "Claims are changed only through an issue-scoped review that preserves the baseline, Decapod condition, failure modes, measurements, proof gate, open questions, and evidence status. Governance artifact generation and publication inventory are tracked under issue #1025; the v0.85.0 cloud auto-login work for #1077 remains bounded to Decapod-owned backend composition, machine-local session reuse/refresh, repository identity, and CI surfaces while Propodus retains hosted authorization and persistence policy. One-time human GitHub approval, live protected todo behavior, and cross-system acceptance remain explicitly unproven follow-ups. The #1100/#1101/#1102 session lifecycle proof now covers local-session-first custody, machine-session reuse/refresh, pending-flow deduplication, one-time exchange persistence, and secret-free failure handling; deployment-provided live proof remains a protected follow-up. Hermes governance integrity and readiness substrate: issues #1094, #1096, #1059, #1055, and #1072." }, "scope": { "product": "decapod", diff --git a/.decapod/governance/plan.json b/.decapod/governance/plan.json index bf3aa6a3..2f3041f7 100644 --- a/.decapod/governance/plan.json +++ b/.decapod/governance/plan.json @@ -1,25 +1,23 @@ { "schema_version": "1.0.0", - "title": "Finish backend=cloud session lifecycle (#1100/#1101/#1102)", - "intent": "Make decapod session acquire establish local custody first, then reuse or refresh the machine-local Propodus session and complete one-time onboarding without repository credentials or local SQLite fallback.", - "state": "DONE", + "title": "Hermes governance integrity and readiness substrate (#1094 #1096 #1059 #1065 #1055 #1072)", + "intent": "Provide authoritative, deterministic governance artifact integrity, semantic freshness, workspace-versus-target delta state, and dirty-file classification for Hermes readiness and later effective-state diagnostics.", + "state": "APPROVED", "todo_ids": [ - "bend_01kyktj9m6ftb44j" + "feat_01kym05w1mxdpyhw" ], "proof_hooks": [ - "cargo test --lib propodus", - "cargo test --test cloud_backend_storage", + "cargo test --test governance_artifacts", + "cargo test --test hermes_governance_substrate", "cargo fmt --all -- --check", - "CLIPPY_CONF_DIR=.config cargo clippy --all-targets --all-features -- -D warnings", - "decapod validate --refresh-specs --projections" + "cargo clippy --all-targets --all-features -- -D warnings", + "decapod validate --projections" ], "unknowns": [], "human_questions": [], "stop_conditions": [], "unresolved_contradictions": [], - "deferred_questions": [ - "Run the existing ignored propodus_live proof through the protected GitHub environment with deployment-provided secrets; local implementation and redaction proof are complete." - ], + "deferred_questions": [], "constraints": { "forbidden_paths": [ ".decapod/data" @@ -27,5 +25,5 @@ "file_touch_budget": 40 }, "phases": [], - "updated_at": "1785227881Z" + "updated_at": "1785231514Z" } \ No newline at end of file diff --git a/.decapod/governance/trajectory.json b/.decapod/governance/trajectory.json index 49fa7ab7..46fe49f2 100644 --- a/.decapod/governance/trajectory.json +++ b/.decapod/governance/trajectory.json @@ -1,145 +1,134 @@ { "schema_version": "1.1.0", - "run_id": "01KYKVSESSION1071", - "intent_id": "intent:01KYKVSESSION1071", - "task_id": "bend_01kyktj9m6ftb44j", - "original_intent": "Finish Decapod backend=cloud session lifecycle with seamless local custody and Propodus machine-session reuse", - "derived_intent": "Move local session acquisition ahead of cloud preflight, preserve distinct cloud session custody, and prove reuse/refresh/redaction without local SQLite fallback", - "current_phase": "publication", + "run_id": "01KYM0HERMES", + "intent_id": "intent:01KYM0HERMES", + "task_id": "feat_01kym05w1mxdpyhw", + "original_intent": "Solve the Hermes governance integrity and readiness substrate wave in Decapod and publish a focused PR.", + "derived_intent": "Provide deterministic governance artifact integrity, semantic freshness, workspace-versus-target state, and dirty-file classification for Hermes readiness.", + "current_phase": "validation", "next_transitions": [ - "commit and publish focused PR", - "complete validated todo and publish focused PR", - "inspect issue contracts and current auth/session tests", "publish" ], + "blockers": [ + "pre-existing done TODO bend_01kyktj9m6ftb44j has stale validation epoch and remains unverified", + "pre-existing release_artifact_policy failure for inline tests in cli.rs, cloud_backend.rs, propodus.rs, repo_identity.rs", + "pre-existing release_artifact_policy flags inline tests in cli.rs, cloud_backend.rs, propodus.rs, repo_identity.rs", + "prior cloud-session TODO bend_01kyktj9m6ftb44j has stale validation epoch and remains unverified" + ], "active_boundaries": [ - "Work only in Decapod; do not modify Propodus; preserve root and unrelated dirty state" + "Decapod repository only; preserve unrelated user changes; no Propodus changes; no protected-root mutation." ], "repo_scope": [ - "src/decapod auth/session/Propodus client and deterministic tests/specs" + "Issues #1094 #1096 #1059 #1065 #1055 #1072" ], "inspected_files": [ - "src/decapod/core/auth.rs", - "src/decapod/core/entrypoint_integrity.rs", - "src/decapod/core/error.rs", - "src/decapod/core/propodus.rs", - "src/decapod/lib.rs", - "tests/cloud_backend_storage.rs", - "tests/propodus_contract.rs" + "src/decapod/core/governance_artifacts.rs", + "src/decapod/core/trajectory.rs", + "src/decapod/core/validate.rs", + "src/decapod/core/workspace.rs", + "tests/governance_artifacts.rs" ], "modified_files": [ - ".decapod/governance/plan.json", ".decapod/governance/trajectory.json", ".decapod/governance/validation.json", - ".decapod/managed/specs", - "src/decapod/core/auth.rs", - "src/decapod/core/entrypoint_integrity.rs", - "src/decapod/core/error.rs", - "src/decapod/core/propodus.rs", + "src/decapod/cli.rs", + "src/decapod/core/atomic.rs", + "src/decapod/core/dirty_classification.rs", + "src/decapod/core/governance_artifacts.rs", + "src/decapod/core/research_claims.rs", + "src/decapod/core/trajectory.rs", + "src/decapod/core/validate.rs", "src/decapod/lib.rs", - "tests/cloud_backend_storage.rs" + "tests/governance_artifacts.rs", + "tests/hermes_governance_substrate.rs" ], "declared_commands": [ - "CLIPPY_CONF_DIR=.config cargo clippy --all-targets --all-features -- -D warnings", + "cargo clippy --all-targets --all-features -- -D warnings", "cargo fmt --all -- --check", - "cargo test --locked -- --test-threads=1", - "decapod qa check --all", - "decapod release check", + "cargo test --all-targets -- --test-threads=1", + "cargo test --test hermes_governance_substrate --test governance_artifacts --test commit_often_gate", "decapod validate", - "decapod validate --projections --format json" + "docker run ... target/debug/decapod validate --format json" ], "tool_calls": [], "checks": [ { - "name": "artifact_inventory", - "status": "passed" + "name": "all_targets", + "status": "partial" }, { - "name": "claims_ledger", + "name": "cargo_fmt", "status": "passed" }, { - "name": "clippy", + "name": "claims_note", "status": "passed" }, { - "name": "decapod validate", + "name": "claims_note_test", "status": "passed" }, { - "name": "decapod_validate", + "name": "clippy", "status": "passed" }, { - "name": "default_full_tests", + "name": "container_validation", "status": "partial" }, { - "name": "focused_tests", - "status": "passed" - }, - { - "name": "format", + "name": "decapod validate", "status": "passed" }, { - "name": "protected_propodus_proof", - "status": "unavailable" - }, - { - "name": "qa_check", + "name": "fmt", "status": "passed" }, { - "name": "release_check", + "name": "focused_tests", "status": "passed" }, { - "name": "serialized_full_tests", + "name": "inventory", "status": "passed" }, { - "name": "targeted_secret_scan", + "name": "specs_refresh", "status": "passed" } ], "evidence": [ - "All deterministic cloud/session tests use synthetic credentials and fake transports; protected live proof reads deployment environment only and remains ignored by default.", - "All four required governance artifacts are present and staged; PR-diff inventory will be rechecked after commit.", - "Focused session/auth and cloud backend tests pass; serialized full suite passed all feature and governance tests except an existing release-source-tree test policy failure in pre-existing source modules during the default run.", - "Protected live Propodus proof was not run because deployment-provided secrets were unavailable.", - "decapod validate --projections reported status ok, 199 gates passed, zero failures, four existing hygiene warnings.", - "validation epoch ve_32f036e0a69734a3 completed with zero failures", - "validation epoch ve_4d1a663bbebccf3b completed with zero failures" - ], - "shortcut_risk_signals": [ - "Protected live provider proof unavailable locally; not claimed." - ], - "unresolved_assumptions": [ - "No Propodus changes are in scope; provider contract is treated as existing and sufficient." + "214 unit tests and focused Hermes suite passed; serialized all-target run stopped only at pre-existing release_artifact_policy inline-test inventory; container validation passed all product gates and retained one unrelated prior cloud TODO proof-hook failure", + "Governed claims-note update preserves ledger layout and is idempotent; focused Hermes and governance tests pass.", + "focused Hermes governance substrate tests passed", + "validation epoch ve_251274fa8eeb6d3f completed with zero failures", + "validation epoch ve_7e353d28f3232cb5 completed with zero failures", + "validation epoch ve_89ad123716a27170 completed with zero failures" ], + "shortcut_risk_signals": [], + "unresolved_assumptions": [], "proof_status": "partial", "verdicts": { "intent_alignment": "supported", "boundary_discipline": "supported", - "shortcut_risk": "caution", + "shortcut_risk": "supported", "completion_proof": "caution" }, - "artifact_hash": "sha256:8dfede0546786ba4492acb0c18dc6cfde4d10674a074df3fe99eb4ab1f052ce1", + "artifact_hash": "sha256:6f01aa0d8ff082f595709805251a17801c9ccec16a7d8ab6c62aec4e1a833ec1", "custody": { "schema_version": "1.0.0", "intents": { - "intent:01KYKVSESSION1071": { - "id": "intent:01KYKVSESSION1071", - "raw_intent": "Finish Decapod backend=cloud session lifecycle with seamless local custody and Propodus machine-session reuse", - "refined_intent": "Move local session acquisition ahead of cloud preflight, preserve distinct cloud session custody, and prove reuse/refresh/redaction without local SQLite fallback", + "intent:01KYM0HERMES": { + "id": "intent:01KYM0HERMES", + "raw_intent": "Solve the Hermes governance integrity and readiness substrate wave in Decapod and publish a focused PR.", + "refined_intent": "Provide deterministic governance artifact integrity, semantic freshness, workspace-versus-target state, and dirty-file classification for Hermes readiness.", "acceptance_criteria": [], "constraints": [ - "src/decapod auth/session/Propodus client and deterministic tests/specs" + "Issues #1094 #1096 #1059 #1065 #1055 #1072" ], "assumptions": [], "boundaries": [ - "Work only in Decapod; do not modify Propodus; preserve root and unrelated dirty state" + "Decapod repository only; preserve unrelated user changes; no Propodus changes; no protected-root mutation." ], "out_of_scope": [], "proof_requirements": [], @@ -152,155 +141,290 @@ { "sequence": 2, "event_id": "event:2", - "intent_id": "intent:01KYKVSESSION1071", + "intent_id": "intent:01KYM0HERMES", "kind": "created" }, { "sequence": 3, "event_id": "event:3", - "intent_id": "intent:01KYKVSESSION1071", + "intent_id": "intent:01KYM0HERMES", "kind": "refined" }, { "sequence": 4, "event_id": "event:4", - "intent_id": "intent:01KYKVSESSION1071", + "intent_id": "intent:01KYM0HERMES", "kind": "trajectory_step_recorded", - "detail": "trajectory:01KYKVSESSION1071" + "detail": "trajectory:01KYM0HERMES" }, { "sequence": 5, "event_id": "event:5", - "intent_id": "intent:01KYKVSESSION1071", + "intent_id": "intent:01KYM0HERMES", "kind": "trajectory_step_recorded", - "detail": "trajectory:01KYKVSESSION1071" + "detail": "trajectory:01KYM0HERMES" }, { "sequence": 6, "event_id": "event:6", - "intent_id": "intent:01KYKVSESSION1071", + "intent_id": "intent:01KYM0HERMES", "kind": "trajectory_step_recorded", - "detail": "trajectory:01KYKVSESSION1071" + "detail": "trajectory:01KYM0HERMES" }, { "sequence": 7, "event_id": "event:7", - "intent_id": "intent:01KYKVSESSION1071", + "intent_id": "intent:01KYM0HERMES", + "kind": "trajectory_step_recorded", + "detail": "trajectory:01KYM0HERMES" + }, + { + "sequence": 8, + "event_id": "event:8", + "intent_id": "intent:01KYM0HERMES", + "kind": "trajectory_step_recorded", + "detail": "trajectory:01KYM0HERMES" + }, + { + "sequence": 9, + "event_id": "event:9", + "intent_id": "intent:01KYM0HERMES", + "kind": "trajectory_step_recorded", + "detail": "trajectory:01KYM0HERMES" + }, + { + "sequence": 10, + "event_id": "event:10", + "intent_id": "intent:01KYM0HERMES", + "kind": "trajectory_step_recorded", + "detail": "trajectory:01KYM0HERMES" + }, + { + "sequence": 11, + "event_id": "event:11", + "intent_id": "intent:01KYM0HERMES", + "kind": "trajectory_step_recorded", + "detail": "trajectory:01KYM0HERMES" + }, + { + "sequence": 12, + "event_id": "event:12", + "intent_id": "intent:01KYM0HERMES", + "kind": "trajectory_step_recorded", + "detail": "trajectory:01KYM0HERMES" + }, + { + "sequence": 13, + "event_id": "event:13", + "intent_id": "intent:01KYM0HERMES", "kind": "trajectory_step_recorded", - "detail": "trajectory:01KYKVSESSION1071" + "detail": "trajectory:01KYM0HERMES" } ], "trajectories": { - "01KYKVSESSION1071": { - "id": "01KYKVSESSION1071", - "intent_id": "intent:01KYKVSESSION1071", + "01KYM0HERMES": { + "id": "01KYM0HERMES", + "intent_id": "intent:01KYM0HERMES", "evidence_only": true, "steps": [ { "sequence": 4, + "action": "implementation", + "command": "cargo test --test hermes_governance_substrate --test governance_artifacts --test commit_often_gate", + "scope": [ + "Issues #1094 #1096 #1059 #1065 #1055 #1072" + ], + "observations": [ + "src/decapod/core/governance_artifacts.rs", + "src/decapod/core/validate.rs", + "src/decapod/core/trajectory.rs", + "src/decapod/core/workspace.rs", + "tests/governance_artifacts.rs", + "src/decapod/core/atomic.rs", + "src/decapod/core/dirty_classification.rs", + "src/decapod/core/governance_artifacts.rs", + "src/decapod/core/trajectory.rs", + "src/decapod/core/validate.rs", + "tests/hermes_governance_substrate.rs", + "focused Hermes governance substrate tests passed" + ], + "proof_refs": [ + "focused_tests" + ], + "validation_findings": [], + "custody_event_id": "event:4" + }, + { + "sequence": 5, + "action": "validation", + "command": "cargo fmt --all -- --check", + "scope": [ + "Issues #1094 #1096 #1059 #1065 #1055 #1072" + ], + "observations": [ + "214 unit tests and focused Hermes suite passed; serialized all-target run stopped only at pre-existing release_artifact_policy inline-test inventory; container validation passed all product gates and retained one unrelated prior cloud TODO proof-hook failure" + ], + "proof_refs": [ + "cargo_fmt", + "clippy", + "focused_tests", + "all_targets", + "container_validation" + ], + "validation_findings": [], + "custody_event_id": "event:5" + }, + { + "sequence": 6, + "action": "trajectory.record", + "scope": [ + "Issues #1094 #1096 #1059 #1065 #1055 #1072" + ], + "observations": [], + "proof_refs": [ + "specs_refresh", + "cargo_fmt", + "clippy", + "focused_tests", + "all_targets" + ], + "validation_findings": [], + "custody_event_id": "event:6" + }, + { + "sequence": 7, + "action": "trajectory.record", + "scope": [ + "Issues #1094 #1096 #1059 #1065 #1055 #1072" + ], + "observations": [], + "proof_refs": [ + "container_validation", + "inventory" + ], + "validation_findings": [], + "custody_event_id": "event:7" + }, + { + "sequence": 8, "action": "validation", "command": "decapod validate", "scope": [ - "src/decapod auth/session/Propodus client and deterministic tests/specs" + "Issues #1094 #1096 #1059 #1065 #1055 #1072" ], "observations": [ ".decapod/governance/trajectory.json", ".decapod/governance/validation.json", - "validation epoch ve_4d1a663bbebccf3b completed with zero failures" + "validation epoch ve_251274fa8eeb6d3f completed with zero failures" ], "proof_refs": [ "decapod validate" ], "validation_findings": [], - "custody_event_id": "event:4" + "custody_event_id": "event:8" }, { - "sequence": 5, + "sequence": 9, "action": "validation", "command": "decapod validate", "scope": [ - "src/decapod auth/session/Propodus client and deterministic tests/specs" + "Issues #1094 #1096 #1059 #1065 #1055 #1072" ], "observations": [ ".decapod/governance/trajectory.json", ".decapod/governance/validation.json", - "validation epoch ve_32f036e0a69734a3 completed with zero failures" + "validation epoch ve_7e353d28f3232cb5 completed with zero failures" ], "proof_refs": [ "decapod validate" ], "validation_findings": [], - "custody_event_id": "event:5" + "custody_event_id": "event:9" }, { - "sequence": 6, - "action": "verification", - "command": "cargo fmt --all -- --check", + "sequence": 10, + "action": "trajectory.record", "scope": [ - "src/decapod auth/session/Propodus client and deterministic tests/specs" + "Issues #1094 #1096 #1059 #1065 #1055 #1072" ], "observations": [ + "src/decapod/core/research_claims.rs", + "src/decapod/core/governance_artifacts.rs", + "src/decapod/cli.rs", "src/decapod/lib.rs", - "src/decapod/core/auth.rs", - "src/decapod/core/error.rs", - "src/decapod/core/propodus.rs", - "src/decapod/core/entrypoint_integrity.rs", - "tests/cloud_backend_storage.rs", - "tests/propodus_contract.rs", - "src/decapod/lib.rs", - "src/decapod/core/auth.rs", - "src/decapod/core/error.rs", - "src/decapod/core/propodus.rs", - "src/decapod/core/entrypoint_integrity.rs", - "tests/cloud_backend_storage.rs", - ".decapod/managed/specs", - ".decapod/governance/plan.json", - ".decapod/governance/validation.json", - ".decapod/governance/trajectory.json", - "Focused session/auth and cloud backend tests pass; serialized full suite passed all feature and governance tests except an existing release-source-tree test policy failure in pre-existing source modules during the default run.", - "decapod validate --projections reported status ok, 199 gates passed, zero failures, four existing hygiene warnings.", - "Protected live Propodus proof was not run because deployment-provided secrets were unavailable." + "tests/governance_artifacts.rs", + "Governed claims-note update preserves ledger layout and is idempotent; focused Hermes and governance tests pass." ], "proof_refs": [ - "format", + "claims_note", + "claims_note_test", + "specs_refresh", "focused_tests", - "serialized_full_tests", - "default_full_tests", "clippy", - "decapod_validate", - "qa_check", - "release_check", - "protected_propodus_proof" + "fmt" + ], + "validation_findings": [], + "custody_event_id": "event:10" + }, + { + "sequence": 11, + "action": "validation", + "command": "decapod validate", + "scope": [ + "Issues #1094 #1096 #1059 #1065 #1055 #1072" ], - "validation_findings": [ - "Protected live provider proof unavailable locally; not claimed.", - "No Propodus changes are in scope; provider contract is treated as existing and sufficient." + "observations": [ + ".decapod/governance/trajectory.json", + ".decapod/governance/validation.json", + "validation epoch ve_7e353d28f3232cb5 completed with zero failures" ], - "custody_event_id": "event:6" + "proof_refs": [ + "decapod validate" + ], + "validation_findings": [], + "custody_event_id": "event:11" }, { - "sequence": 7, - "action": "publication", + "sequence": 12, + "action": "validation", + "command": "decapod validate", "scope": [ - "src/decapod auth/session/Propodus client and deterministic tests/specs" + "Issues #1094 #1096 #1059 #1065 #1055 #1072" ], "observations": [ - "All deterministic cloud/session tests use synthetic credentials and fake transports; protected live proof reads deployment environment only and remains ignored by default.", - "All four required governance artifacts are present and staged; PR-diff inventory will be rechecked after commit." + ".decapod/governance/trajectory.json", + ".decapod/governance/validation.json", + "validation epoch ve_7e353d28f3232cb5 completed with zero failures" ], "proof_refs": [ - "claims_ledger", - "artifact_inventory", - "targeted_secret_scan" + "decapod validate" ], "validation_findings": [], - "custody_event_id": "event:7" + "custody_event_id": "event:12" + }, + { + "sequence": 13, + "action": "validation", + "command": "decapod validate", + "scope": [ + "Issues #1094 #1096 #1059 #1065 #1055 #1072" + ], + "observations": [ + ".decapod/governance/trajectory.json", + ".decapod/governance/validation.json", + "validation epoch ve_89ad123716a27170 completed with zero failures" + ], + "proof_refs": [ + "decapod validate" + ], + "validation_findings": [], + "custody_event_id": "event:13" } ] } }, "summaries": {}, "knowledge_candidates": [], - "next_sequence": 7 + "next_sequence": 13 } } \ No newline at end of file diff --git a/.decapod/governance/validation.json b/.decapod/governance/validation.json index aa08d83c..a55bcc40 100644 --- a/.decapod/governance/validation.json +++ b/.decapod/governance/validation.json @@ -1,82 +1,77 @@ { "schema_version": "1.0.0", "kind": "validation_receipt", - "decapod_release": "0.86.2", - "git_revision": "def52a98a2fdade3e85fcda9d600e70ebb1cea8d", - "repo_signal_fingerprint": "2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b", - "trajectory_run_id": "01KYKVSESSION1071", - "trajectory_artifact_hash": "sha256:556e556afc3cf421be2a3f3cf87c4d189c69cecd528730231b8bdd15b3c5edab", + "decapod_release": "0.87.0", + "git_revision": "8e098a24bbce43e26d195066599c59eae1953030", + "repo_signal_fingerprint": "c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1", + "trajectory_run_id": "01KYM0HERMES", + "trajectory_artifact_hash": "sha256:6f01aa0d8ff082f595709805251a17801c9ccec16a7d8ab6c62aec4e1a833ec1", "validation_epoch": { "schema_version": "1.0.0", - "epoch_id": "ve_32f036e0a69734a3", - "evaluator_identity": "decapod-validate@0.86.2", - "evaluator_set_hash": "sha256:d733ccf8bc3a64661eb3ff80ab4309051d1c5c1b5460beb7f5d39fdf63cc3dd5", - "constitution_version": "embedded-docs@0.86.2", - "constitution_hash": "sha256:7729aac4966092fa60e12d72c4f630dc565f65c54dcc8155e207f3d3aae50b03", + "epoch_id": "ve_89ad123716a27170", + "evaluator_identity": "decapod-validate@0.87.0", + "evaluator_set_hash": "sha256:69c3f2a1c197f5fbe84f8c0f2033069038588d76fd02d74888772d53ff076672", + "constitution_version": "embedded-docs@0.87.0", + "constitution_hash": "sha256:e64d6eff0cfbd42b096e4b022c71451abc0790405acea49907c272b57564413b", "validation_profile": "default", "validation_profile_hash": "sha256:37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", "proof_rubric": "decapod-validate/current-proof-v1", "proof_rubric_hash": "sha256:99e8ef25629da391e9b479db9fff52b82f340224f7a69d8a88d2992ae8e90466", - "generated_specs_manifest_hash": "sha256:aec4fd8ad13b0dd11687f0a36c7567f8f5f3b02de21c80ef17002d020e7db5b2", - "generated_specs_fingerprint": "2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b", + "generated_specs_manifest_hash": "sha256:de8ce833aeffec6b458e422167cae81805e4825fc56fd663390792bd44362859", + "generated_specs_fingerprint": "c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1", "material_hashes": { - "constitution:core/DECAPOD": "sha256:7729aac4966092fa60e12d72c4f630dc565f65c54dcc8155e207f3d3aae50b03", - "generated_spec:.decapod/managed/specs/ARCHITECTURE.md": "sha256:fd0842afe8d4e696f089aef6b2a2f1c89b13778874840f6a803be416db6be194", - "generated_spec:.decapod/managed/specs/INTENT.md": "sha256:9b8c61d3c3fababb257672eb840cda650bd358f52fb1b69833841b1e879e1b5e", - "generated_spec:.decapod/managed/specs/INTERFACES.md": "sha256:713d3c099f4b3935e54ee6289e9c213df1c48cc763e4218897ce5d4ce6c84f06", - "generated_spec:.decapod/managed/specs/OPERATIONS.md": "sha256:e579070af84c3a5b8ba5b5bebfd8527782081243f826eddc5a9a162713c7c1be", - "generated_spec:.decapod/managed/specs/README.md": "sha256:2e243f4f9e5091c421e1abac86f785c9556a6fcbb0c127463eb5b9563c4ddb6c", - "generated_spec:.decapod/managed/specs/SECURITY.md": "sha256:47e769e5204a20740344945a46f61f02edbce308023420561152d430a76e15d6", - "generated_spec:.decapod/managed/specs/SEMANTICS.md": "sha256:48bafe92adbaa51b304e290ca538d509f2b39f69531e7990003a430a8e364ef5", - "generated_spec:.decapod/managed/specs/VALIDATION.md": "sha256:371e4101c7360bdb9cfb13603ead861c74e8786a005ca371fd606d74228ab3d5", - "generated_specs_fingerprint": "2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b", - "generated_specs_manifest": "sha256:aec4fd8ad13b0dd11687f0a36c7567f8f5f3b02de21c80ef17002d020e7db5b2", + "constitution:core/DECAPOD": "sha256:e64d6eff0cfbd42b096e4b022c71451abc0790405acea49907c272b57564413b", + "generated_spec:.decapod/managed/specs/ARCHITECTURE.md": "sha256:400e3618a6a259acb5a901e884419917bc824eb4414a42021d32c5e55b07ab70", + "generated_spec:.decapod/managed/specs/INTENT.md": "sha256:e2f76d44e62620d870f69362962730aecbdf9308ee5ae5ae79faefacf62ab40d", + "generated_spec:.decapod/managed/specs/INTERFACES.md": "sha256:5b6c409f4c30d5ee4b41d8a59dbe628eeb12e74344b4c510adee28865e3720c7", + "generated_spec:.decapod/managed/specs/OPERATIONS.md": "sha256:16b9990fcbc18c66ea5344fd60fd0ba12a4e6406afbb9551e96aeccc71a759bb", + "generated_spec:.decapod/managed/specs/README.md": "sha256:7fe8e1a8b8d420e39b056fd8322b309f9221cf89913c229fbd417dfe338ecc92", + "generated_spec:.decapod/managed/specs/SECURITY.md": "sha256:ad7b1e9baa4b0312af217f8c754d12f3887a2ac55df452de242a345f911a40b7", + "generated_spec:.decapod/managed/specs/SEMANTICS.md": "sha256:b9fcde0fb285437d54e07dcfa6a9057d88b91a20f7aad2e3d0ce72f5b9279419", + "generated_spec:.decapod/managed/specs/VALIDATION.md": "sha256:cf90e3ef0b70ed1a79c3939efec39b901c14ad0c489b01892c0c8f4448abd501", + "generated_specs_fingerprint": "c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1", + "generated_specs_manifest": "sha256:de8ce833aeffec6b458e422167cae81805e4825fc56fd663390792bd44362859", "proof_rubric": "sha256:99e8ef25629da391e9b479db9fff52b82f340224f7a69d8a88d2992ae8e90466", "validation_profile": "sha256:37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f" } }, "status": "ok", - "pass_count": 199, + "pass_count": 171, "fail_count": 0, - "warn_count": 4, - "elapsed_ms": 2047, + "warn_count": 3, + "elapsed_ms": 987, "drift_findings": [], "temporary_artifacts_cleaned": 1, "failures": [], "warnings": [ "Risk map missing (run `decapod riskmap init`)", "Spec markdown drift checks are hygiene-only. Use validate_machine_contract for authoritative governance.", - "Watcher audit trail missing (run `decapod govern watcher run`)", - "[federation.rebuild_determinism] DB diverged from event replay. Current: 488 nodes/488 sources/399 edges. Rebuilt: 1 nodes/1 sources/0 edges. Run: decapod data federation rebuild" + "Watcher audit trail missing (run `decapod govern watcher run`)" ], "gate_timings": [ { "name": "validate_machine_contract", - "elapsed_ms": 436 + "elapsed_ms": 609 }, { - "name": "validate_control_plane_contract", - "elapsed_ms": 248 - }, - { - "name": "validate_projection_consistency", - "elapsed_ms": 239 + "name": "validate_markdown_primitives_roundtrip_gate", + "elapsed_ms": 97 }, { "name": "validate_schema_determinism", - "elapsed_ms": 44 + "elapsed_ms": 47 }, { - "name": "validate_federation_gates", - "elapsed_ms": 39 + "name": "validate_project_specs_docs", + "elapsed_ms": 19 }, { - "name": "validate_project_specs_docs", - "elapsed_ms": 18 + "name": "validate_obligations", + "elapsed_ms": 17 }, { "name": "validate_embedded_self_contained", - "elapsed_ms": 14 + "elapsed_ms": 15 }, { "name": "validate_no_legacy_namespaces", @@ -86,120 +81,108 @@ "name": "validate_repomap_determinism", "elapsed_ms": 5 }, - { - "name": "validate_markdown_primitives_roundtrip_gate", - "elapsed_ms": 3 - }, { "name": "validate_health_purity", - "elapsed_ms": 2 + "elapsed_ms": 3 }, { "name": "validate_gatekeeper_gate", "elapsed_ms": 2 }, { - "name": "validate_generated_artifact_whitelist", - "elapsed_ms": 2 - }, - { - "name": "validate_obligations", - "elapsed_ms": 1 - }, - { - "name": "validate_database_schema_versions", - "elapsed_ms": 1 + "name": "validate_interface_contract_bootstrap", + "elapsed_ms": 0 }, { - "name": "validate_knowledge_integrity", - "elapsed_ms": 1 + "name": "validate_research_claims_if_present", + "elapsed_ms": 0 }, { - "name": "validate_canon_mutation", + "name": "validate_trajectory_artifacts_if_present", "elapsed_ms": 0 }, { - "name": "validate_watcher_purity", + "name": "validate_entrypoint_invariants", "elapsed_ms": 0 }, { - "name": "validate_research_claims_if_present", + "name": "validate_heartbeat_invocation_gate", "elapsed_ms": 0 }, { - "name": "validate_interface_contract_bootstrap", + "name": "validate_repo_map", "elapsed_ms": 0 }, { - "name": "validate_risk_map_violations", + "name": "validate_validation_receipt_if_present", "elapsed_ms": 0 }, { - "name": "validate_heartbeat_invocation_gate", + "name": "validate_project_config_toml", "elapsed_ms": 0 }, { - "name": "validate_entrypoint_invariants", + "name": "validate_spec_drift", "elapsed_ms": 0 }, { - "name": "validate_repo_map", + "name": "validate_state_commit_gate", "elapsed_ms": 0 }, { - "name": "validate_project_config_toml", + "name": "validate_context_capsule_policy_contract", "elapsed_ms": 0 }, { - "name": "validate_trajectory_artifacts_if_present", + "name": "validate_health_cache_integrity", "elapsed_ms": 0 }, { - "name": "validate_validation_receipt_if_present", + "name": "validate_workunit_manifests_if_present", "elapsed_ms": 0 }, { - "name": "validate_project_scoped_state", + "name": "validate_lcm_immutability", "elapsed_ms": 0 }, { - "name": "validate_spec_drift", + "name": "validate_docs_templates_bucket", "elapsed_ms": 0 }, { - "name": "validate_state_commit_gate", + "name": "validate_federation_gates", "elapsed_ms": 0 }, { - "name": "validate_context_capsule_policy_contract", + "name": "validate_eval_gate_if_required", "elapsed_ms": 0 }, { - "name": "validate_docs_templates_bucket", + "name": "validate_watcher_audit", "elapsed_ms": 0 }, { - "name": "validate_workunit_manifests_if_present", + "name": "validate_database_schema_versions", "elapsed_ms": 0 }, { - "name": "validate_watcher_audit", + "name": "validate_lineage_hard_gate", "elapsed_ms": 0 }, { - "name": "validate_eval_gate_if_required", + "name": "validate_policy_integrity", "elapsed_ms": 0 }, { - "name": "validate_lcm_immutability", + "name": "validate_git_workspace_context", "elapsed_ms": 0 }, { - "name": "validate_root_dockerfile_seed_detection", + "name": "validate_risk_map", "elapsed_ms": 0 }, { - "name": "validate_lineage_hard_gate", + "name": "validate_root_dockerfile_seed_detection", "elapsed_ms": 0 }, { @@ -207,11 +190,11 @@ "elapsed_ms": 0 }, { - "name": "validate_policy_integrity", + "name": "validate_coplayer_policy_tightening", "elapsed_ms": 0 }, { - "name": "validate_context_capsules_if_present", + "name": "validate_knowledge_integrity", "elapsed_ms": 0 }, { @@ -219,7 +202,7 @@ "elapsed_ms": 0 }, { - "name": "validate_health_cache_integrity", + "name": "validate_context_capsules_if_present", "elapsed_ms": 0 }, { @@ -231,29 +214,33 @@ "elapsed_ms": 0 }, { - "name": "validate_coplayer_policy_tightening", + "name": "validate_watcher_purity", "elapsed_ms": 0 }, { - "name": "validate_git_workspace_context", + "name": "validate_lcm_rebuild_gate", "elapsed_ms": 0 }, { - "name": "validate_risk_map", + "name": "validate_control_plane_contract", "elapsed_ms": 0 }, { - "name": "validate_tooling_gate", + "name": "validate_risk_map_violations", "elapsed_ms": 0 }, { - "name": "validate_lcm_rebuild_gate", + "name": "validate_canon_mutation", "elapsed_ms": 0 }, { "name": "validate_plan_governed_execution_gate", "elapsed_ms": 0 }, + { + "name": "validate_tooling_gate", + "elapsed_ms": 0 + }, { "name": "validate_stale_workspaces", "elapsed_ms": 0 @@ -265,6 +252,14 @@ { "name": "validate_git_push_pr_gate", "elapsed_ms": 0 + }, + { + "name": "validate_project_scoped_state", + "elapsed_ms": 0 + }, + { + "name": "validate_generated_artifact_whitelist", + "elapsed_ms": 0 } ], "parallelism": 1, @@ -274,13 +269,12 @@ "reasons": [ "Risk map missing (run `decapod riskmap init`)", "Spec markdown drift checks are hygiene-only. Use validate_machine_contract for authoritative governance.", - "Watcher audit trail missing (run `decapod govern watcher run`)", - "[federation.rebuild_determinism] DB diverged from event replay. Current: 488 nodes/488 sources/399 edges. Rebuilt: 1 nodes/1 sources/0 edges. Run: decapod data federation rebuild" + "Watcher audit trail missing (run `decapod govern watcher run`)" ], "recommendations": [ "Review the reported validation warnings before relying on the CI result.", "Use `decapod validate -v --format json` to inspect the affected gate output." ] }, - "receipt_hash": "sha256:a470796087fe62e61ebc0f57b6305ace75223445126a9296ca29ec3f08565227" + "receipt_hash": "sha256:6e3de83acf5e79c7b2712044bb0dadc11e2c74883dccd3f92dc2aba6c75f5165" } \ No newline at end of file diff --git a/.decapod/managed/Dockerfile.decapod b/.decapod/managed/Dockerfile.decapod index 6e3a41be..e772dc34 100644 --- a/.decapod/managed/Dockerfile.decapod +++ b/.decapod/managed/Dockerfile.decapod @@ -2,9 +2,9 @@ # Path: .decapod/managed/Dockerfile.decapod # Managed seed: Decapod maintains the image/version header; agents may # mutate project-specific packages and commands in workspace branches. -ARG DECAPOD_IMAGE=ghcr.io/decapodlabs/decapod:v0.86.2-debian +ARG DECAPOD_IMAGE=ghcr.io/decapodlabs/decapod:v0.87.0-debian FROM $DECAPOD_IMAGE -ARG DECAPOD_VERSION=0.86.2 +ARG DECAPOD_VERSION=0.87.0 ARG DECAPOD_WORKSPACE_PATH=unknown ARG DECAPOD_USE_LOCAL_BINARY=0 LABEL org.opencontainers.image.base.name="$DECAPOD_IMAGE" diff --git a/.decapod/managed/specs/.manifest.json b/.decapod/managed/specs/.manifest.json index ddb66ace..78d89a40 100644 --- a/.decapod/managed/specs/.manifest.json +++ b/.decapod/managed/specs/.manifest.json @@ -1,8 +1,8 @@ { "schema_version": "1.1.0", "template_version": "scaffold-v3", - "generated_at": "1785226872Z", - "repo_signal_fingerprint": "2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b", + "generated_at": "1785235147Z", + "repo_signal_fingerprint": "c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1", "declared_capabilities": [ "agent-helper", "authentication", @@ -16,82 +16,82 @@ "scheduled-jobs" ], "capability_definition_version": "1.0.0", - "config_input_hash": "8d09a6ab4f6034d39463e02c4c8530704b92bd227a6e6831bc63bb59b6ad55cc", - "spec_input_hash": "a3c1b275db28f5fc6dffd8e5570b6ceee0ab59a4db09b661d46c3a3d0b11dbfb", - "decapod_release": "0.86.2", + "config_input_hash": "4fd4fbb5f2e6bb3ce0c0f14971ef28a2067288d440caa70b9d22915f184a73b6", + "spec_input_hash": "cc3a342fe65c5a8b604c424e607aa05b7235bea2be3e5549c1e0335e56f8fc1f", + "decapod_release": "0.87.0", "entrypoints": [ { "path": "AGENTS.md", - "template_hash": "c0ae1635330acd0641ee5be3224b9adc1f87d02e0129f8501eace424776a82e8", - "content_hash": "c0ae1635330acd0641ee5be3224b9adc1f87d02e0129f8501eace424776a82e8", - "fingerprint": "776d7696f5579c5188091323d94fcb1fff0a1f3cb9cf692d00de934ef06c1915" + "template_hash": "052f2bf82d738aecb88aa1e4adc8798e5b83eec53b3f815bf0f3f1cfd0310caf", + "content_hash": "052f2bf82d738aecb88aa1e4adc8798e5b83eec53b3f815bf0f3f1cfd0310caf", + "fingerprint": "88c4330305ab1dcc2d1ade14f1244d7172dead921b5a3ea62ec8fc4f44430389" }, { "path": "CLAUDE.md", - "template_hash": "1a593a48fa082436f2f4ea3b579d67ed9ec37c6db02c528f271e53e538139bdd", - "content_hash": "1a593a48fa082436f2f4ea3b579d67ed9ec37c6db02c528f271e53e538139bdd", - "fingerprint": "ee5ad891871fc9e76265288fd2fe90c39b8bef52e7732d171953b705edb8b90d" + "template_hash": "8491baf0248ec03e53879dc73fc025ad623d2835fa7a27a285c61d6c9aebf23b", + "content_hash": "8491baf0248ec03e53879dc73fc025ad623d2835fa7a27a285c61d6c9aebf23b", + "fingerprint": "4154e046dff7389d6e1a3bafc32f0b9bae24121cbe0c505300b9999d97175a8f" }, { "path": "GEMINI.md", - "template_hash": "5da8af5e10ab075c36da5961f6b56c806432494cd434dc43751d354715ca624f", - "content_hash": "5da8af5e10ab075c36da5961f6b56c806432494cd434dc43751d354715ca624f", - "fingerprint": "777afd971370e9116098c70836946a05fab951412600e7da718ae1e7e5539c34" + "template_hash": "c1c4633701d498d84275d4fc76d3e1d5e00ae42cc63b48079e71415a403d2944", + "content_hash": "c1c4633701d498d84275d4fc76d3e1d5e00ae42cc63b48079e71415a403d2944", + "fingerprint": "7e841746cc64c02324864e784d822ef4d6b5bf12a891c65c5141b08ef989b4d0" }, { "path": "CODEX.md", - "template_hash": "168e96679d789e121586ee8b0be9de8e7f2ec611cb900a247c6f531f6b97e393", - "content_hash": "168e96679d789e121586ee8b0be9de8e7f2ec611cb900a247c6f531f6b97e393", - "fingerprint": "e32646bb33972e6d0b21f4b6fd0fa2bfe4b6eab68f71d572be86ea7453630910" + "template_hash": "6664df5be06c16f53d1aee440ad0333a71c62a7f882ec42af3feb8f417da88d3", + "content_hash": "6664df5be06c16f53d1aee440ad0333a71c62a7f882ec42af3feb8f417da88d3", + "fingerprint": "b95e082e11a1f63548d588574319d88f54bd7cadec50efef04eafbe7f7ba495f" } ], "files": [ { "path": ".decapod/managed/specs/README.md", "template_hash": "6957cf384fae78bcc966182ee914b88230c0cbaea909b7b406c53bece8bbfe6c", - "content_hash": "2e243f4f9e5091c421e1abac86f785c9556a6fcbb0c127463eb5b9563c4ddb6c", + "content_hash": "7fe8e1a8b8d420e39b056fd8322b309f9221cf89913c229fbd417dfe338ecc92", "fingerprint": "" }, { "path": ".decapod/managed/specs/INTENT.md", "template_hash": "e6c06430e23ca87ae33169a6b1f9c70a3fd283655f5b5b29755fb83181f989e9", - "content_hash": "9b8c61d3c3fababb257672eb840cda650bd358f52fb1b69833841b1e879e1b5e", + "content_hash": "e2f76d44e62620d870f69362962730aecbdf9308ee5ae5ae79faefacf62ab40d", "fingerprint": "" }, { "path": ".decapod/managed/specs/ARCHITECTURE.md", "template_hash": "58f2abf6f6e0c3da62bc0850e71e12abd76c5064c793701c7b9330036713b163", - "content_hash": "fd0842afe8d4e696f089aef6b2a2f1c89b13778874840f6a803be416db6be194", + "content_hash": "400e3618a6a259acb5a901e884419917bc824eb4414a42021d32c5e55b07ab70", "fingerprint": "" }, { "path": ".decapod/managed/specs/INTERFACES.md", "template_hash": "15cc8c3bbfe951da695384106eb78a44c7af28ddc002bcc83c8a7770229fe999", - "content_hash": "713d3c099f4b3935e54ee6289e9c213df1c48cc763e4218897ce5d4ce6c84f06", + "content_hash": "5b6c409f4c30d5ee4b41d8a59dbe628eeb12e74344b4c510adee28865e3720c7", "fingerprint": "" }, { "path": ".decapod/managed/specs/VALIDATION.md", "template_hash": "8e22417cbcbd602159a516e761bdf78173fc4f27bac6d92d91a4ccb968534395", - "content_hash": "371e4101c7360bdb9cfb13603ead861c74e8786a005ca371fd606d74228ab3d5", + "content_hash": "cf90e3ef0b70ed1a79c3939efec39b901c14ad0c489b01892c0c8f4448abd501", "fingerprint": "" }, { "path": ".decapod/managed/specs/SEMANTICS.md", "template_hash": "3a79850c94b81e29c6462a7ea70d45198252e3eeba2132c6e9206f7b4e4a5829", - "content_hash": "48bafe92adbaa51b304e290ca538d509f2b39f69531e7990003a430a8e364ef5", + "content_hash": "b9fcde0fb285437d54e07dcfa6a9057d88b91a20f7aad2e3d0ce72f5b9279419", "fingerprint": "" }, { "path": ".decapod/managed/specs/OPERATIONS.md", "template_hash": "54ab819ae22bea9f786793a37196749cbacd72c60f58108898e9db044c534acf", - "content_hash": "e579070af84c3a5b8ba5b5bebfd8527782081243f826eddc5a9a162713c7c1be", + "content_hash": "16b9990fcbc18c66ea5344fd60fd0ba12a4e6406afbb9551e96aeccc71a759bb", "fingerprint": "" }, { "path": ".decapod/managed/specs/SECURITY.md", "template_hash": "b283bdc62b7c2fce411becf6b31928d1b3502da28be7ad618531e3e1820fc147", - "content_hash": "47e769e5204a20740344945a46f61f02edbce308023420561152d430a76e15d6", + "content_hash": "ad7b1e9baa4b0312af217f8c754d12f3887a2ac55df452de242a345f911a40b7", "fingerprint": "" } ] diff --git a/.decapod/managed/specs/ARCHITECTURE.md b/.decapod/managed/specs/ARCHITECTURE.md index 3ac64cc2..a7329760 100644 --- a/.decapod/managed/specs/ARCHITECTURE.md +++ b/.decapod/managed/specs/ARCHITECTURE.md @@ -138,7 +138,7 @@ sequenceDiagram ## Codebase Attestation -- Repository signal fingerprint: `2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b` -- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (97 files), `tests/` (4 files) +- Repository signal fingerprint: `c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1` +- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (99 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/INTENT.md b/.decapod/managed/specs/INTENT.md index cd72b5d4..1ab0ec21 100644 --- a/.decapod/managed/specs/INTENT.md +++ b/.decapod/managed/specs/INTENT.md @@ -132,7 +132,7 @@ flowchart LR ## Codebase Attestation -- Repository signal fingerprint: `2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b` -- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (97 files), `tests/` (4 files) +- Repository signal fingerprint: `c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1` +- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (99 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/INTERFACES.md b/.decapod/managed/specs/INTERFACES.md index 02c0360e..f9a546aa 100644 --- a/.decapod/managed/specs/INTERFACES.md +++ b/.decapod/managed/specs/INTERFACES.md @@ -73,7 +73,7 @@ pub enum ApiError { ## Codebase Attestation -- Repository signal fingerprint: `2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b` -- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (97 files), `tests/` (4 files) +- Repository signal fingerprint: `c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1` +- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (99 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/OPERATIONS.md b/.decapod/managed/specs/OPERATIONS.md index 2b28a17b..e4f41db7 100644 --- a/.decapod/managed/specs/OPERATIONS.md +++ b/.decapod/managed/specs/OPERATIONS.md @@ -25,7 +25,14 @@ Describe the operational runtime model, scheduling, and system deployment archit - Triage: - Mitigation: - Communication: -- Post-mortem: +- Post-mortem:## Rollout Strategy +- Blue/green deployment: +- Canary release: +- Rolling update: +- Feature flags:## Capacity Planning +- Traffic patterns: +- Resource utilization: +- Scaling triggers: @@ -64,17 +71,6 @@ Describe the operational runtime model, scheduling, and system deployment archit - Migration health checks and rollback triggers -## Rollout Strategy -- Blue/green deployment: -- Canary release: -- Rolling update: -- Feature flags: - -## Capacity Planning -- Traffic patterns: -- Resource utilization: -- Scaling triggers: - ## Logging Use `tracing` + `tracing-subscriber` with structured JSON output and request correlation ids. @@ -105,7 +101,7 @@ Use `tracing` + `tracing-subscriber` with structured JSON output and request cor ## Codebase Attestation -- Repository signal fingerprint: `2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b` -- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (97 files), `tests/` (4 files) +- Repository signal fingerprint: `c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1` +- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (99 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/README.md b/.decapod/managed/specs/README.md index 58b188eb..cbb86e36 100644 --- a/.decapod/managed/specs/README.md +++ b/.decapod/managed/specs/README.md @@ -47,7 +47,7 @@ These files are the project-local contract for humans and agents. ## Codebase Attestation -- Repository signal fingerprint: `2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b` -- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (97 files), `tests/` (4 files) +- Repository signal fingerprint: `c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1` +- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (99 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/SECURITY.md b/.decapod/managed/specs/SECURITY.md index e1fa861f..f76dbfb5 100644 --- a/.decapod/managed/specs/SECURITY.md +++ b/.decapod/managed/specs/SECURITY.md @@ -83,7 +83,7 @@ Describe the security primitives and security controls implemented in this repos ## Codebase Attestation -- Repository signal fingerprint: `2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b` -- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (97 files), `tests/` (4 files) +- Repository signal fingerprint: `c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1` +- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (99 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/SEMANTICS.md b/.decapod/managed/specs/SEMANTICS.md index 2cd67719..72f4612d 100644 --- a/.decapod/managed/specs/SEMANTICS.md +++ b/.decapod/managed/specs/SEMANTICS.md @@ -23,7 +23,13 @@ stateDiagram-v2 - Replay order: - Conflict resolution: - Snapshot cadence: -- Determinism proof strategy: +- Determinism proof strategy:## Error Code Semantics +- Namespace: +- Stable compatibility window: +- Mapping to retry/degrade behavior:## Domain Rules +- Business rule 1: +- Business rule 2: +- Business rule 3: @@ -65,16 +71,6 @@ stateDiagram-v2 - Recovery test cadence MUST be selected for the project and recorded as a proof obligation -## Error Code Semantics -- Namespace: -- Stable compatibility window: -- Mapping to retry/degrade behavior: - -## Domain Rules -- Business rule 1: -- Business rule 2: -- Business rule 3: - ## Idempotency Contracts | Operation | Idempotency Key | Duplicate Behavior | |---|---|---| @@ -87,7 +83,7 @@ stateDiagram-v2 ## Codebase Attestation -- Repository signal fingerprint: `2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b` -- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (97 files), `tests/` (4 files) +- Repository signal fingerprint: `c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1` +- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (99 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/VALIDATION.md b/.decapod/managed/specs/VALIDATION.md index 752c0ae2..21747591 100644 --- a/.decapod/managed/specs/VALIDATION.md +++ b/.decapod/managed/specs/VALIDATION.md @@ -31,7 +31,19 @@ flowchart TD V --> P{All blocking gates pass?} P -->|No| F4[Fail: promotion blocked] P -->|Yes| E[Emit promotion evidence] -``` +```## Promotion Flow +```mermaid +flowchart LR + A[Plan] --> B[Implement] + B --> C[Test] + C --> D[Validate] + D --> E[Assemble Evidence] + E --> F[Promote] +```## Proof Surfaces +- `decapod validate` +- Required test commands: +- `cargo test` +- Required integration/e2e commands: @@ -73,22 +85,6 @@ flowchart TD - Data integrity validation after recovery -## Promotion Flow -```mermaid -flowchart LR - A[Plan] --> B[Implement] - B --> C[Test] - C --> D[Validate] - D --> E[Assemble Evidence] - E --> F[Promote] -``` - -## Proof Surfaces -- `decapod validate` -- Required test commands: -- `cargo test` -- Required integration/e2e commands: - ## Promotion Gates ## Blocking Gates @@ -134,7 +130,7 @@ flowchart LR ## Codebase Attestation -- Repository signal fingerprint: `2922f42f59ddf6cd3ce5e4e3970c58caa8900a555733ec86f7a2c3d81f87a75b` -- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (97 files), `tests/` (4 files) +- Repository signal fingerprint: `c95d945d65177ab861f944f7c022b996ab58002f3b464fa0c0f625d8456a0ff1` +- Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (99 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/AGENTS.md b/AGENTS.md index 5895e0a2..5ab034cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,5 @@ - - + + # AGENTS.md — Universal Agent Contract This is a Decapod-managed repository. **Strict Dependency: You are strictly bound to the Decapod governance kernel.** diff --git a/CLAUDE.md b/CLAUDE.md index 94690a66..53eb873a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,5 @@ - - + + # CLAUDE.md - Agent Entrypoint You are working in a Decapod-managed repository. diff --git a/CODEX.md b/CODEX.md index 4cb18ba4..4f48499f 100644 --- a/CODEX.md +++ b/CODEX.md @@ -1,5 +1,5 @@ - - + + # CODEX.md - Agent Entrypoint You are working in a Decapod-managed repository. diff --git a/GEMINI.md b/GEMINI.md index 4cf47a09..9f42fd30 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,5 +1,5 @@ - - + + # GEMINI.md - Agent Entrypoint You are working in a Decapod-managed repository. diff --git a/src/decapod/cli.rs b/src/decapod/cli.rs index a244c3d9..55a16d42 100644 --- a/src/decapod/cli.rs +++ b/src/decapod/cli.rs @@ -800,6 +800,9 @@ pub(crate) enum ArtifactsCommand { /// Create the claims ledger template when claims.json is absent #[clap(long)] repair: bool, + /// Append an issue-scoped note to the claims change policy atomically + #[clap(long)] + claims_note: Option, }, } diff --git a/src/decapod/core/atomic.rs b/src/decapod/core/atomic.rs new file mode 100644 index 00000000..e8c6e99f --- /dev/null +++ b/src/decapod/core/atomic.rs @@ -0,0 +1,34 @@ +//! Small filesystem primitives for durable governance artifacts. + +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Replace a file without exposing a partially-written JSON artifact. +pub fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> { + let parent = path.parent().unwrap_or_else(|| Path::new(".")); + fs::create_dir_all(parent)?; + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("artifact"); + let temporary = parent.join(format!(".{file_name}.tmp-{}-{nonce}", std::process::id())); + let result = (|| { + let mut file = OpenOptions::new() + .create_new(true) + .write(true) + .open(&temporary)?; + file.write_all(bytes)?; + file.sync_all()?; + fs::rename(&temporary, path) + })(); + if result.is_err() { + let _ = fs::remove_file(&temporary); + } + result +} diff --git a/src/decapod/core/dirty_classification.rs b/src/decapod/core/dirty_classification.rs new file mode 100644 index 00000000..24408324 --- /dev/null +++ b/src/decapod/core/dirty_classification.rs @@ -0,0 +1,170 @@ +//! Deterministic classification of repository modifications. + +use serde::Serialize; +use std::collections::BTreeMap; +use std::path::Path; +use std::process::Command; + +pub const DIRTY_SCHEMA_VERSION: &str = "1.0.0"; + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "snake_case")] +pub enum DirtyFileClass { + UserAuthored, + GovernanceTracked, + DeterministicProjection, + RuntimeEphemeral, + BackupTemporary, + PreExistingUnrelated, + Unknown, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct DirtyFile { + pub path: String, + pub status: String, + pub class: DirtyFileClass, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct DirtyGroup { + pub class: DirtyFileClass, + pub count: usize, + pub files: Vec, + pub limit: Option, + pub state: String, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct DirtyClassification { + pub schema_version: String, + pub kind: String, + pub files: Vec, + pub groups: Vec, + pub blocked: bool, + pub blocker_classes: Vec, +} + +pub fn classify( + repo_root: &Path, + max_user_authored: usize, +) -> Result { + classify_with_pre_existing(repo_root, max_user_authored, &[]) +} + +pub fn classify_with_pre_existing( + repo_root: &Path, + max_user_authored: usize, + pre_existing: &[String], +) -> Result { + let output = Command::new("git") + .current_dir(repo_root) + .args(["status", "--porcelain=v1", "--untracked-files=all"]) + .output()?; + if !output.status.success() { + return Err(std::io::Error::other("git status --porcelain=v1 failed")); + } + let mut files = String::from_utf8_lossy(&output.stdout) + .lines() + .filter_map(parse_status_line) + .map(|(status, path)| DirtyFile { + class: classify_path(&path, pre_existing), + path, + status, + }) + .collect::>(); + files.sort_by(|left, right| { + left.path + .cmp(&right.path) + .then(left.status.cmp(&right.status)) + }); + + let mut grouped = BTreeMap::>::new(); + for file in &files { + grouped + .entry(file.class) + .or_default() + .push(file.path.clone()); + } + let mut groups = grouped + .into_iter() + .map(|(class, mut paths)| { + paths.sort(); + let limit = (class == DirtyFileClass::UserAuthored).then_some(max_user_authored); + let state = match class { + DirtyFileClass::UserAuthored if paths.len() > max_user_authored => "blocked", + DirtyFileClass::Unknown => "blocked", + DirtyFileClass::GovernanceTracked + | DirtyFileClass::DeterministicProjection + | DirtyFileClass::RuntimeEphemeral + | DirtyFileClass::BackupTemporary + | DirtyFileClass::PreExistingUnrelated => "ignored", + DirtyFileClass::UserAuthored => "within_limit", + }; + DirtyGroup { + class, + count: paths.len(), + files: paths, + limit, + state: state.to_string(), + } + }) + .collect::>(); + groups.sort_by_key(|group| group.class); + let blocker_classes = groups + .iter() + .filter(|group| group.state == "blocked") + .map(|group| group.class) + .collect::>(); + Ok(DirtyClassification { + schema_version: DIRTY_SCHEMA_VERSION.to_string(), + kind: "dirty_file_classification".to_string(), + files, + blocked: !blocker_classes.is_empty(), + blocker_classes, + groups, + }) +} + +pub fn classify_path(path: &str, pre_existing: &[String]) -> DirtyFileClass { + if pre_existing.iter().any(|candidate| candidate == path) { + return DirtyFileClass::PreExistingUnrelated; + } + if path.ends_with(".before_revert") + || path.ends_with(".bak") + || path.ends_with(".tmp") + || path.contains("/.tmp-") + { + return DirtyFileClass::BackupTemporary; + } + if path.starts_with(".decapod/governance/") { + return DirtyFileClass::GovernanceTracked; + } + if path.starts_with(".decapod/managed/") || path.starts_with(".decapod/generated/") { + return DirtyFileClass::DeterministicProjection; + } + if path.starts_with(".decapod/data/") || path.starts_with(".decapod/workspaces/") { + return DirtyFileClass::RuntimeEphemeral; + } + if path.starts_with(".decapod/") { + if path == ".decapod/config.toml" || path == ".decapod/OVERRIDE.md" { + return DirtyFileClass::UserAuthored; + } + return DirtyFileClass::Unknown; + } + DirtyFileClass::UserAuthored +} + +fn parse_status_line(line: &str) -> Option<(String, String)> { + if line.len() < 4 { + return None; + } + let status = line[..2].to_string(); + let raw_path = line[3..].trim(); + let path = raw_path + .rsplit_once(" -> ") + .map(|(_, new)| new) + .unwrap_or(raw_path); + let path = path.trim_matches('"'); + (!path.is_empty()).then(|| (status, path.to_string())) +} diff --git a/src/decapod/core/entrypoint_integrity.rs b/src/decapod/core/entrypoint_integrity.rs index a8769fe3..cdf4354f 100644 --- a/src/decapod/core/entrypoint_integrity.rs +++ b/src/decapod/core/entrypoint_integrity.rs @@ -24,25 +24,25 @@ pub struct EntrypointExpectation { pub fingerprint: &'static str, } -// These values are the v0.86.2 release manifest. Keep them immutable for the +// These values are the v0.87.0 release manifest. Keep them immutable for the // lifetime of that release; a later release must update them deliberately and // regenerate the four root entrypoints through Decapod. pub const EXPECTED_ENTRYPOINTS: [EntrypointExpectation; 4] = [ EntrypointExpectation { surface: "AGENTS.md", - fingerprint: "776d7696f5579c5188091323d94fcb1fff0a1f3cb9cf692d00de934ef06c1915", + fingerprint: "88c4330305ab1dcc2d1ade14f1244d7172dead921b5a3ea62ec8fc4f44430389", }, EntrypointExpectation { surface: "CLAUDE.md", - fingerprint: "ee5ad891871fc9e76265288fd2fe90c39b8bef52e7732d171953b705edb8b90d", + fingerprint: "4154e046dff7389d6e1a3bafc32f0b9bae24121cbe0c505300b9999d97175a8f", }, EntrypointExpectation { surface: "GEMINI.md", - fingerprint: "777afd971370e9116098c70836946a05fab951412600e7da718ae1e7e5539c34", + fingerprint: "7e841746cc64c02324864e784d822ef4d6b5bf12a891c65c5141b08ef989b4d0", }, EntrypointExpectation { surface: "CODEX.md", - fingerprint: "e32646bb33972e6d0b21f4b6fd0fa2bfe4b6eab68f71d572be86ea7453630910", + fingerprint: "b95e082e11a1f63548d588574319d88f54bd7cadec50efef04eafbe7f7ba495f", }, ]; diff --git a/src/decapod/core/governance_artifacts.rs b/src/decapod/core/governance_artifacts.rs index 621ee480..b370c1ac 100644 --- a/src/decapod/core/governance_artifacts.rs +++ b/src/decapod/core/governance_artifacts.rs @@ -1,18 +1,40 @@ -//! Agent-facing inventory and repair for required publication artifacts. +//! Agent-facing inventory and integrity report for required publication artifacts. //! -//! This surface deliberately distinguishes the repository research claims -//! ledger from Health Engine claims stored in `health.db`. +//! This is the authoritative read-only substrate consumed by later readiness +//! diagnostics. It deliberately keeps repository research claims separate from +//! Health Engine claims stored in `health.db`. -use crate::core::{research_claims, trajectory, validate}; +use crate::core::{dirty_classification, research_claims, trajectory, validate}; use crate::plan_governance; use serde::Serialize; +use sha2::{Digest, Sha256}; use std::collections::BTreeSet; use std::path::Path; use std::process::Command; -pub const INVENTORY_SCHEMA_VERSION: &str = "1.0.0"; +pub const INVENTORY_SCHEMA_VERSION: &str = "1.1.0"; pub const INVENTORY_COMMAND: &str = "decapod govern artifacts inventory"; +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum WorkspaceTargetState { + Identical, + WorkspaceOnly, + TargetOnly, + Divergent, + Missing, + Unknown, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum SemanticFreshness { + Current, + Stale, + Invalid, + Unknown, +} + #[derive(Debug, Clone, Serialize)] pub struct GovernanceArtifactEntry { pub path: String, @@ -21,6 +43,12 @@ pub struct GovernanceArtifactEntry { pub valid: bool, pub staged: bool, pub in_pr_diff: bool, + pub workspace_sha: Option, + pub target_sha: Option, + pub workspace_target_state: WorkspaceTargetState, + pub semantic_freshness: SemanticFreshness, + pub freshness_reasons: Vec, + pub schema_error: Option, pub remediation: String, } @@ -34,18 +62,40 @@ pub struct GovernanceArtifactInventory { pub all_valid: bool, pub all_staged: bool, pub all_in_pr_diff: bool, + pub all_semantically_current: bool, + pub workspace_branch: Option, + pub workspace_sha: Option, + pub dirty: dirty_classification::DirtyClassification, pub claims_source: String, pub repair_command: String, } +struct ArtifactValidity { + valid: bool, + semantic_freshness: SemanticFreshness, + schema_error: Option, +} + pub fn inventory( repo_root: &Path, base_branch: Option<&str>, repair: bool, +) -> Result { + inventory_with_claims_note(repo_root, base_branch, repair, None) +} + +pub fn inventory_with_claims_note( + repo_root: &Path, + base_branch: Option<&str>, + repair: bool, + claims_note: Option<&str>, ) -> Result { if repair { let _ = research_claims::ensure_template(repo_root, false)?; } + if let Some(note) = claims_note { + let _ = research_claims::append_change_note(repo_root, note)?; + } let base_ref = resolve_base_ref(repo_root, base_branch); let pr_paths = base_ref @@ -60,12 +110,33 @@ pub fn inventory( .unwrap_or_default(); let staged_paths = git_paths(repo_root, &["diff", "--cached", "--name-only"])?; + let plan_result = plan_governance::load_plan(repo_root); + let claims_result = research_claims::load_and_validate(repo_root); + let trajectory_result = trajectory::load_trajectory_cookie(repo_root); + let receipt_result = load_validation_receipt(repo_root); + let plan = plan_result.as_ref().ok().and_then(Option::as_ref); + let claims = claims_result.as_ref().ok().and_then(Option::as_ref); + let trajectory = trajectory_result.as_ref().ok().and_then(Option::as_ref); + let receipt = receipt_result.as_ref().ok().and_then(Option::as_ref); + let subject_freshness = subject_freshness(repo_root, plan, trajectory); + let receipt_freshness = receipt_freshness(receipt, trajectory); + let receipt_chain_valid = receipt.is_some_and(|item| { + item.validate_integrity().is_ok() + && trajectory.is_some_and(|run| { + item.trajectory_artifact_hash.as_deref() == Some(run.artifact_hash.as_str()) + }) + }); + let mut artifacts = vec![ entry( repo_root, plan_governance::PLAN_PATH, "governed intent and phase plan", - plan_governance::load_plan(repo_root)?.is_some(), + ArtifactValidity { + valid: plan.is_some(), + semantic_freshness: subject_freshness, + schema_error: plan_result.as_ref().err().map(ToString::to_string), + }, &staged_paths, &pr_paths, ), @@ -73,7 +144,15 @@ pub fn inventory( repo_root, research_claims::CLAIMS_PATH, "repository research claims ledger; distinct from Health Engine claims in health.db", - research_claims::load_and_validate(repo_root)?.is_some(), + ArtifactValidity { + valid: claims.is_some(), + semantic_freshness: if claims.is_some() { + SemanticFreshness::Current + } else { + SemanticFreshness::Invalid + }, + schema_error: claims_result.as_ref().err().map(ToString::to_string), + }, &staged_paths, &pr_paths, ), @@ -81,15 +160,23 @@ pub fn inventory( repo_root, trajectory::TRAJECTORY_PATH, "agent-run trajectory cookie and proof evidence", - trajectory::load_trajectory_cookie(repo_root)?.is_some(), + ArtifactValidity { + valid: trajectory.is_some(), + semantic_freshness: subject_freshness, + schema_error: trajectory_result.as_ref().err().map(ToString::to_string), + }, &staged_paths, &pr_paths, ), entry( repo_root, validate::VALIDATION_RECEIPT_PATH, - "successful Decapod validation receipt", - validation_receipt_is_valid(repo_root)?, + "successful Decapod validation receipt bound to the current trajectory", + ArtifactValidity { + valid: receipt_chain_valid, + semantic_freshness: receipt_freshness, + schema_error: receipt_result.as_ref().err().map(ToString::to_string), + }, &staged_paths, &pr_paths, ), @@ -100,6 +187,9 @@ pub fn inventory( let all_valid = artifacts.iter().all(|item| item.valid); let all_staged = artifacts.iter().all(|item| item.staged); let all_in_pr_diff = base_ref.is_some() && artifacts.iter().all(|item| item.in_pr_diff); + let all_semantically_current = artifacts + .iter() + .all(|item| item.semantic_freshness == SemanticFreshness::Current); Ok(GovernanceArtifactInventory { schema_version: INVENTORY_SCHEMA_VERSION.to_string(), kind: "governance_artifact_inventory".to_string(), @@ -109,7 +199,12 @@ pub fn inventory( all_valid, all_staged, all_in_pr_diff, - claims_source: "repository research ledger at .decapod/governance/claims.json; Health Engine claims remain in .decapod/data/health.db".to_string(), + all_semantically_current, + workspace_branch: current_branch(repo_root), + workspace_sha: current_revision(repo_root), + dirty: dirty_classification::classify(repo_root, commit_often_limit()) + .map_err(crate::core::error::DecapodError::IoError)?, + claims_source: ".decapod/governance/claims.json; Health Engine claims remain in .decapod/data/health.db".to_string(), repair_command: format!("{INVENTORY_COMMAND} --repair"), }) } @@ -118,8 +213,9 @@ pub fn run_inventory( repo_root: &Path, base_branch: Option<&str>, repair: bool, + claims_note: Option<&str>, ) -> Result<(), crate::core::error::DecapodError> { - let report = inventory(repo_root, base_branch, repair)?; + let report = inventory_with_claims_note(repo_root, base_branch, repair, claims_note)?; println!( "{}", serde_json::to_string_pretty(&report).map_err(|error| { @@ -128,9 +224,13 @@ pub fn run_inventory( )) })? ); - if !report.all_present || !report.all_valid || !report.all_in_pr_diff { + if !report.all_present + || !report.all_valid + || !report.all_in_pr_diff + || !report.all_semantically_current + { return Err(crate::core::error::DecapodError::ValidationError(format!( - "governance artifact inventory is incomplete; run `{INVENTORY_COMMAND} --repair`, then stage all four artifacts and rerun with `--base-branch `." + "governance artifact inventory is incomplete or stale; run `{INVENTORY_COMMAND} --repair`, then stage all four artifacts and rerun with `--base-branch `." ))); } Ok(()) @@ -140,18 +240,53 @@ fn entry( repo_root: &Path, path: &str, role: &str, - valid: bool, + validity: ArtifactValidity, staged_paths: &BTreeSet, pr_paths: &BTreeSet, ) -> GovernanceArtifactEntry { let present = repo_root.join(path).is_file(); + let workspace_sha = present.then(|| file_sha(&repo_root.join(path))); + let target_sha = git_blob_sha(repo_root, "HEAD", path); + let workspace_target_state = match (workspace_sha.as_deref(), target_sha.as_deref()) { + (Some(workspace), Some(target)) if workspace == target => WorkspaceTargetState::Identical, + (Some(_), Some(_)) => WorkspaceTargetState::Divergent, + (Some(_), None) => WorkspaceTargetState::WorkspaceOnly, + (None, Some(_)) => WorkspaceTargetState::TargetOnly, + (None, None) => WorkspaceTargetState::Missing, + }; + let mut freshness_reasons = Vec::new(); + if !validity.valid { + freshness_reasons.push("schema_or_integrity_invalid".to_string()); + } + if !staged_paths.contains(path) { + freshness_reasons.push("not_staged".to_string()); + } + if !pr_paths.contains(path) { + freshness_reasons.push("not_in_target_diff".to_string()); + } + if workspace_target_state != WorkspaceTargetState::Identical { + freshness_reasons.push("workspace_differs_from_target".to_string()); + } + if path == validate::VALIDATION_RECEIPT_PATH { + freshness_reasons.push("receipt_must_bind_current_trajectory".to_string()); + } GovernanceArtifactEntry { path: path.to_string(), role: role.to_string(), present, - valid: present && valid, + valid: present && validity.valid, staged: staged_paths.contains(path), in_pr_diff: pr_paths.contains(path), + workspace_sha, + target_sha, + workspace_target_state, + semantic_freshness: if !present { + SemanticFreshness::Unknown + } else { + validity.semantic_freshness + }, + freshness_reasons, + schema_error: validity.schema_error, remediation: if path == research_claims::CLAIMS_PATH { format!("Run `{INVENTORY_COMMAND} --repair`; existing claims content is preserved.") } else { @@ -160,18 +295,107 @@ fn entry( } } -fn validation_receipt_is_valid(repo_root: &Path) -> Result { +fn load_validation_receipt( + repo_root: &Path, +) -> Result, String> { let path = repo_root.join(validate::VALIDATION_RECEIPT_PATH); if !path.is_file() { - return Ok(false); + return Ok(None); + } + let raw = std::fs::read_to_string(path).map_err(|error| error.to_string())?; + serde_json::from_str(&raw) + .map(Some) + .map_err(|error| format!("invalid validation receipt: {error}")) +} + +fn commit_often_limit() -> usize { + std::env::var("DECAPOD_COMMIT_OFTEN_MAX_DIRTY_FILES") + .ok() + .and_then(|value| value.parse().ok()) + .filter(|value| *value > 0) + .unwrap_or(6) +} + +fn subject_freshness( + repo_root: &Path, + plan: Option<&plan_governance::GovernedPlan>, + trajectory: Option<&trajectory::TrajectoryArtifact>, +) -> SemanticFreshness { + let (Some(plan), Some(trajectory), Some(branch)) = + (plan, trajectory, current_branch(repo_root)) + else { + return SemanticFreshness::Unknown; + }; + let Some(task_id) = trajectory.task_id.as_deref() else { + return SemanticFreshness::Unknown; + }; + if plan.todo_ids.iter().any(|todo| todo == task_id) && branch.contains(task_id) { + SemanticFreshness::Current + } else { + SemanticFreshness::Stale + } +} + +fn receipt_freshness( + receipt: Option<&validate::ValidationReceipt>, + trajectory: Option<&trajectory::TrajectoryArtifact>, +) -> SemanticFreshness { + let (Some(receipt), Some(trajectory)) = (receipt, trajectory) else { + return SemanticFreshness::Unknown; + }; + if receipt.validate_integrity().is_err() + || receipt.trajectory_artifact_hash.as_deref() != Some(trajectory.artifact_hash.as_str()) + { + SemanticFreshness::Stale + } else { + SemanticFreshness::Current + } +} + +fn current_branch(repo_root: &Path) -> Option { + git_text(repo_root, &["symbolic-ref", "--quiet", "--short", "HEAD"]) +} + +fn current_revision(repo_root: &Path) -> Option { + git_text(repo_root, &["rev-parse", "HEAD"]) +} + +fn git_text(repo_root: &Path, args: &[&str]) -> Option { + let output = Command::new("git") + .current_dir(repo_root) + .args(args) + .output() + .ok()?; + output + .status + .success() + .then(|| String::from_utf8_lossy(&output.stdout).trim().to_string()) + .filter(|value| !value.is_empty()) +} + +fn git_blob_sha(repo_root: &Path, reference: &str, path: &str) -> Option { + let spec = format!("{reference}:{path}"); + let output = Command::new("git") + .current_dir(repo_root) + .args(["rev-parse", &spec]) + .output() + .ok()?; + output + .status + .success() + .then(|| String::from_utf8_lossy(&output.stdout).trim().to_string()) + .filter(|value| !value.is_empty()) +} + +fn file_sha(path: &Path) -> String { + match std::fs::read(path) { + Ok(bytes) => { + let mut hasher = Sha256::new(); + hasher.update(bytes); + format!("sha256:{:x}", hasher.finalize()) + } + Err(_) => "unavailable".to_string(), } - let raw = std::fs::read_to_string(path).map_err(crate::core::error::DecapodError::IoError)?; - let receipt: validate::ValidationReceipt = serde_json::from_str(&raw).map_err(|error| { - crate::core::error::DecapodError::ValidationError(format!( - "invalid validation receipt: {error}" - )) - })?; - Ok(receipt.validate_integrity().is_ok()) } fn resolve_base_ref(repo_root: &Path, requested: Option<&str>) -> Option { diff --git a/src/decapod/core/mod.rs b/src/decapod/core/mod.rs index a0cfef23..405a51fa 100644 --- a/src/decapod/core/mod.rs +++ b/src/decapod/core/mod.rs @@ -7,6 +7,7 @@ pub mod agent_eval; pub mod ansi; pub mod assets; pub mod assurance; +pub mod atomic; pub mod auth; pub mod broker; pub mod capabilities; @@ -21,6 +22,7 @@ pub mod context_capsule; pub mod coplayer; pub mod custody; pub mod db; +pub mod dirty_classification; pub mod docs; pub mod docs_cli; pub mod entrypoint_integrity; diff --git a/src/decapod/core/research_claims.rs b/src/decapod/core/research_claims.rs index f439628c..872f46d5 100644 --- a/src/decapod/core/research_claims.rs +++ b/src/decapod/core/research_claims.rs @@ -4,6 +4,7 @@ //! Schema is the machine-readable public contract; these Rust types provide //! the executable semantic checks used by validation. +use crate::core::atomic; use crate::core::error::DecapodError; use serde::Deserialize; use serde_json::Value; @@ -406,6 +407,68 @@ pub fn ensure_template(repo_root: &Path, dry_run: bool) -> Result Result { + let note = note.trim(); + if note.is_empty() { + return Err(DecapodError::ValidationError( + "claims note must not be empty".to_string(), + )); + } + let path = repo_root.join(CLAIMS_PATH); + let raw = fs::read_to_string(&path).map_err(DecapodError::IoError)?; + let ledger = load_and_validate(repo_root)?.ok_or_else(|| { + DecapodError::ValidationError(format!("claims ledger is missing: {}", path.display())) + })?; + if ledger.authority.change_policy.contains(note) { + return Ok(false); + } + let marker = "\"change_policy\":"; + let marker_start = raw.find(marker).ok_or_else(|| { + DecapodError::ValidationError( + "research claims ledger authority.change_policy is missing".to_string(), + ) + })?; + let line_end = raw[marker_start..] + .find('\n') + .map_or(raw.len(), |index| marker_start + index); + let colon = raw[marker_start..line_end] + .find(':') + .map_or(marker_start, |index| marker_start + index); + let value_start = raw[colon + 1..line_end] + .find('"') + .map_or(line_end, |index| colon + 1 + index); + let value_end = raw[..line_end].rfind('"').unwrap_or(value_start); + let encoded_note = serde_json::to_string(note).map_err(|error| { + DecapodError::ValidationError(format!("claims note serialization failed: {error}")) + })?; + let encoded_policy = serde_json::to_string(&format!( + "{} {}", + ledger.authority.change_policy, + encoded_note.trim_matches('"') + )) + .map_err(|error| { + DecapodError::ValidationError(format!("claims policy serialization failed: {error}")) + })?; + let updated = format!( + "{}{}{}{}", + &raw[..value_start], + encoded_policy, + &raw[value_end + 1..line_end], + &raw[line_end..] + ); + atomic::write_atomic(&path, updated.as_bytes()).map_err(DecapodError::IoError)?; + load_and_validate(repo_root)?.ok_or_else(|| { + DecapodError::ValidationError(format!( + "claims ledger was updated but could not be loaded: {}", + path.display() + )) + })?; + Ok(true) +} + pub fn validate_schema_document() -> Result<(), DecapodError> { let schema: Value = serde_json::from_str(CLAIMS_SCHEMA_DOCUMENT).map_err(|error| { DecapodError::ValidationError(format!( diff --git a/src/decapod/core/trajectory.rs b/src/decapod/core/trajectory.rs index 12d9994e..d8874c7b 100644 --- a/src/decapod/core/trajectory.rs +++ b/src/decapod/core/trajectory.rs @@ -496,7 +496,7 @@ pub fn write_trajectory( "failed to serialize trajectory artifact: {e}" )) })?; - fs::write(&path, bytes).map_err(error::DecapodError::IoError)?; + crate::core::atomic::write_atomic(&path, &bytes).map_err(error::DecapodError::IoError)?; Ok(canonical) } diff --git a/src/decapod/core/validate.rs b/src/decapod/core/validate.rs index 8d177f1b..3bf64496 100644 --- a/src/decapod/core/validate.rs +++ b/src/decapod/core/validate.rs @@ -4267,27 +4267,57 @@ fn validate_commit_often_gate( return Ok(()); } - let dirty_count = String::from_utf8_lossy(&status_output.stdout) - .lines() - .filter(|line| !line.trim().is_empty()) - .count(); - - if dirty_count == 0 { + let classification = + match crate::core::dirty_classification::classify(repo_root, max_dirty_files) { + Ok(report) => report, + Err(error) => { + warn( + &format!("Commit-often gate skipped: unable to classify git status: {error}"), + ctx, + ); + return Ok(()); + } + }; + if classification.files.is_empty() { pass("Commit-often gate: working tree is clean", ctx); return Ok(()); } - if dirty_count > max_dirty_files { - fail( - &format!( - "Commit-often mandate violation: {dirty_count} dirty file(s) exceed limit {max_dirty_files}. Commit incremental changes before continuing." - ), - ctx, - ); + let user_count = classification + .groups + .iter() + .find(|group| { + group.class == crate::core::dirty_classification::DirtyFileClass::UserAuthored + }) + .map(|group| group.count) + .unwrap_or(0); + if classification.blocked { + let unknown = classification + .groups + .iter() + .find(|group| group.class == crate::core::dirty_classification::DirtyFileClass::Unknown) + .map(|group| group.count) + .unwrap_or(0); + if unknown > 0 { + fail( + &format!( + "Commit-often gate blocked: {unknown} unclassified Decapod file(s); classify or remove them before continuing." + ), + ctx, + ); + } else { + fail( + &format!( + "Commit-often mandate violation: {user_count} user-authored dirty file(s) exceed limit {max_dirty_files}. Commit incremental changes before continuing." + ), + ctx, + ); + } } else { + let ignored = classification.files.len().saturating_sub(user_count); pass( &format!( - "Commit-often gate: {dirty_count} dirty file(s) within limit {max_dirty_files}" + "Commit-often gate: {user_count} user-authored dirty file(s) within limit {max_dirty_files}; {ignored} governed/ephemeral file(s) classified" ), ctx, ); diff --git a/src/decapod/lib.rs b/src/decapod/lib.rs index 900db10b..6e95cf83 100644 --- a/src/decapod/lib.rs +++ b/src/decapod/lib.rs @@ -5943,7 +5943,7 @@ fn write_validation_receipt( "validation receipt serialization failed: {e}" )) })?; - fs::write(&path, bytes).map_err(error::DecapodError::IoError)?; + core::atomic::write_atomic(&path, &bytes).map_err(error::DecapodError::IoError)?; Ok(path) } @@ -6124,10 +6124,12 @@ fn run_govern_command( ArtifactsCommand::Inventory { base_branch, repair, + claims_note, } => core::governance_artifacts::run_inventory( workspace_root, base_branch.as_deref(), repair, + claims_note.as_deref(), )?, }, GovernCommand::Plan(plan_cli) => run_plan_command(plan_cli, project_store, workspace_root)?, diff --git a/tests/governance_artifacts.rs b/tests/governance_artifacts.rs index 66402822..1e529845 100644 --- a/tests/governance_artifacts.rs +++ b/tests/governance_artifacts.rs @@ -49,6 +49,26 @@ fn proof_init_creates_and_refresh_preserves_claims_ledger() { ); } +#[test] +fn claims_note_update_is_atomic_semantic_and_idempotent() { + let temp = TempDir::new().expect("tempdir"); + let init = run_decapod( + temp.path(), + &["init", "--proof", "--no-container-workspaces"], + ); + assert!(init.status.success(), "initialization failed"); + let claims_path = temp.path().join(research_claims::CLAIMS_PATH); + let original = fs::read_to_string(&claims_path).expect("claims template"); + let note = "Hermes governance substrate test note."; + assert!(research_claims::append_change_note(temp.path(), note).expect("append note")); + assert!(!research_claims::append_change_note(temp.path(), note).expect("idempotent note")); + let updated = fs::read_to_string(&claims_path).expect("updated claims"); + assert!(updated.contains(note)); + assert_eq!(updated.matches(note).count(), 1); + assert_eq!(updated.lines().count(), original.lines().count()); + research_claims::load_and_validate(temp.path()).expect("updated claims remain valid"); +} + #[test] fn inventory_distinguishes_health_claims_and_reports_pr_diff() { let temp = TempDir::new().expect("tempdir"); diff --git a/tests/hermes_governance_substrate.rs b/tests/hermes_governance_substrate.rs new file mode 100644 index 00000000..5cd1a180 --- /dev/null +++ b/tests/hermes_governance_substrate.rs @@ -0,0 +1,165 @@ +use decapod::core::atomic; +use decapod::core::dirty_classification::{self, DirtyFileClass}; +use decapod::core::governance_artifacts::{self, WorkspaceTargetState}; +use decapod::core::workspace; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use tempfile::TempDir; + +fn git(dir: &Path, args: &[&str]) { + let output = Command::new("git") + .current_dir(dir) + .args(args) + .output() + .expect("git command"); + assert!( + output.status.success(), + "git {:?} failed: {}", + args, + String::from_utf8_lossy(&output.stderr) + ); +} + +fn repo() -> (TempDir, PathBuf) { + let temp = TempDir::new().expect("tempdir"); + let dir = temp.path().to_path_buf(); + git(&dir, &["init", "-b", "master"]); + git(&dir, &["config", "user.name", "Hermes Test"]); + git(&dir, &["config", "user.email", "hermes@example.test"]); + (temp, dir) +} + +#[test] +fn dirty_classification_is_deterministic_and_groups_governance_files() { + let (_temp, dir) = repo(); + fs::create_dir_all(dir.join(".decapod/governance")).expect("governance directory"); + fs::create_dir_all(dir.join(".decapod/managed/specs")).expect("spec directory"); + fs::create_dir_all(dir.join(".decapod/data/sessions")).expect("session directory"); + fs::write(dir.join("user.rs"), "fn main() {}\n").expect("user file"); + fs::write(dir.join(".decapod/governance/plan.json"), "{}\n").expect("plan"); + fs::write(dir.join(".decapod/managed/specs/INTENT.md"), "projection\n").expect("projection"); + fs::write( + dir.join(".decapod/data/sessions/runtime.json"), + "ephemeral\n", + ) + .expect("session"); + + let report = dirty_classification::classify(&dir, 1).expect("classification"); + assert!( + !report.blocked, + "classified files should not block the user limit" + ); + assert_eq!(report.files[0].path, ".decapod/data/sessions/runtime.json"); + assert_eq!( + dirty_classification::classify_path(".decapod/governance/plan.json", &[]), + DirtyFileClass::GovernanceTracked + ); + assert_eq!( + dirty_classification::classify_path(".decapod/managed/specs/INTENT.md", &[]), + DirtyFileClass::DeterministicProjection + ); + assert_eq!( + dirty_classification::classify_path(".decapod/unknown.json", &[]), + DirtyFileClass::Unknown + ); + let first = serde_json::to_string(&report).expect("stable JSON"); + let second = serde_json::to_string(&dirty_classification::classify(&dir, 1).unwrap()) + .expect("stable JSON"); + assert_eq!(first, second); +} + +#[test] +fn clean_and_pre_existing_classification_are_explicit() { + let (_temp, dir) = repo(); + let clean = dirty_classification::classify(&dir, 6).expect("clean classification"); + assert!(clean.files.is_empty()); + assert!(!clean.blocked); + fs::write(dir.join("old.txt"), "pre-existing\n").expect("old file"); + let report = + dirty_classification::classify_with_pre_existing(&dir, 0, &["old.txt".to_string()]) + .expect("pre-existing classification"); + assert_eq!(report.files[0].class, DirtyFileClass::PreExistingUnrelated); + assert!(!report.blocked); +} + +#[test] +fn inventory_reports_workspace_target_divergence_missing_base_and_does_not_mutate() { + let (_temp, dir) = repo(); + fs::create_dir_all(dir.join(".decapod/governance")).expect("governance"); + fs::write( + dir.join(".decapod/governance/plan.json"), + "{\n \"schema_version\": \"1.0.0\"\n}\n", + ) + .expect("plan"); + git(&dir, &["add", "."]); + git(&dir, &["commit", "-m", "fixture"]); + fs::write( + dir.join(".decapod/governance/plan.json"), + "{\n \"schema_version\": \"1.0.0\",\n \"title\": \"changed\"\n}\n", + ) + .expect("divergent plan"); + let before = Command::new("git") + .current_dir(&dir) + .args(["status", "--porcelain"]) + .output() + .expect("status") + .stdout; + let report = governance_artifacts::inventory(&dir, Some("missing-target"), false) + .expect("read-only inventory"); + let plan = report + .artifacts + .iter() + .find(|artifact| artifact.path == ".decapod/governance/plan.json") + .expect("plan artifact"); + assert_eq!(plan.workspace_target_state, WorkspaceTargetState::Divergent); + assert!(plan.schema_error.is_some()); + assert_eq!(report.base_ref, None); + assert!(!report.all_in_pr_diff); + let after = Command::new("git") + .current_dir(&dir) + .args(["status", "--porcelain"]) + .output() + .expect("status") + .stdout; + assert_eq!( + before, after, + "inventory must never mutate repository state" + ); +} + +#[test] +fn protected_root_status_is_a_blocker_and_json_has_stable_shape() { + let (_temp, dir) = repo(); + fs::write(dir.join("README.md"), "fixture\n").expect("readme"); + git(&dir, &["add", "."]); + git(&dir, &["commit", "-m", "fixture"]); + let status = workspace::get_workspace_status(&dir).expect("workspace status"); + assert!(status.git.is_protected); + assert!(!status.can_work); + + let report = dirty_classification::classify(&dir, 6).expect("classification"); + let json = serde_json::to_string_pretty(&report).expect("JSON"); + assert!(json.contains("\"schema_version\"")); + assert!(json.contains("\"blocker_classes\"")); + assert!(!json.contains("token")); + assert!(!json.contains("Authorization")); +} + +#[test] +fn atomic_writer_replaces_complete_content_without_leftover_temp_files() { + let (_temp, dir) = repo(); + let path = dir.join(".decapod/governance/receipt.json"); + atomic::write_atomic(&path, b"{\"status\":\"ok\"}\n").expect("atomic write"); + assert_eq!(fs::read_to_string(&path).unwrap(), "{\"status\":\"ok\"}\n"); + let leftovers = fs::read_dir(path.parent().unwrap()) + .unwrap() + .filter_map(Result::ok) + .filter(|item| { + item.file_name() + .to_string_lossy() + .contains(".receipt.json.tmp-") + }) + .count(); + assert_eq!(leftovers, 0); +}