@khoavu882/doflow 2.0.0-beta.5 → 2.0.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/README.md +16 -16
- package/bin/doflow.js +22 -12
- package/core/.antigravity-plugin/plugin.json +1 -2
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/settings/settings.json +26 -9
- package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
- package/core/harnesses/codex/agents/core-implementer.toml +1 -0
- package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
- package/core/harnesses/codex/agents/research-writer.toml +1 -1
- package/core/harnesses/codex/agents/reviewer.toml +10 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
- package/core/harnesses/codex/agents/system-architect.toml +1 -1
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
- package/core/harnesses/copilot/hooks/doflow-lib.sh +94 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow.json +51 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -11
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
- package/core/harnesses/kiro/hooks/session-start.sh +1 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
- package/core/harnesses/pi/extensions/doflow-guard.js +11 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
- package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +16 -2
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +20 -7
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
- package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
- package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
- package/core/harnesses/shared/hooks/stream-hook-runner.js +58 -20
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +55 -82
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +58 -31
- package/core/registry/contribution-points.json +35 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1445 -366
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +599 -996
- package/core/registry/mcp.json +5 -1
- package/core/registry/models.json +7 -1
- package/core/registry/package-templates/README.md +19 -0
- package/core/registry/package-templates/agent/files/agent.md +9 -0
- package/core/registry/package-templates/agent/template.json +4 -0
- package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
- package/core/registry/package-templates/capability-provider/template.json +11 -0
- package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
- package/core/registry/package-templates/debate-preset/template.json +4 -0
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/package-templates/review-preset/files/preset.json +30 -0
- package/core/registry/package-templates/review-preset/template.json +4 -0
- package/core/registry/package-templates/skill/files/SKILL.md +8 -0
- package/core/registry/package-templates/skill/template.json +4 -0
- package/core/registry/package-templates/worker/files/worker.json +92 -0
- package/core/registry/package-templates/worker/template.json +11 -0
- package/core/registry/packages/doflow/doflow-package.json +15 -0
- package/core/registry/packages/doflow/presets/debate-3.json +26 -0
- package/core/registry/protocols.json +61 -0
- package/core/registry/routes.json +17 -0
- package/core/registry/rules.json +431 -0
- package/core/registry/settings.json +272 -0
- package/core/registry/storage.json +687 -31
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -6
- package/core/registry/workflows.json +55 -0
- package/core/shared/agent-specs/core-implementer.md +3 -0
- package/core/shared/agent-specs/delivery-lead.md +68 -0
- package/core/shared/agent-specs/quality-guardian.md +5 -1
- package/core/shared/agent-specs/research-writer.md +10 -3
- package/core/shared/agent-specs/reviewer.md +53 -0
- package/core/shared/agent-specs/spec-analyst.md +5 -6
- package/core/shared/agent-specs/system-architect.md +5 -1
- package/core/shared/guidance/DOFLOW_CORE.md +1 -5
- package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
- package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
- package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
- package/core/shared/guidance/pointers/codex.md +1 -4
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
- package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
- package/core/shared/guidance/references/ORCHESTRATION.md +3 -2
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +45 -0
- package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
- package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
- package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +49 -5
- package/core/shared/scripts/doflow/bin/doflow-run +152 -34
- package/core/shared/skills/do/SKILL.md +14 -12
- package/core/shared/skills/do/references/advice_calls.md +26 -0
- package/core/shared/skills/do/references/pm_routing.md +1 -2
- package/core/shared/skills/do/references/program_mode.md +31 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +45 -97
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -0
- package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
- package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
- package/core/shared/skills/do-code-review/SKILL.md +48 -148
- package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
- package/core/shared/skills/do-code-review/references/tools.md +90 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
- package/core/shared/skills/do-design/SKILL.md +23 -20
- package/core/shared/skills/do-diagnose/SKILL.md +18 -6
- package/core/shared/skills/do-document/SKILL.md +12 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +19 -7
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +16 -2
- package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
- package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
- package/core/shared/skills/do-flow/SKILL.md +10 -11
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +13 -14
- package/core/shared/skills/do-plan/SKILL.md +34 -20
- package/core/shared/skills/do-test/SKILL.md +7 -4
- package/core/shared/templates/doflow/constitution-template.md +9 -3
- package/core/shared/templates/doflow/data-model-template.md +1 -1
- package/core/shared/templates/doflow/design-template.md +113 -16
- package/core/shared/templates/doflow/plan-template.md +11 -10
- package/core/shared/templates/doflow/question-log-template.md +14 -2
- package/core/shared/templates/doflow/requirement-template.md +1 -1
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +143 -66
- package/src/adapters/claude/index.js +46 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +61 -13
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +291 -46
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +23 -12
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +2 -1
- package/src/adapters/pi/index.js +14 -7
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +22 -3
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +2 -1
- package/src/cli/index.js +337 -32
- package/src/cli/runtime-commands.js +220 -60
- package/src/cli/shared.js +0 -2
- package/src/helper/git.js +1 -1
- package/src/helper/guardrails.js +1 -1
- package/src/helper/marker-merge.js +31 -13
- package/src/helper/package-ids.js +37 -0
- package/src/helper/semver.js +94 -0
- package/src/install/backup.js +105 -15
- package/src/install/context.js +1 -1
- package/src/install/targets.js +31 -16
- package/src/lifecycle/coverage.js +4 -16
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +412 -78
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +551 -26
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +472 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +20 -11
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +13 -7
- package/src/runtime/cli.js +196 -19
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/delivery-store.js +4 -6
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/pointer.js +67 -12
- package/src/runtime/context/primary-command.js +117 -0
- package/src/runtime/context/primary.js +344 -0
- package/src/runtime/context/summary-store.js +655 -0
- package/src/runtime/context-pack.js +158 -79
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-ledger.js +52 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +33 -3
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +624 -18
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +281 -26
- package/src/runtime/intake/engine.js +677 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/inventory/coverage.js +1 -1
- package/src/runtime/inventory/index.js +1 -1
- package/src/runtime/inventory/precedence.js +2 -2
- package/src/runtime/inventory/siblings.js +6 -4
- package/src/runtime/knowledge/code-corpus.js +277 -0
- package/src/runtime/knowledge/code-lexical.js +130 -0
- package/src/runtime/knowledge/code-retrieval.js +307 -0
- package/src/runtime/knowledge/code-validation.js +106 -0
- package/src/runtime/knowledge/gp3-gate.js +74 -0
- package/src/runtime/knowledge/graph-adapter.js +288 -0
- package/src/runtime/knowledge/graph-fallback.js +394 -0
- package/src/runtime/knowledge/hybrid.js +136 -8
- package/src/runtime/knowledge/retrieval.js +34 -1
- package/src/runtime/knowledge/rtk-compress.js +77 -0
- package/src/runtime/knowledge/semble-bridge.py +106 -0
- package/src/runtime/knowledge/semble-lifecycle.js +114 -0
- package/src/runtime/knowledge/semble-provider.js +365 -0
- package/src/runtime/lifecycle/cli.js +88 -10
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +38 -7
- package/src/runtime/lifecycle/overview.js +26 -2
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +2 -2
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +487 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +46 -5
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +10 -30
- package/src/runtime/retrieval-record.js +75 -5
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +273 -0
- package/src/runtime/roadmap/loop.js +1719 -0
- package/src/runtime/roadmap/merge.js +390 -0
- package/src/runtime/roadmap/rows.js +1122 -0
- package/src/runtime/roadmap/seed.js +1063 -0
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +60 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +1 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +85 -14
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +176 -55
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +373 -64
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verb-view.js +267 -0
- package/src/runtime/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -54
- package/src/runtime/verification/impact.js +617 -0
- package/src/runtime/verification/inflight.js +205 -0
- package/src/runtime/verification/registry.js +176 -0
- package/src/runtime/verification/reuse.js +184 -0
- package/src/runtime/verification/telemetry-emit.js +254 -0
- package/src/runtime/verification/test-selection.js +67 -7
- package/src/runtime/worker/active.js +90 -0
- package/src/runtime/worker/cancel.js +222 -0
- package/src/runtime/worker/catalogue.js +122 -0
- package/src/runtime/worker/command.js +677 -0
- package/src/runtime/worker/evidence.js +288 -0
- package/src/runtime/worker/launch.js +256 -0
- package/src/runtime/worker/models.js +137 -0
- package/src/runtime/worker/parsers.js +218 -0
- package/src/runtime/worker/probes/index.js +706 -0
- package/src/runtime/worker/process-tree.js +415 -0
- package/src/runtime/worker/quota.js +43 -0
- package/src/runtime/worker/readiness.js +713 -0
- package/src/runtime/worker/run-store.js +166 -0
- package/src/runtime/worker/supervisor.js +541 -0
- package/src/runtime/worker/worktree-check.js +252 -0
- package/src/runtime/workflow-engine.js +5 -1
- package/src/runtime/workflow-orchestrator.js +610 -67
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -7
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -251
- package/src/adapters/gemini/index.js +0 -429
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 088 G.4, IC-017: synchronous, local-host coordination, never a completed-result cache.
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const { defaultIsAlive } = require('../../state/run-lock.js');
|
|
8
|
+
|
|
9
|
+
const TOKEN = /^[0-9a-f]{32}$/;
|
|
10
|
+
const KEY = /^[0-9a-f]{16}$/;
|
|
11
|
+
const RESULT_TTL_MS = 2 * 60 * 1000;
|
|
12
|
+
const SCRATCH_TTL_MS = 10 * 60 * 1000;
|
|
13
|
+
const MAX_STREAM_CHARS = 2000;
|
|
14
|
+
const wait = (ms ) => { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); };
|
|
15
|
+
|
|
16
|
+
function validLock(lock ) {
|
|
17
|
+
return lock?.v === 1 && TOKEN.test(lock.token) && Number.isSafeInteger(lock.pid) && lock.pid > 0
|
|
18
|
+
&& Number.isSafeInteger(lock.startedAt) && Number.isSafeInteger(lock.deadlineAt)
|
|
19
|
+
&& lock.deadlineAt >= lock.startedAt;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Only the execution fields the runner consumes cross the process boundary, never pid/argv. */
|
|
23
|
+
function execution(result ) {
|
|
24
|
+
if (!result || !(result.status === null || (Number.isInteger(result.status) && result.status >= 0))
|
|
25
|
+
|| typeof result.stdout !== 'string' || typeof result.stderr !== 'string'
|
|
26
|
+
|| !(result.signal == null || typeof result.signal === 'string')
|
|
27
|
+
|| (result.error && typeof result.error.message !== 'string')) throw new Error('Invalid execution result');
|
|
28
|
+
return {
|
|
29
|
+
status: result.status,
|
|
30
|
+
stdout: result.bounded === true ? result.stdout : result.stdout.slice(0, MAX_STREAM_CHARS),
|
|
31
|
+
stderr: result.bounded === true ? result.stderr : result.stderr.slice(0, MAX_STREAM_CHARS),
|
|
32
|
+
truncated: result.truncated === true || result.stdout.length > MAX_STREAM_CHARS || result.stderr.length > MAX_STREAM_CHARS,
|
|
33
|
+
signal: result.signal ?? null,
|
|
34
|
+
...(result.error ? { error: { message: result.error.message, code: result.error.code ?? null } } : {}),
|
|
35
|
+
...(result.bounded === true ? {
|
|
36
|
+
bounded: true,
|
|
37
|
+
tests: result.tests,
|
|
38
|
+
testsFailed: result.testsFailed,
|
|
39
|
+
...(result.failedTests ? { failedTests: result.failedTests } : {}),
|
|
40
|
+
...(result.failedFiles ? { failedFiles: result.failedFiles } : {}),
|
|
41
|
+
...(Object.hasOwn(result, 'logPath') ? { logPath: result.logPath } : {}),
|
|
42
|
+
} : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** All removers use a per-instance claim, including the leader. Without this, a sweeper that
|
|
47
|
+
* inspected an old lock could unlink a new leader between its token read and unlink. Claims
|
|
48
|
+
* are never waited for or taken over; an orphan costs sharing, never correctness. */
|
|
49
|
+
function removeLock(io , file , token , expired , now ) {
|
|
50
|
+
const claim = `${file}.${token}.claim`;
|
|
51
|
+
io.writeFileSync(claim, '', { flag: 'wx', mode: 0o600 });
|
|
52
|
+
try {
|
|
53
|
+
const current = JSON.parse(io.readFileSync(file, 'utf8'));
|
|
54
|
+
if (validLock(current) && current.token === token && (!expired || now() > current.deadlineAt)) {
|
|
55
|
+
io.unlinkSync(file);
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
io.unlinkSync(claim);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sweepInflight(dir , { fsImpl = fs, now = Date.now } = {}) {
|
|
63
|
+
for (const name of fsImpl.readdirSync(dir)) {
|
|
64
|
+
const result = /^inflight-[0-9a-f]{16}\.[0-9a-f]{32}\.result\.json$/.test(name);
|
|
65
|
+
const lock = /^inflight-[0-9a-f]{16}\.lock$/.test(name);
|
|
66
|
+
const scratch = /^inflight-[0-9a-f]{16}(?:\.[0-9a-f]{32}\.tmp|\.lock\.[0-9a-f]{32}\.claim)$/.test(name);
|
|
67
|
+
if (!result && !lock && !scratch) continue;
|
|
68
|
+
const file = path.join(dir, name);
|
|
69
|
+
try {
|
|
70
|
+
if (scratch) {
|
|
71
|
+
if (now() - fsImpl.statSync(file).mtimeMs > SCRATCH_TTL_MS) fsImpl.unlinkSync(file);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const raw = fsImpl.readFileSync(file, 'utf8');
|
|
75
|
+
let record ;
|
|
76
|
+
try {
|
|
77
|
+
record = JSON.parse(raw);
|
|
78
|
+
if (result ? record?.v !== 1 || !Number.isSafeInteger(record.finishedAt) : !validLock(record)) {
|
|
79
|
+
throw new Error('Invalid inflight record');
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Fresh empty locks still get the follower's bounded retry. Only malformed
|
|
83
|
+
// records use mtime: valid locks always require the token-bound removal claim.
|
|
84
|
+
if (now() - fsImpl.statSync(file).mtimeMs > RESULT_TTL_MS) fsImpl.unlinkSync(file);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (result) {
|
|
88
|
+
if (now() - record.finishedAt > RESULT_TTL_MS) fsImpl.unlinkSync(file);
|
|
89
|
+
} else {
|
|
90
|
+
if (now() > record.deadlineAt) removeLock(fsImpl, file, record.token, true, now);
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// A raced, inaccessible or claimed entry never disables unrelated sharing.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Returns spawnSync's execution plus out-of-report telemetry. A coordination fault executes
|
|
99
|
+
* locally; exceptions from execute itself are left to the runner's ordinary FAIL handling. */
|
|
100
|
+
function runInflight({ dir, key, timeoutMs, execute, summarize, fsImpl = fs, now = Date.now,
|
|
101
|
+
isAlive = defaultIsAlive, sleep = wait, monotonic = () => process.hrtime.bigint(), mintToken = () => crypto.randomBytes(16).toString('hex') } ) {
|
|
102
|
+
const unshared = () => ({ result: execute(), shared: 'none', sharedRef: null });
|
|
103
|
+
if (typeof key !== 'string' || !KEY.test(key) || !Number.isSafeInteger(timeoutMs) || timeoutMs < 0) return unshared();
|
|
104
|
+
let token ;
|
|
105
|
+
let lockFile ;
|
|
106
|
+
let temp ;
|
|
107
|
+
let tempOwned = false;
|
|
108
|
+
let owner = false;
|
|
109
|
+
let follower = null;
|
|
110
|
+
let arrival = 0;
|
|
111
|
+
let arrivalNs ;
|
|
112
|
+
try {
|
|
113
|
+
fsImpl.mkdirSync(dir, { recursive: true });
|
|
114
|
+
sweepInflight(dir, { fsImpl, now });
|
|
115
|
+
token = mintToken();
|
|
116
|
+
if (!TOKEN.test(token)) throw new Error('Invalid token');
|
|
117
|
+
lockFile = path.join(dir, `inflight-${key}.lock`);
|
|
118
|
+
temp = path.join(dir, `inflight-${key}.${token}.tmp`);
|
|
119
|
+
const startedAt = now();
|
|
120
|
+
const record = { v: 1, token, pid: process.pid, startedAt, deadlineAt: startedAt + timeoutMs + 30000 };
|
|
121
|
+
if (!validLock(record)) throw new Error('Invalid clock');
|
|
122
|
+
fsImpl.writeFileSync(temp, JSON.stringify(record), { flag: 'wx', mode: 0o600 });
|
|
123
|
+
tempOwned = true;
|
|
124
|
+
try {
|
|
125
|
+
fsImpl.linkSync(temp, lockFile);
|
|
126
|
+
owner = true;
|
|
127
|
+
} catch (error ) {
|
|
128
|
+
if (error.code !== 'EEXIST') throw error;
|
|
129
|
+
arrival = now();
|
|
130
|
+
if (!Number.isSafeInteger(arrival)) throw new Error('Invalid arrival clock');
|
|
131
|
+
arrivalNs = monotonic();
|
|
132
|
+
}
|
|
133
|
+
fsImpl.unlinkSync(temp);
|
|
134
|
+
tempOwned = false;
|
|
135
|
+
temp = undefined;
|
|
136
|
+
if (!owner) {
|
|
137
|
+
// Bound retries by count as well as clock: a stalled wall clock must not hang verification.
|
|
138
|
+
for (let attempt = 0; attempt <= 4; attempt++) {
|
|
139
|
+
let raw = '';
|
|
140
|
+
try { raw = fsImpl.readFileSync(lockFile, 'utf8'); } catch (error ) {
|
|
141
|
+
if (error.code !== 'ENOENT') throw error;
|
|
142
|
+
}
|
|
143
|
+
if (raw) { follower = JSON.parse(raw); break; }
|
|
144
|
+
if (attempt === 4 || now() - arrival >= 1000) break;
|
|
145
|
+
sleep(250);
|
|
146
|
+
}
|
|
147
|
+
if (!validLock(follower)) throw new Error('Invalid follower lock');
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
if (tempOwned && temp) { try { fsImpl.unlinkSync(temp); } catch { /* local temp only */ } }
|
|
151
|
+
if (owner) { try { removeLock(fsImpl, lockFile , token , false, now); } catch { /* never remove an unknown owner */ } }
|
|
152
|
+
return unshared();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!owner) {
|
|
156
|
+
try {
|
|
157
|
+
const resultFile = path.join(dir, `inflight-${key}.${follower.token}.result.json`);
|
|
158
|
+
const maxPolls = Math.ceil((timeoutMs + 30000) / 250) + 1;
|
|
159
|
+
for (let attempt = 0; attempt < maxPolls; attempt++) {
|
|
160
|
+
let raw = null;
|
|
161
|
+
try { raw = fsImpl.readFileSync(resultFile, 'utf8'); } catch (error ) {
|
|
162
|
+
if (error.code !== 'ENOENT') throw error;
|
|
163
|
+
}
|
|
164
|
+
if (raw !== null) {
|
|
165
|
+
const record = JSON.parse(raw);
|
|
166
|
+
if (record?.v !== 1 || record.token !== follower.token || !Number.isSafeInteger(record.finishedAt)
|
|
167
|
+
|| arrival > record.finishedAt || record.finishedAt < follower.startedAt) throw new Error('Unrelated result');
|
|
168
|
+
return { result: execution(record.result), shared: 'follower', sharedRef: follower.token, durationMs: Math.max(0, Number(monotonic() - arrivalNs ) / 1e6) };
|
|
169
|
+
}
|
|
170
|
+
const current = JSON.parse(fsImpl.readFileSync(lockFile , 'utf8'));
|
|
171
|
+
if (!validLock(current) || current.token !== follower.token || !isAlive(follower.pid)
|
|
172
|
+
|| now() > follower.deadlineAt) break;
|
|
173
|
+
sleep(250);
|
|
174
|
+
}
|
|
175
|
+
} catch { /* any result, lock, liveness or wait fault runs locally */ }
|
|
176
|
+
return unshared();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let result ;
|
|
180
|
+
let shared = 'leader';
|
|
181
|
+
try {
|
|
182
|
+
result = execute();
|
|
183
|
+
if (summarize) result = summarize(result);
|
|
184
|
+
const persisted = execution(result);
|
|
185
|
+
const finishedAt = now();
|
|
186
|
+
if (!Number.isSafeInteger(finishedAt)) throw new Error('Invalid result clock');
|
|
187
|
+
const resultFile = path.join(dir, `inflight-${key}.${token }.result.json`);
|
|
188
|
+
temp = path.join(dir, `inflight-${key}.${token }.tmp`);
|
|
189
|
+
fsImpl.writeFileSync(temp, JSON.stringify({ v: 1, token: token , finishedAt, result: persisted }), { flag: 'wx', mode: 0o600 });
|
|
190
|
+
tempOwned = true;
|
|
191
|
+
fsImpl.renameSync(temp, resultFile);
|
|
192
|
+
tempOwned = false;
|
|
193
|
+
temp = undefined;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Execution already happened locally. Publication faults do not replace its verdict.
|
|
196
|
+
shared = 'none';
|
|
197
|
+
if (result === undefined) throw error;
|
|
198
|
+
} finally {
|
|
199
|
+
if (tempOwned && temp) { try { fsImpl.unlinkSync(temp); } catch { /* local temp only */ } }
|
|
200
|
+
try { removeLock(fsImpl, lockFile , token , false, now); } catch { shared = 'none'; }
|
|
201
|
+
}
|
|
202
|
+
return { result, shared, sharedRef: shared === 'leader' ? token : null };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { runInflight, sweepInflight };
|
|
@@ -16,6 +16,7 @@ const path = require('node:path');
|
|
|
16
16
|
const nodeFs = require('node:fs');
|
|
17
17
|
const { parseYamlFile } = require('../capability-router.js');
|
|
18
18
|
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
19
|
+
const { configHome } = require('../config-home.js');
|
|
19
20
|
|
|
20
21
|
const REGISTRY_FILENAME = 'verification.json';
|
|
21
22
|
|
|
@@ -107,8 +108,22 @@ function loadVerificationRegistry(options = {}) {
|
|
|
107
108
|
// of the registry, so it is checkable here instead of being re-asserted in prose.
|
|
108
109
|
assert(firstModelOrder > lastNonModelOrder, 'every model-based tier must be ordered after every deterministic tier');
|
|
109
110
|
|
|
111
|
+
assert(data.levels && typeof data.levels === 'object', 'levels must be an object');
|
|
112
|
+
const l0 = data.levels.L0;
|
|
113
|
+
assert(l0 && typeof l0 === 'object', 'levels.L0 must be an object');
|
|
114
|
+
assert(Array.isArray(l0.tiers) && l0.tiers.length > 0, 'levels.L0.tiers must be a non-empty array');
|
|
115
|
+
for (const t of l0.tiers) {
|
|
116
|
+
assert(seen.has(t), `levels.L0 tier '${t}' must exist in tiers`);
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
for (const [level, def] of Object.entries(data.riskLevels) ) {
|
|
111
120
|
assert(def && Array.isArray(def.requiredTiers), `risk level '${level}' needs requiredTiers`);
|
|
121
|
+
for (let i = 0; i < l0.tiers.length; i++) {
|
|
122
|
+
assert(
|
|
123
|
+
def.requiredTiers[i] === l0.tiers[i],
|
|
124
|
+
`risk level '${level}' requiredTiers must begin with L0 tiers [${l0.tiers.join(', ')}]`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
112
127
|
for (const id of def.requiredTiers.concat(def.advisoryTiers || [])) {
|
|
113
128
|
assert(seen.has(id), `risk level '${level}' references unknown tier '${id}'`);
|
|
114
129
|
}
|
|
@@ -118,13 +133,174 @@ function loadVerificationRegistry(options = {}) {
|
|
|
118
133
|
);
|
|
119
134
|
}
|
|
120
135
|
|
|
136
|
+
const levelNames = ['L1', 'L2', 'L3'] ;
|
|
137
|
+
const riskLevelsInLevels = new Map ();
|
|
138
|
+
for (const lvl of levelNames) {
|
|
139
|
+
const entry = data.levels[lvl];
|
|
140
|
+
assert(entry && typeof entry === 'object', `levels.${lvl} must be an object`);
|
|
141
|
+
assert(Array.isArray(entry.riskLevels), `levels.${lvl}.riskLevels must be an array`);
|
|
142
|
+
for (const r of entry.riskLevels) {
|
|
143
|
+
assert(typeof r === 'string' && r !== '', `levels.${lvl}.riskLevels entries must be non-empty strings`);
|
|
144
|
+
assert(Object.hasOwn(data.riskLevels, r), `levels.${lvl} references unknown risk level '${r}'`);
|
|
145
|
+
assert(!riskLevelsInLevels.has(r), `risk level '${r}' is declared in multiple levels ('${riskLevelsInLevels.get(r)}' and '${lvl}')`);
|
|
146
|
+
riskLevelsInLevels.set(r, lvl);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const r of Object.keys(data.riskLevels)) {
|
|
150
|
+
assert(riskLevelsInLevels.has(r), `risk level '${r}' must be assigned to exactly one of L1..L3`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
assert(policy.v0 && typeof policy.v0 === 'object', 'policy.v0 must be an object');
|
|
154
|
+
assert(Array.isArray(policy.v0.levels), 'policy.v0.levels must be an array');
|
|
155
|
+
const allowedV0Levels = new Set(['L1', 'L2']);
|
|
156
|
+
for (const lvl of policy.v0.levels) {
|
|
157
|
+
assert(allowedV0Levels.has(lvl), `policy.v0.levels must be a subset of ['L1', 'L2']: found '${lvl}'`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 089 IC-008: the reuse policy is data (levels that may reuse, age bound, declared ignored inputs).
|
|
161
|
+
assert(policy.reuse && typeof policy.reuse === 'object', 'policy.reuse must be an object');
|
|
162
|
+
assert(Array.isArray(policy.reuse.levels), 'policy.reuse.levels must be an array');
|
|
163
|
+
for (const lvl of policy.reuse.levels) {
|
|
164
|
+
assert(allowedV0Levels.has(lvl), `policy.reuse.levels must be a subset of ['L1', 'L2']: found '${lvl}'`);
|
|
165
|
+
}
|
|
166
|
+
assert(
|
|
167
|
+
Number.isInteger(policy.reuse.maxAgeMs) && policy.reuse.maxAgeMs > 0,
|
|
168
|
+
'policy.reuse.maxAgeMs must be a positive integer',
|
|
169
|
+
);
|
|
170
|
+
assert(Array.isArray(policy.reuse.ignoredInputs), 'policy.reuse.ignoredInputs must be an array');
|
|
171
|
+
for (const input of policy.reuse.ignoredInputs) {
|
|
172
|
+
assert(
|
|
173
|
+
typeof input === 'string' && input !== '' && !input.includes('\0')
|
|
174
|
+
&& !path.isAbsolute(input) && !path.win32.isAbsolute(input)
|
|
175
|
+
&& !input.split(/[\\/]+/).includes('..'),
|
|
176
|
+
`policy.reuse.ignoredInputs entries must be relative paths without '..': found '${String(input)}'`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 095b IC-003: the review depth policy is data. An invalid block is a load error, never a default.
|
|
181
|
+
const depthPolicy = data.reviewDepth;
|
|
182
|
+
assert(isPlainObject(depthPolicy), 'reviewDepth must be an object');
|
|
183
|
+
for (const key of Object.keys(depthPolicy)) {
|
|
184
|
+
assert(REVIEW_DEPTH_KEYS.includes(key), `reviewDepth has an unknown key '${key}' (allowed: ${REVIEW_DEPTH_KEYS.join(', ')})`);
|
|
185
|
+
}
|
|
186
|
+
assert(Array.isArray(depthPolicy.singleRisks), 'reviewDepth.singleRisks must be an array');
|
|
187
|
+
for (const risk of depthPolicy.singleRisks) {
|
|
188
|
+
assert(
|
|
189
|
+
typeof risk === 'string' && Object.hasOwn(data.riskLevels, risk),
|
|
190
|
+
`reviewDepth.singleRisks must be a subset of the riskLevels keys: found '${String(risk)}'`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
assert(
|
|
194
|
+
Array.isArray(depthPolicy.lightPaths) && depthPolicy.lightPaths.length > 0,
|
|
195
|
+
'reviewDepth.lightPaths must be a non-empty array',
|
|
196
|
+
);
|
|
197
|
+
for (const entry of depthPolicy.lightPaths) {
|
|
198
|
+
assert(
|
|
199
|
+
typeof entry === 'string' && entry !== '' && !entry.includes('\0')
|
|
200
|
+
&& !entry.startsWith('/') && !entry.split(/[\\/]+/).includes('..')
|
|
201
|
+
&& !entry.startsWith('./') && !entry.includes('\\'),
|
|
202
|
+
`reviewDepth.lightPaths entries must be non-empty '/'-separated strings without a '..' segment, a leading '/' or './', or a backslash: found '${String(entry)}'`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
121
206
|
return data;
|
|
122
207
|
}
|
|
123
208
|
|
|
209
|
+
/** The keys a `reviewDepth` block may carry (095b IC-003); anything else can never take effect. The seat counts
|
|
210
|
+
* live in core/registry/protocols.json (119 D-4), so a `depths` key is refused here as unknown. */
|
|
211
|
+
const REVIEW_DEPTH_KEYS = ['description', 'singleRisks', 'lightPaths'];
|
|
212
|
+
|
|
213
|
+
function isPlainObject(value ) {
|
|
214
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The validated review depth policy (095b IC-003): `{ description, singleRisks, lightPaths }`.
|
|
219
|
+
* Throws when the registry carries no such block; `loadVerificationRegistry` already guarantees it for a loaded registry.
|
|
220
|
+
*/
|
|
221
|
+
function reviewDepthPolicy(registry ) {
|
|
222
|
+
assert(isPlainObject(registry?.reviewDepth), 'reviewDepth must be an object');
|
|
223
|
+
return registry.reviewDepth;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Maps a risk level ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL') to its verification level ('L1' | 'L2' | 'L3').
|
|
228
|
+
* Returns null if the risk level is unknown, null, or undefined.
|
|
229
|
+
*/
|
|
230
|
+
function levelOf(registry , risk ) {
|
|
231
|
+
if (!registry?.levels || typeof risk !== 'string') return null;
|
|
232
|
+
for (const lvl of ['L1', 'L2', 'L3'] ) {
|
|
233
|
+
const riskLevels = registry.levels[lvl]?.riskLevels;
|
|
234
|
+
if (Array.isArray(riskLevels) && riskLevels.includes(risk)) {
|
|
235
|
+
return lvl;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns whether V0 behavior applies to the given risk level under the registry policy.
|
|
243
|
+
* True when the risk level maps to a level listed in policy.v0.levels.
|
|
244
|
+
*/
|
|
245
|
+
function v0Applies(registry , risk ) {
|
|
246
|
+
const level = levelOf(registry, risk);
|
|
247
|
+
if (!level) return false;
|
|
248
|
+
const v0Levels = registry?.policy?.v0?.levels;
|
|
249
|
+
return Array.isArray(v0Levels) && v0Levels.includes(level);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Returns whether result reuse may apply to the given risk level (089 IC-008).
|
|
254
|
+
* True when the risk level maps to a level listed in policy.reuse.levels.
|
|
255
|
+
*/
|
|
256
|
+
function reuseApplies(registry , risk ) {
|
|
257
|
+
const level = levelOf(registry, risk);
|
|
258
|
+
if (!level) return false;
|
|
259
|
+
const reuseLevels = registry?.policy?.reuse?.levels;
|
|
260
|
+
return Array.isArray(reuseLevels) && reuseLevels.includes(level);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolves whether V0 behavior is enabled (IC-015, DD-8).
|
|
265
|
+
* False if env.DOFLOW_VERIFY_V0 === 'off', else false if <configDir>/verify-v0.off exists, else true.
|
|
266
|
+
* A stat fault counts as absent (on). Read once per verify command (handleVerifyCommand).
|
|
267
|
+
*
|
|
268
|
+
* @param {Object} [env=process.env]
|
|
269
|
+
* @param {string|null} [configDir] directory containing verify-v0.off sentinel; falls back to configHome(env).dir
|
|
270
|
+
* @param {Object} [fsImpl=nodeFs]
|
|
271
|
+
* @returns {boolean}
|
|
272
|
+
*/
|
|
273
|
+
function v0Enabled(env = process.env, configDir , fsImpl = nodeFs) {
|
|
274
|
+
if (env && env.DOFLOW_VERIFY_V0 === 'off') {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
const dir = configDir !== undefined ? configDir : configHome(env).dir;
|
|
278
|
+
if (!dir) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
const io = fsImpl || nodeFs;
|
|
282
|
+
try {
|
|
283
|
+
const stat = io.statSync(path.join(dir, 'verify-v0.off'));
|
|
284
|
+
if (typeof stat.isFile === 'function' ? stat.isFile() : true) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// A stat fault counts as absent (on).
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
124
294
|
module.exports = {
|
|
125
295
|
loadVerificationRegistry,
|
|
296
|
+
reviewDepthPolicy,
|
|
297
|
+
levelOf,
|
|
298
|
+
v0Applies,
|
|
299
|
+
reuseApplies,
|
|
300
|
+
v0Enabled,
|
|
126
301
|
TIER_RESOLUTIONS,
|
|
127
302
|
TIER_STATUSES,
|
|
128
303
|
RECOVERY_OUTCOMES,
|
|
129
304
|
REGISTRY_FILENAME,
|
|
130
305
|
};
|
|
306
|
+
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const { loadStorageRegistry } = require('../storage/registry.js');
|
|
7
|
+
const { resolveRoots, resolveStorePath } = require('../storage/roots.js');
|
|
8
|
+
|
|
9
|
+
const KEY = /^[0-9a-f]{16}$/;
|
|
10
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
11
|
+
const REF = /^[0-9a-f]{32}$/;
|
|
12
|
+
const LEVELS = new Set(['L1', 'L2', 'L3']);
|
|
13
|
+
/** The report's bounded excerpt (2,000 kept characters plus the elision marker, at most about 2,060) must fit whole,
|
|
14
|
+
* so a reused check shows exactly what the executed one showed (089 DEVIATION from IC-002's 2,000). */
|
|
15
|
+
const MAX_STREAM_CHARS = 2200;
|
|
16
|
+
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
17
|
+
|
|
18
|
+
function isObject(value ) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
19
|
+
function exactKeys(value , required , optional = []) {
|
|
20
|
+
if (!isObject(value)) return false;
|
|
21
|
+
const keys = Object.keys(value);
|
|
22
|
+
return required.every((key) => Object.hasOwn(value, key))
|
|
23
|
+
&& keys.every((key) => required.includes(key) || optional.includes(key));
|
|
24
|
+
}
|
|
25
|
+
function optionalCount(value ) { return value === null || Number.isSafeInteger(value) && value >= 0; }
|
|
26
|
+
|
|
27
|
+
/** IC-002: validate the closed record/result schemas before any cached PASS is trusted. */
|
|
28
|
+
function validRecord(record ) {
|
|
29
|
+
if (!exactKeys(record, ['v', 'key', 'payloadDigest', 'ref', 'recordedAt', 'level', 'result'])) return false;
|
|
30
|
+
if (record.v !== 1 || typeof record.key !== 'string' || !KEY.test(record.key)
|
|
31
|
+
|| typeof record.payloadDigest !== 'string' || !DIGEST.test(record.payloadDigest)
|
|
32
|
+
|| typeof record.ref !== 'string' || !REF.test(record.ref)
|
|
33
|
+
|| !Number.isSafeInteger(record.recordedAt) || !LEVELS.has(record.level)) return false;
|
|
34
|
+
const result = record.result;
|
|
35
|
+
if (!exactKeys(result, ['status', 'exitCode', 'stdout', 'stderr', 'truncated'], ['tests', 'testsFailed'])) return false;
|
|
36
|
+
if (result.status !== 'PASS' || result.exitCode !== 0
|
|
37
|
+
|| typeof result.stdout !== 'string' || result.stdout.length > MAX_STREAM_CHARS
|
|
38
|
+
|| typeof result.stderr !== 'string' || result.stderr.length > MAX_STREAM_CHARS
|
|
39
|
+
|| typeof result.truncated !== 'boolean') return false;
|
|
40
|
+
if (Object.hasOwn(result, 'tests') && !optionalCount(result.tests)) return false;
|
|
41
|
+
if (Object.hasOwn(result, 'testsFailed') && !optionalCount(result.testsFailed)) return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** IC-008: resolve the declared project-state store through the shared storage registry. */
|
|
46
|
+
function reuseDir({ projectRoot, configDir, env = process.env, fsImpl = fs } = {}) {
|
|
47
|
+
try {
|
|
48
|
+
const root = typeof projectRoot === 'string' ? projectRoot
|
|
49
|
+
: typeof configDir === 'string' ? path.dirname(configDir) : process.cwd();
|
|
50
|
+
const registry = loadStorageRegistry({ repoRoot: REPO_ROOT, fsImpl });
|
|
51
|
+
const store = registry.stores.find((entry ) => entry.id === 'verification-reuse');
|
|
52
|
+
if (!store) return null;
|
|
53
|
+
const roots = resolveRoots({ registry, projectRoot: root, env });
|
|
54
|
+
return resolveStorePath({ store, roots, fsImpl });
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** IC-003: look up one trusted, current PASS record. Invalid inputs and filesystem faults fail closed. */
|
|
61
|
+
function lookupReuse(options = {}) {
|
|
62
|
+
try { return lookupReuseOptions(options || {}); } catch { return { hit: false, reason: 'fault' }; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function lookupReuseOptions({ dir, key, payloadDigest, maxAgeMs, now = Date.now, fsImpl = fs, uid } = {}) {
|
|
66
|
+
if (typeof dir !== 'string' || typeof key !== 'string' || !KEY.test(key)
|
|
67
|
+
|| typeof payloadDigest !== 'string' || !DIGEST.test(payloadDigest)
|
|
68
|
+
|| !Number.isSafeInteger(maxAgeMs) || maxAgeMs < 0 || typeof now !== 'function') {
|
|
69
|
+
return { hit: false, reason: 'invalid' };
|
|
70
|
+
}
|
|
71
|
+
const file = path.join(dir, `reuse-${key}.json`);
|
|
72
|
+
let stat ;
|
|
73
|
+
try { stat = fsImpl.lstatSync(file); } catch (error ) {
|
|
74
|
+
return { hit: false, reason: error?.code === 'ENOENT' ? 'absent' : 'fault' };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
if (!stat || typeof stat.isFile !== 'function' || !stat.isFile()
|
|
78
|
+
|| typeof stat.isSymbolicLink === 'function' && stat.isSymbolicLink()) {
|
|
79
|
+
return { hit: false, reason: 'untrusted' };
|
|
80
|
+
}
|
|
81
|
+
if (process.platform !== 'win32') {
|
|
82
|
+
const expectedUid = typeof uid === 'number' ? uid : (process ).getuid?.();
|
|
83
|
+
if ((stat.mode & 0o077) !== 0) return { hit: false, reason: 'untrusted' };
|
|
84
|
+
if (expectedUid !== undefined && stat.uid !== expectedUid) return { hit: false, reason: 'untrusted' };
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
return { hit: false, reason: 'fault' };
|
|
88
|
+
}
|
|
89
|
+
// 089 re-review S-1: a process that cannot evict from this store (evictReuse ignores EACCES) must
|
|
90
|
+
// not reuse from it either, or an executed FAIL it could not remove is followed by a reused PASS.
|
|
91
|
+
try { fsImpl.accessSync(dir, fs.constants.W_OK); } catch {
|
|
92
|
+
return { hit: false, reason: 'unwritable' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let raw ;
|
|
96
|
+
try { raw = fsImpl.readFileSync(file, 'utf8'); } catch {
|
|
97
|
+
return { hit: false, reason: 'fault' };
|
|
98
|
+
}
|
|
99
|
+
let record ;
|
|
100
|
+
try { record = JSON.parse(raw); } catch {
|
|
101
|
+
return { hit: false, reason: 'invalid' };
|
|
102
|
+
}
|
|
103
|
+
if (!validRecord(record)) return { hit: false, reason: 'invalid' };
|
|
104
|
+
if (record.key !== key || record.payloadDigest !== payloadDigest) return { hit: false, reason: 'digest-mismatch' };
|
|
105
|
+
let current ;
|
|
106
|
+
try { current = now(); } catch { return { hit: false, reason: 'fault' }; }
|
|
107
|
+
if (!Number.isSafeInteger(current)) return { hit: false, reason: 'fault' };
|
|
108
|
+
if (record.recordedAt > current) return { hit: false, reason: 'future' };
|
|
109
|
+
if (current - record.recordedAt > maxAgeMs) return { hit: false, reason: 'stale' };
|
|
110
|
+
return { hit: true, record };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** IC-002/IC-003: publish only a PASS through an owner-only temp file and atomic rename. */
|
|
114
|
+
function storeReuse(options = {}) {
|
|
115
|
+
try { return storeReuseOptions(options || {}); } catch { return { stored: false, ref: null }; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function storeReuseOptions({ dir, key, payloadDigest, level, result, now = Date.now, fsImpl = fs,
|
|
119
|
+
mintRef = () => crypto.randomBytes(16).toString('hex') } = {}) {
|
|
120
|
+
let temp = null;
|
|
121
|
+
let ownsTemp = false;
|
|
122
|
+
try {
|
|
123
|
+
if (typeof dir !== 'string' || typeof key !== 'string' || !KEY.test(key)
|
|
124
|
+
|| typeof payloadDigest !== 'string' || !DIGEST.test(payloadDigest)
|
|
125
|
+
|| !LEVELS.has(level) || typeof now !== 'function' || typeof mintRef !== 'function'
|
|
126
|
+
|| !isObject(result) || result.status !== 'PASS' || result.exitCode !== 0
|
|
127
|
+
|| typeof result.stdout !== 'string' || typeof result.stderr !== 'string'
|
|
128
|
+
|| typeof result.truncated !== 'boolean') return { stored: false, ref: null };
|
|
129
|
+
if (Object.hasOwn(result, 'tests') && !optionalCount(result.tests)) return { stored: false, ref: null };
|
|
130
|
+
if (Object.hasOwn(result, 'testsFailed') && !optionalCount(result.testsFailed)) return { stored: false, ref: null };
|
|
131
|
+
const recordedAt = now();
|
|
132
|
+
if (!Number.isSafeInteger(recordedAt)) return { stored: false, ref: null };
|
|
133
|
+
const ref = mintRef();
|
|
134
|
+
if (typeof ref !== 'string' || !REF.test(ref)) return { stored: false, ref: null };
|
|
135
|
+
const stdout = result.stdout.slice(0, MAX_STREAM_CHARS);
|
|
136
|
+
const stderr = result.stderr.slice(0, MAX_STREAM_CHARS);
|
|
137
|
+
const savedResult = {
|
|
138
|
+
status: 'PASS',
|
|
139
|
+
exitCode: 0,
|
|
140
|
+
stdout,
|
|
141
|
+
stderr,
|
|
142
|
+
truncated: result.truncated || result.stdout.length > MAX_STREAM_CHARS || result.stderr.length > MAX_STREAM_CHARS,
|
|
143
|
+
};
|
|
144
|
+
if (Object.hasOwn(result, 'tests')) savedResult.tests = result.tests;
|
|
145
|
+
if (Object.hasOwn(result, 'testsFailed')) savedResult.testsFailed = result.testsFailed;
|
|
146
|
+
const record = { v: 1, key, payloadDigest, ref, recordedAt, level, result: savedResult };
|
|
147
|
+
if (!validRecord(record)) return { stored: false, ref: null };
|
|
148
|
+
|
|
149
|
+
fsImpl.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
150
|
+
const directory = fsImpl.lstatSync(dir);
|
|
151
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) return { stored: false, ref: null };
|
|
152
|
+
fsImpl.chmodSync(dir, 0o700);
|
|
153
|
+
const file = path.join(dir, `reuse-${key}.json`);
|
|
154
|
+
temp = path.join(dir, `reuse-${key}.${ref}.tmp`);
|
|
155
|
+
fsImpl.writeFileSync(temp, JSON.stringify(record), { flag: 'wx', mode: 0o600 });
|
|
156
|
+
ownsTemp = true;
|
|
157
|
+
fsImpl.chmodSync(temp, 0o600);
|
|
158
|
+
fsImpl.renameSync(temp, file);
|
|
159
|
+
ownsTemp = false;
|
|
160
|
+
return { stored: true, ref };
|
|
161
|
+
} catch {
|
|
162
|
+
if (ownsTemp && temp) { try { fsImpl.unlinkSync(temp); } catch { /* only this call's unique temp */ } }
|
|
163
|
+
return { stored: false, ref: null };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* IC-003 (089 review C-2): remove the stored record for a key after an executed non-PASS, so a
|
|
169
|
+
* failure is never followed by a reused PASS. Only `reuse-<hex16>.json` is touched; a symlink
|
|
170
|
+
* there is unlinked itself, never followed. Any fault is ignored; it never throws.
|
|
171
|
+
*/
|
|
172
|
+
function evictReuse({ dir, key, fsImpl = fs } = {}) {
|
|
173
|
+
try {
|
|
174
|
+
if (typeof dir !== 'string' || typeof key !== 'string' || !KEY.test(key)) return { evicted: false };
|
|
175
|
+
const file = path.join(dir, `reuse-${key}.json`);
|
|
176
|
+
fsImpl.lstatSync(file);
|
|
177
|
+
fsImpl.unlinkSync(file);
|
|
178
|
+
return { evicted: true };
|
|
179
|
+
} catch {
|
|
180
|
+
return { evicted: false };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { lookupReuse, storeReuse, evictReuse, reuseDir };
|