@khoavu882/doflow 2.0.0-beta.5 → 2.0.0
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 +12 -11
- package/bin/doflow.js +21 -11
- 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/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/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/settings/settings.json +18 -0
- 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/lib.sh +44 -25
- 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/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
- package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-bash-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 +57 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -1
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +1 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +1 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +1 -0
- 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 +8 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +0 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +102 -9
- 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 +10 -2
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +48 -15
- 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 +2 -11
- 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-tool-guard.sh +3 -2
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +213 -12
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +114 -38
- package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
- package/core/harnesses/shared/hooks/policies/stop-check.sh +20 -9
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +5 -2
- 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 +47 -8
- 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 +32 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1442 -363
- package/core/registry/lifecycle.json +485 -304
- 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/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 +276 -0
- package/core/registry/storage.json +679 -31
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -1
- 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 +1 -2
- 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 +48 -4
- package/core/shared/scripts/doflow/bin/doflow-run +149 -34
- package/core/shared/skills/do/SKILL.md +12 -10
- 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 +20 -74
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -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 +41 -144
- 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 +18 -17
- package/core/shared/skills/do-diagnose/SKILL.md +14 -4
- package/core/shared/skills/do-document/SKILL.md +11 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +18 -6
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +15 -58
- 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 +13 -1
- 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 +8 -9
- package/core/shared/skills/do-flow/references/task_classes.md +2 -2
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +4 -7
- package/core/shared/skills/do-plan/SKILL.md +26 -15
- 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/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 +24 -4
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/index.js +48 -13
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +290 -44
- 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 +21 -2
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +2 -1
- package/src/cli/index.js +322 -29
- package/src/cli/runtime-commands.js +209 -57
- 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 +20 -3
- 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 +492 -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/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 +154 -78
- 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 +34 -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 +565 -17
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +277 -22
- 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 +16 -6
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +6 -1
- package/src/runtime/lifecycle/overview.js +24 -1
- 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 +477 -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/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +270 -0
- package/src/runtime/roadmap/loop.js +1636 -0
- package/src/runtime/roadmap/merge.js +381 -0
- package/src/runtime/roadmap/rows.js +1113 -0
- package/src/runtime/roadmap/seed.js +951 -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 +47 -11
- 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 +374 -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/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -9
- 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 +288 -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 +578 -63
- 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/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/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,628 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const nodeFs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check key and tree digest (088 E.2, IC-009, DD-2; 089 IC-001, DD-1).
|
|
12
|
+
*
|
|
13
|
+
* Computes a deterministic 16-hex key representing a check's execution identity.
|
|
14
|
+
* Two check invocations with equal check keys can safely share results in in-flight
|
|
15
|
+
* deduplication (P5.4, G.4, IC-017) and, when enabled, reuse a recorded PASS (089).
|
|
16
|
+
*
|
|
17
|
+
* Key (payload version 2) = sha16(JSON.stringify([
|
|
18
|
+
* 2,
|
|
19
|
+
* command,
|
|
20
|
+
* timeoutMs,
|
|
21
|
+
* realpath(cwd),
|
|
22
|
+
* HEAD,
|
|
23
|
+
* treeDigest, modified/untracked files, ignored entries removed (unchanged from version 1)
|
|
24
|
+
* ignoredDigest, sorted names of ignored entries (`!!`), `none` when there are none
|
|
25
|
+
* inputsDigest, bytes of the registry's policy.reuse.ignoredInputs, `absent` when missing
|
|
26
|
+
* linksDigest, bytes of the OUT-OF-REPO targets of tracked symlinks (mode 120000), sha256('none') when none
|
|
27
|
+
* toolDigest, realpath/size/mtime/inode of `node` and of the command's first word on env.PATH
|
|
28
|
+
* process.version,
|
|
29
|
+
* platform,
|
|
30
|
+
* arch,
|
|
31
|
+
* envDigest
|
|
32
|
+
* ]))
|
|
33
|
+
*
|
|
34
|
+
* `checkKeyV2` also returns the full sha256 of the same payload (`payloadDigest`) for the
|
|
35
|
+
* reuse store's trust check; `checkKey` returns the key only. One `git status` call yields
|
|
36
|
+
* both the tree digest and the ignored set (`treeState`).
|
|
37
|
+
*
|
|
38
|
+
* If git faults, more than 500 files or 50 MB are modified/untracked, or a declared input is a
|
|
39
|
+
* symlink, a non-file, unreadable or over 1 MB, the key is null (unkeyable, runs unshared).
|
|
40
|
+
* REUSE-UNKEYABLE (089 review C-3, C-4): when a tracked symlink points at a directory or an
|
|
41
|
+
* unreadable/oversized target outside the repository (or there are over 500 links), or the
|
|
42
|
+
* command's first word cannot be parsed safely (quotes, escapes, `$`, backtick, `~`, a glob, an
|
|
43
|
+
* inline `PATH=`), `checkKeyV2` still returns `key` (in-flight sharing and telemetry use it) but
|
|
44
|
+
* `payloadDigest` is null, so the check is neither looked up nor stored.
|
|
45
|
+
* Residual (design RK-1): an ignored file outside the declared list that changes content, and
|
|
46
|
+
* a tool behind a wrapper, are not seen.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
const MAX_UNCOMMITTED_FILES = 500;
|
|
50
|
+
const MAX_UNCOMMITTED_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
51
|
+
const MAX_INPUT_BYTES = 1024 * 1024; // 1 MB per declared ignored input
|
|
52
|
+
const PAYLOAD_VERSION = 2;
|
|
53
|
+
/** Stands for an ignored-entry digest that could not be read; never equals a sha256 hex digest. */
|
|
54
|
+
const IGNORED_UNAVAILABLE = 'unavailable';
|
|
55
|
+
/** Tracked symlinks are not read (the caller asked for no link read; telemetry/in-flight keys only). */
|
|
56
|
+
const LINKS_SKIPPED = 'skipped';
|
|
57
|
+
/** A tracked symlink whose out-of-repo target cannot be keyed: the check is reuse-unkeyable. */
|
|
58
|
+
const LINKS_UNKEYABLE = 'unkeyable';
|
|
59
|
+
const MAX_TRACKED_LINKS = 500;
|
|
60
|
+
|
|
61
|
+
/** First words that are shell syntax, not a program on PATH (IC-001 toolDigest). */
|
|
62
|
+
const SHELL_KEYWORDS = new Set([
|
|
63
|
+
'if', 'then', 'else', 'elif', 'fi', 'for', 'while', 'until', 'do', 'done', 'case', 'esac',
|
|
64
|
+
'select', 'function', 'time', 'coproc', '!', '{', '}', '[[', ']]', '(', ')',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
function sha256(text ) {
|
|
68
|
+
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Registry-owned inputs of the key; a load fault makes a check unkeyable at the caller. */
|
|
72
|
+
function keyPolicy() {
|
|
73
|
+
const { loadVerificationRegistry } = require('./registry.js');
|
|
74
|
+
const policy = loadVerificationRegistry().policy;
|
|
75
|
+
const exclude = policy?.dedup?.envExclude;
|
|
76
|
+
if (!Array.isArray(exclude) || !exclude.every((key ) => typeof key === 'string')) {
|
|
77
|
+
throw new Error('Invalid dedup environment exclusions');
|
|
78
|
+
}
|
|
79
|
+
const inputs = policy?.reuse?.ignoredInputs;
|
|
80
|
+
if (!Array.isArray(inputs) || !inputs.every((entry ) => typeof entry === 'string')) {
|
|
81
|
+
throw new Error('Invalid reuse ignored inputs');
|
|
82
|
+
}
|
|
83
|
+
return { envExclude: exclude, ignoredInputs: inputs };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function envExclude() {
|
|
87
|
+
return keyPolicy().envExclude;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Computes sha256 of sorted NAME=VALUE lines minus excluded environment variables.
|
|
92
|
+
* @param {Record<string, string | undefined>} [env=process.env]
|
|
93
|
+
* @param {readonly string[]} [exclude=registry policy.dedup.envExclude]
|
|
94
|
+
* @returns {string} sha256 hex digest
|
|
95
|
+
*/
|
|
96
|
+
function envDigest(
|
|
97
|
+
env = process.env,
|
|
98
|
+
exclude = envExclude(),
|
|
99
|
+
) {
|
|
100
|
+
const excludeSet = new Set(exclude);
|
|
101
|
+
const lines = [];
|
|
102
|
+
for (const [key, value] of Object.entries(env)) {
|
|
103
|
+
if (value === undefined || excludeSet.has(key)) continue;
|
|
104
|
+
// Raw NAME=VALUE lines must be unambiguous. Multiline environments run unshared
|
|
105
|
+
// rather than collide with a second environment containing an additional key.
|
|
106
|
+
if (key.includes('=') || key.includes('\n') || value.includes('\n')) {
|
|
107
|
+
throw new Error('Unkeyable environment');
|
|
108
|
+
}
|
|
109
|
+
lines.push(`${key}=${value}`);
|
|
110
|
+
}
|
|
111
|
+
lines.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
112
|
+
return crypto.createHash('sha256').update(lines.join('\n'), 'utf8').digest('hex');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The HEAD commit hash of the git repository at root, or null on git fault.
|
|
117
|
+
* @param {string} root
|
|
118
|
+
* @param {{ spawnSyncImpl?: typeof spawnSync }} [options]
|
|
119
|
+
* @returns {string | null} 40- or 64-character hex commit, or null
|
|
120
|
+
*/
|
|
121
|
+
function headCommit(root , { spawnSyncImpl = spawnSync } = {}) {
|
|
122
|
+
try {
|
|
123
|
+
const res = spawnSyncImpl('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' });
|
|
124
|
+
if (res.status !== 0 || typeof res.stdout !== 'string') return null;
|
|
125
|
+
const trimmed = res.stdout.trim();
|
|
126
|
+
return /^[0-9a-f]{40,64}$/.test(trimmed) ? trimmed : null;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** What one `git status` call says about the working tree (089 IC-001). */
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
/** `links: false` skips the `git ls-files` spawn (reuse off: nothing reads the link digest). */
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Runs the ONE `git status --porcelain=v1 -z --untracked-files=all --ignored=matching` call and
|
|
156
|
+
* splits it: modified/untracked entries (runtime-state directories left out) and the digest of the
|
|
157
|
+
* ignored (`!!`) entry names. Null on a git fault or a malformed rename entry.
|
|
158
|
+
*/
|
|
159
|
+
function readStatus(
|
|
160
|
+
root ,
|
|
161
|
+
{ spawnSyncImpl = spawnSync, fsImpl = nodeFs, configDir, links: readLinks = true } ,
|
|
162
|
+
) {
|
|
163
|
+
const top = spawnSyncImpl('git', ['rev-parse', '--show-toplevel'], { cwd: root, encoding: 'utf8' });
|
|
164
|
+
if (top.status !== 0 || typeof top.stdout !== 'string' || !path.isAbsolute(top.stdout.trim())) return null;
|
|
165
|
+
const gitRoot = fsImpl.realpathSync(top.stdout.trim());
|
|
166
|
+
// 089 review C-5: runtime state is left out, tracked project config is not. Under `.doflow` and
|
|
167
|
+
// the config dir, untracked (`??`) and ignored (`!!`) entries and everything in `state/` are
|
|
168
|
+
// excluded; a tracked entry (any other status) is an input and enters the tree digest.
|
|
169
|
+
const excluded = ['.doflow'];
|
|
170
|
+
if (configDir) {
|
|
171
|
+
// A first run may not have created its config directory yet. Resolve its nearest
|
|
172
|
+
// existing ancestor so a symlinked parent has the same identity after creation.
|
|
173
|
+
let ancestor = path.resolve(configDir);
|
|
174
|
+
const missing = [];
|
|
175
|
+
let realConfig ;
|
|
176
|
+
for (;;) {
|
|
177
|
+
try { realConfig = path.join(fsImpl.realpathSync(ancestor), ...missing); break; }
|
|
178
|
+
catch (error ) {
|
|
179
|
+
if (error.code !== 'ENOENT' || path.dirname(ancestor) === ancestor) throw error;
|
|
180
|
+
missing.unshift(path.basename(ancestor));
|
|
181
|
+
ancestor = path.dirname(ancestor);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const relative = path.relative(gitRoot, realConfig );
|
|
185
|
+
if (relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) {
|
|
186
|
+
excluded.push(relative.split(path.sep).join('/'));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const res = spawnSyncImpl(
|
|
190
|
+
'git',
|
|
191
|
+
['status', '--porcelain=v1', '-z', '--untracked-files=all', '--ignored=matching'],
|
|
192
|
+
{ cwd: root, encoding: 'utf8' },
|
|
193
|
+
);
|
|
194
|
+
if (res.status !== 0 || typeof res.stdout !== 'string') return null;
|
|
195
|
+
|
|
196
|
+
const raw = res.stdout;
|
|
197
|
+
const tokens = raw.split('\0');
|
|
198
|
+
// The trailing null produces an empty token at the end
|
|
199
|
+
if (tokens.length > 0 && tokens[tokens.length - 1] === '') {
|
|
200
|
+
tokens.pop();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const entries = [];
|
|
204
|
+
const ignoredPaths = [];
|
|
205
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
206
|
+
const token = tokens[i];
|
|
207
|
+
if (!token) continue;
|
|
208
|
+
const status = token.slice(0, 2);
|
|
209
|
+
const relPath = token.slice(3);
|
|
210
|
+
let entry = token;
|
|
211
|
+
// Both names are part of a rename/copy entry; dropping the source can give two
|
|
212
|
+
// different uncommitted trees the same digest when their destination bytes match.
|
|
213
|
+
if (/[RC]/.test(status)) {
|
|
214
|
+
const source = tokens[++i];
|
|
215
|
+
if (!source) return null;
|
|
216
|
+
entry += `\0${source}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Only runtime state is excluded; a tracked file under .doflow or the config dir is an input.
|
|
220
|
+
if (excluded.some((dir) => (relPath === dir || relPath.startsWith(`${dir}/`))
|
|
221
|
+
&& (status === '??' || status === '!!' || relPath === `${dir}/state` || relPath.startsWith(`${dir}/state/`)))) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// Ignored entries (`!!`) are named in the key, never read: the tree digest stays what it was.
|
|
225
|
+
if (status === '!!') {
|
|
226
|
+
ignoredPaths.push(relPath);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
entries.push({ entry, relPath });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
ignoredPaths.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
233
|
+
const links = readLinks ? linksDigest(gitRoot, { spawnSyncImpl, fsImpl }) : LINKS_SKIPPED;
|
|
234
|
+
return { gitRoot, entries, ignored: sha256(ignoredPaths.length === 0 ? 'none' : ignoredPaths.join('\0')), links };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function insideRoot(root , target ) {
|
|
238
|
+
const relative = path.relative(root, target);
|
|
239
|
+
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Digest of the OUT-OF-REPO targets of tracked symlinks (089 review C-3). One `git ls-files -s -z`;
|
|
244
|
+
* entries of mode 120000. A target inside the git root is covered by the tree digest or the
|
|
245
|
+
* ignored set and skipped. A target outside is a regular file of at most 1 MB (`target\0sha256`)
|
|
246
|
+
* or missing (`target\0absent`); a directory, another type, an unreadable or oversized target,
|
|
247
|
+
* more than 500 links or a git fault gives LINKS_UNKEYABLE (the check is not reused).
|
|
248
|
+
*/
|
|
249
|
+
function linksDigest(gitRoot , { spawnSyncImpl = spawnSync, fsImpl = nodeFs } ) {
|
|
250
|
+
try {
|
|
251
|
+
const res = spawnSyncImpl('git', ['ls-files', '-s', '-z'], { cwd: gitRoot, encoding: 'utf8' });
|
|
252
|
+
if (res.status !== 0 || typeof res.stdout !== 'string') return LINKS_UNKEYABLE;
|
|
253
|
+
const links = new Set ();
|
|
254
|
+
for (const token of res.stdout.split('\0')) {
|
|
255
|
+
const match = /^120000 [0-9a-f]+ \d\t([\s\S]+)$/.exec(token);
|
|
256
|
+
if (match) links.add(match[1]);
|
|
257
|
+
}
|
|
258
|
+
if (links.size === 0) return sha256('none');
|
|
259
|
+
if (links.size > MAX_TRACKED_LINKS) return LINKS_UNKEYABLE;
|
|
260
|
+
const lines = [];
|
|
261
|
+
for (const rel of links) {
|
|
262
|
+
const full = path.resolve(gitRoot, rel);
|
|
263
|
+
let lstat ;
|
|
264
|
+
try { lstat = fsImpl.lstatSync(full); } catch (error ) {
|
|
265
|
+
if (error.code === 'ENOENT') continue; // removed: the status entry covers it
|
|
266
|
+
return LINKS_UNKEYABLE;
|
|
267
|
+
}
|
|
268
|
+
if (!lstat.isSymbolicLink()) continue; // replaced by a file: the status entry covers it
|
|
269
|
+
let real ;
|
|
270
|
+
try { real = fsImpl.realpathSync(full); } catch (error ) {
|
|
271
|
+
if (error.code !== 'ENOENT') return LINKS_UNKEYABLE;
|
|
272
|
+
const lexical = path.resolve(path.dirname(full), fsImpl.readlinkSync(full));
|
|
273
|
+
if (!insideRoot(gitRoot, lexical)) lines.push(`${lexical}\0absent`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (insideRoot(gitRoot, real)) continue;
|
|
277
|
+
const stat = fsImpl.statSync(real);
|
|
278
|
+
if (!stat.isFile() || stat.size > MAX_INPUT_BYTES) return LINKS_UNKEYABLE;
|
|
279
|
+
const content = fsImpl.readFileSync(real);
|
|
280
|
+
if (content.length > MAX_INPUT_BYTES) return LINKS_UNKEYABLE;
|
|
281
|
+
lines.push(`${real}\0${crypto.createHash('sha256').update(content).digest('hex')}`);
|
|
282
|
+
}
|
|
283
|
+
if (lines.length === 0) return sha256('none');
|
|
284
|
+
lines.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
285
|
+
return sha256(lines.join('\n'));
|
|
286
|
+
} catch {
|
|
287
|
+
return LINKS_UNKEYABLE;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Digest of the modified/untracked entries: reads each file's bytes. Null when more than 500
|
|
293
|
+
* files or 50 MB are dirty, or an entry is a symlink, a non-file or unreadable.
|
|
294
|
+
*/
|
|
295
|
+
function hashEntries(
|
|
296
|
+
gitRoot ,
|
|
297
|
+
entries ,
|
|
298
|
+
fsImpl ,
|
|
299
|
+
) {
|
|
300
|
+
if (entries.length === 0) {
|
|
301
|
+
return crypto.createHash('sha256').update('clean', 'utf8').digest('hex');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (entries.length > MAX_UNCOMMITTED_FILES) {
|
|
305
|
+
return null; // More than 500 uncommitted files -> unkeyable
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Sort entries deterministically
|
|
309
|
+
entries.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
|
|
310
|
+
|
|
311
|
+
const hasher = crypto.createHash('sha256');
|
|
312
|
+
let totalBytes = 0;
|
|
313
|
+
|
|
314
|
+
for (const item of entries) {
|
|
315
|
+
const fullPath = path.resolve(gitRoot, item.relPath);
|
|
316
|
+
let fileHash = '';
|
|
317
|
+
try {
|
|
318
|
+
if (typeof fsImpl.lstatSync === 'function' && fsImpl.lstatSync(fullPath).isSymbolicLink()) return null;
|
|
319
|
+
const stat = fsImpl.statSync(fullPath);
|
|
320
|
+
if (!stat.isFile()) return null; // directory/submodule contents are not represented by porcelain
|
|
321
|
+
if (stat.isFile()) {
|
|
322
|
+
totalBytes += stat.size;
|
|
323
|
+
if (totalBytes > MAX_UNCOMMITTED_BYTES) {
|
|
324
|
+
return null; // More than 50 MB total dirty content -> unkeyable
|
|
325
|
+
}
|
|
326
|
+
const content = fsImpl.readFileSync(fullPath);
|
|
327
|
+
fileHash = crypto.createHash('sha256').update(content).digest('hex');
|
|
328
|
+
}
|
|
329
|
+
} catch (error ) {
|
|
330
|
+
// A deletion has no content; an unreadable live file cannot be safely keyed.
|
|
331
|
+
if (error.code !== 'ENOENT' || !item.entry.slice(0, 2).includes('D')) return null;
|
|
332
|
+
}
|
|
333
|
+
hasher.update(item.entry, 'utf8');
|
|
334
|
+
hasher.update('\0', 'utf8');
|
|
335
|
+
hasher.update(fileHash, 'utf8');
|
|
336
|
+
hasher.update('\0', 'utf8');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return hasher.digest('hex');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Computes the tree digest and the ignored-entry digest from ONE git status call.
|
|
344
|
+
* Returns null if not in a git repository, or if > 500 files or > 50 MB are dirty.
|
|
345
|
+
* @param {string} root
|
|
346
|
+
* @param {{ spawnSyncImpl?: typeof spawnSync, fsImpl?: typeof nodeFs, configDir?: string }} [options]
|
|
347
|
+
* @returns {TreeState | null}
|
|
348
|
+
*/
|
|
349
|
+
function treeState(root , options = {}) {
|
|
350
|
+
try {
|
|
351
|
+
const status = readStatus(root, options);
|
|
352
|
+
if (!status) return null;
|
|
353
|
+
const tree = hashEntries(status.gitRoot, status.entries, options.fsImpl || nodeFs);
|
|
354
|
+
return tree === null ? null : { tree, ignored: status.ignored, gitRoot: status.gitRoot, links: status.links };
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* IC-001 compatibility seam for tests; the runner uses checkKeyV2/treeState.
|
|
362
|
+
* Computes a digest of all modified and untracked files in the working tree.
|
|
363
|
+
* Returns null if not in a git repository, or if > 500 files or > 50 MB are dirty.
|
|
364
|
+
* Values for trees without ignored entries are those of payload version 1.
|
|
365
|
+
* @param {string} root
|
|
366
|
+
* @param {{ spawnSyncImpl?: typeof spawnSync, fsImpl?: typeof nodeFs, configDir?: string }} [options]
|
|
367
|
+
* @returns {string | null}
|
|
368
|
+
*/
|
|
369
|
+
function treeDigest(root , options = {}) {
|
|
370
|
+
return treeState(root, options)?.tree ?? null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Digest of the declared ignored inputs (089 IC-001, DA-1): `path\0sha256(bytes)` or
|
|
375
|
+
* `path\0absent` per path, in registry order, relative to the git root. A symlink, a
|
|
376
|
+
* non-file, a read fault other than ENOENT or a file over 1 MB makes the key null.
|
|
377
|
+
* @returns {string | null}
|
|
378
|
+
*/
|
|
379
|
+
function inputsDigest(gitRoot , inputs , fsImpl = nodeFs) {
|
|
380
|
+
try {
|
|
381
|
+
const lines = [];
|
|
382
|
+
for (const rel of inputs) {
|
|
383
|
+
const full = path.resolve(gitRoot, rel);
|
|
384
|
+
let stat ;
|
|
385
|
+
try { stat = fsImpl.lstatSync(full); }
|
|
386
|
+
catch (error ) {
|
|
387
|
+
if (error.code === 'ENOENT') { lines.push(`${rel}\0absent`); continue; }
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_INPUT_BYTES) return null;
|
|
391
|
+
const content = fsImpl.readFileSync(full);
|
|
392
|
+
if (content.length > MAX_INPUT_BYTES) return null;
|
|
393
|
+
lines.push(`${rel}\0${crypto.createHash('sha256').update(content).digest('hex')}`);
|
|
394
|
+
}
|
|
395
|
+
return sha256(lines.join('\n'));
|
|
396
|
+
} catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* First word of a shell command, past leading NAME=value assignments and quotes, and whether the
|
|
403
|
+
* parser can trust it (089 review C-4). Not reusable: the first word or a leading assignment holds
|
|
404
|
+
* a quote, a backslash, `$` or a backtick (the shell expands or joins what the whitespace split
|
|
405
|
+
* cut), a leading assignment sets PATH (the tool resolves on another PATH), or the first word
|
|
406
|
+
* starts with `~` or holds `*`, `?` or `[` (the shell expands it). `word` is still the key's tool.
|
|
407
|
+
*/
|
|
408
|
+
function firstShellWord(command ) {
|
|
409
|
+
let reusable = true;
|
|
410
|
+
for (const word of command.trim().split(/\s+/)) {
|
|
411
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) {
|
|
412
|
+
if (/["'\\$`]/.test(word) || word.startsWith('PATH=')) reusable = false;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (/["'\\$`*?[]/.test(word) || word.startsWith('~')) reusable = false;
|
|
416
|
+
return { word: word.replace(/^["']+|["']+$/g, ''), reusable };
|
|
417
|
+
}
|
|
418
|
+
return { word: '', reusable };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Identity of one program: its first match on PATH (PATHEXT on win32), realpath, size,
|
|
423
|
+
* mtimeMs and inode, or `absent`. A word that is a shell keyword or contains a slash is
|
|
424
|
+
* taken as a path relative to cwd. No process is spawned.
|
|
425
|
+
*/
|
|
426
|
+
function toolIdentity(
|
|
427
|
+
word ,
|
|
428
|
+
{ cwd, env, fsImpl, platform } ,
|
|
429
|
+
) {
|
|
430
|
+
if (word === '') return `${word}\0absent`;
|
|
431
|
+
const win = platform === 'win32';
|
|
432
|
+
const direct = SHELL_KEYWORDS.has(word) || word.includes('/') || (win && word.includes('\\'));
|
|
433
|
+
const extensions = win
|
|
434
|
+
? ['', ...String(env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)]
|
|
435
|
+
: [''];
|
|
436
|
+
const candidates = [];
|
|
437
|
+
if (direct) {
|
|
438
|
+
for (const ext of extensions) candidates.push(path.resolve(cwd, word + ext));
|
|
439
|
+
} else if (typeof env.PATH === 'string') {
|
|
440
|
+
for (const dir of env.PATH.split(win ? ';' : ':')) {
|
|
441
|
+
for (const ext of extensions) candidates.push(path.resolve(cwd, dir === '' ? '.' : dir, word + ext));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const candidate of candidates) {
|
|
445
|
+
let isFile = false;
|
|
446
|
+
try { isFile = fsImpl.statSync(candidate).isFile(); } catch { isFile = false; }
|
|
447
|
+
if (!isFile) continue;
|
|
448
|
+
const real = fsImpl.realpathSync(candidate);
|
|
449
|
+
const stat = fsImpl.statSync(real);
|
|
450
|
+
return [word, real, stat.size, stat.mtimeMs, stat.ino].join('\0');
|
|
451
|
+
}
|
|
452
|
+
return `${word}\0absent`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Digest of the identity of `node` and of the command's first word (089 IC-001, DA-2).
|
|
457
|
+
* @returns {string}
|
|
458
|
+
*/
|
|
459
|
+
function toolDigest(
|
|
460
|
+
command ,
|
|
461
|
+
{ cwd, env = process.env, fsImpl = nodeFs, platform = process.platform }
|
|
462
|
+
,
|
|
463
|
+
) {
|
|
464
|
+
const io = { cwd, env, fsImpl, platform };
|
|
465
|
+
return sha256([toolIdentity('node', io), toolIdentity(firstShellWord(command).word, io)].join('\n'));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The version 2 payload (089 IC-001); the order of the entries is part of the key's identity. */
|
|
469
|
+
function payloadOf(parts
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
) {
|
|
473
|
+
const { command, timeoutMs, realCwd, head, tree, ignoredDig, inputsDig, linksDig, toolDig, envDig } = parts;
|
|
474
|
+
const payload = [
|
|
475
|
+
PAYLOAD_VERSION,
|
|
476
|
+
command,
|
|
477
|
+
timeoutMs,
|
|
478
|
+
realCwd,
|
|
479
|
+
head,
|
|
480
|
+
tree,
|
|
481
|
+
ignoredDig,
|
|
482
|
+
inputsDig,
|
|
483
|
+
linksDig,
|
|
484
|
+
toolDig,
|
|
485
|
+
process.version,
|
|
486
|
+
process.platform,
|
|
487
|
+
(process ).arch,
|
|
488
|
+
envDig,
|
|
489
|
+
];
|
|
490
|
+
return payload;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Computes the version 2 check key and the full payload digest for a command run in cwd
|
|
514
|
+
* (089 IC-001). Returns null if unkeyable; any thrown error is null.
|
|
515
|
+
* @param {Object} input
|
|
516
|
+
* @param {string} input.command
|
|
517
|
+
* @param {number} input.timeoutMs
|
|
518
|
+
* @param {string} input.cwd
|
|
519
|
+
* @param {string | null} [input.head] HEAD commit; resolved from cwd if omitted
|
|
520
|
+
* @param {string | null} [input.tree] treeDigest; resolved from cwd if omitted
|
|
521
|
+
* @param {string | null} [input.ignored] ignored-entry digest from treeState; resolved with one
|
|
522
|
+
* status call when omitted (also resolves `tree` when that is omitted too)
|
|
523
|
+
* @param {Record<string, string | undefined>} [input.env=process.env]
|
|
524
|
+
* @returns {{ key: string, payloadDigest: string | null } | null} `payloadDigest` is null when the
|
|
525
|
+
* check is reuse-unkeyable (unparseable first word, unkeyable or unread tracked symlinks); the
|
|
526
|
+
* `key` is still returned for in-flight sharing and telemetry.
|
|
527
|
+
*/
|
|
528
|
+
function checkKeyV2({
|
|
529
|
+
command,
|
|
530
|
+
timeoutMs,
|
|
531
|
+
cwd,
|
|
532
|
+
head,
|
|
533
|
+
tree,
|
|
534
|
+
ignored,
|
|
535
|
+
gitRoot,
|
|
536
|
+
links,
|
|
537
|
+
configDir,
|
|
538
|
+
env = process.env,
|
|
539
|
+
fsImpl = nodeFs,
|
|
540
|
+
spawnSyncImpl = spawnSync,
|
|
541
|
+
platform = process.platform,
|
|
542
|
+
} ) {
|
|
543
|
+
try {
|
|
544
|
+
if (typeof command !== 'string' || !Number.isFinite(timeoutMs) || typeof cwd !== 'string') {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const realCwd = fsImpl.realpathSync(cwd);
|
|
549
|
+
|
|
550
|
+
let resolvedHead = head;
|
|
551
|
+
if (resolvedHead === undefined) {
|
|
552
|
+
resolvedHead = headCommit(realCwd, { spawnSyncImpl });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let resolvedTree = tree;
|
|
556
|
+
let resolvedIgnored = ignored;
|
|
557
|
+
let resolvedRoot = gitRoot;
|
|
558
|
+
let resolvedLinks = links;
|
|
559
|
+
if (resolvedTree === undefined) {
|
|
560
|
+
// Nothing precomputed: one status call supplies the tree, the ignored set and the root.
|
|
561
|
+
const state = treeState(realCwd, { spawnSyncImpl, fsImpl, configDir });
|
|
562
|
+
if (!state) return null;
|
|
563
|
+
resolvedTree = state.tree;
|
|
564
|
+
if (resolvedIgnored === undefined) resolvedIgnored = state.ignored;
|
|
565
|
+
if (resolvedRoot === undefined) resolvedRoot = state.gitRoot;
|
|
566
|
+
if (resolvedLinks === undefined) resolvedLinks = state.links;
|
|
567
|
+
} else if (resolvedIgnored === undefined || resolvedRoot === undefined || resolvedLinks === undefined) {
|
|
568
|
+
// A tree without its ignored set: one status call, without reading any dirty file again.
|
|
569
|
+
// The caller already read the tree, so a fault here means this directory is not a git
|
|
570
|
+
// checkout (a test seam); the key then carries IGNORED_UNAVAILABLE, which no healthy
|
|
571
|
+
// digest equals, and the declared inputs are read from cwd.
|
|
572
|
+
let status = null;
|
|
573
|
+
try { status = readStatus(realCwd, { spawnSyncImpl, fsImpl, configDir }); } catch { status = null; }
|
|
574
|
+
if (resolvedIgnored === undefined) resolvedIgnored = status ? status.ignored : IGNORED_UNAVAILABLE;
|
|
575
|
+
if (resolvedRoot === undefined) resolvedRoot = status ? status.gitRoot : realCwd;
|
|
576
|
+
if (resolvedLinks === undefined) resolvedLinks = status ? status.links : LINKS_UNKEYABLE;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!resolvedHead || !resolvedTree || !resolvedIgnored || !resolvedRoot) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
// A caller that supplied tree/ignored/root without links gets one read; null means not read.
|
|
583
|
+
const linksDig = typeof resolvedLinks === 'string' ? resolvedLinks : LINKS_SKIPPED;
|
|
584
|
+
|
|
585
|
+
const policy = keyPolicy();
|
|
586
|
+
const envDig = envDigest(env, policy.envExclude);
|
|
587
|
+
const ignoredDig = resolvedIgnored;
|
|
588
|
+
const inputsDig = inputsDigest(resolvedRoot, policy.ignoredInputs, fsImpl);
|
|
589
|
+
if (inputsDig === null) return null;
|
|
590
|
+
const toolDig = toolDigest(command, { cwd: realCwd, env, fsImpl, platform });
|
|
591
|
+
const payload = payloadOf({
|
|
592
|
+
command, timeoutMs, realCwd, head: resolvedHead, tree: resolvedTree,
|
|
593
|
+
ignoredDig, inputsDig, linksDig, toolDig, envDig,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const payloadDigest = sha256(JSON.stringify(payload));
|
|
597
|
+
// Reuse-unkeyable (089 review C-3, C-4): the key is kept, the stored-PASS identity is not.
|
|
598
|
+
const reusable = linksDig !== LINKS_UNKEYABLE && linksDig !== LINKS_SKIPPED
|
|
599
|
+
&& firstShellWord(command).reusable;
|
|
600
|
+
return { key: payloadDigest.slice(0, 16), payloadDigest: reusable ? payloadDigest : null };
|
|
601
|
+
} catch {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* IC-001 compatibility seam for tests; the runner uses checkKeyV2/treeState.
|
|
608
|
+
* Computes checkKey for a command run in cwd.
|
|
609
|
+
* Returns 16 hex chars, or null if unkeyable.
|
|
610
|
+
* @param {Object} input see checkKeyV2
|
|
611
|
+
* @returns {string | null}
|
|
612
|
+
*/
|
|
613
|
+
function checkKey(input ) {
|
|
614
|
+
return checkKeyV2(input)?.key ?? null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// `checkKey` and `treeDigest` are IC-001 compatibility seams for tests; the runner uses
|
|
618
|
+
// checkKeyV2/treeState.
|
|
619
|
+
module.exports = {
|
|
620
|
+
envDigest,
|
|
621
|
+
headCommit,
|
|
622
|
+
treeDigest,
|
|
623
|
+
treeState,
|
|
624
|
+
inputsDigest,
|
|
625
|
+
toolDigest,
|
|
626
|
+
checkKey,
|
|
627
|
+
checkKeyV2,
|
|
628
|
+
};
|