@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,617 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Feature 082 B.4 (IC-016, IC-017, IC-018, IC-020, IC-021; DEC-004, DEC-014, DEC-022, DEC-023,
|
|
4
|
+
// DEC-036, DEC-049, DEC-050): integration.
|
|
5
|
+
//
|
|
6
|
+
// Worktrees. Every dispatch worktree lives under `<root>/.doflow/state/dispatch-worktrees/<id>`,
|
|
7
|
+
// made through `WorktreeManager` with a branch prefix per kind, a 120 s bound and an env without
|
|
8
|
+
// the repository variables (IC-016). The integration worktree (`p-` id, branch
|
|
9
|
+
// `doflow/<slug>/<p-id>`) is never removed here; it is handed to the user (DEC-004).
|
|
10
|
+
//
|
|
11
|
+
// Attempt commit (IC-017). The changed paths of an attempt worktree, filtered of the base pointer
|
|
12
|
+
// and of DoFlow's declared state stores, are staged into a temporary index on the attempt base and
|
|
13
|
+
// written as one commit `C` with parent `b` by `commit-tree`. The worktree's own index, branch and
|
|
14
|
+
// files are never touched. Identity is set in the child env only; git's settings are never read for
|
|
15
|
+
// it or changed, and no hook-skipping flag exists here (DEC-036).
|
|
16
|
+
//
|
|
17
|
+
// Integration (IC-018). One dispatch per call, under the store's lease: `cherry-pick --no-commit`
|
|
18
|
+
// of `C` on the integration HEAD, `write-tree`, `commit-tree` on the pre-integration head, then
|
|
19
|
+
// `update-ref` naming that head as the expected old value, so a concurrent move of the branch fails
|
|
20
|
+
// the update instead of being overwritten (DEC-050). Verification follows on the integrated HEAD
|
|
21
|
+
// (IC-019), then B.3's end rule finishes or releases the 080 attempt inside the dispatch lock.
|
|
22
|
+
// Undo is `reset --hard` to the pre-integration head, run only while the integration branch is
|
|
23
|
+
// checked out there and points at a head this integration made (IC-020).
|
|
24
|
+
//
|
|
25
|
+
// Locks. Each locked step is one `mutateDispatch`; its decide callback makes only the in-process
|
|
26
|
+
// 080 calls of `attempt-end.ts`. Git, the verification spawn and worktree removal all run between
|
|
27
|
+
// lock sections (IC-006). The 080 ledger is read before a section that checks staleness.
|
|
28
|
+
//
|
|
29
|
+
// This module runs git only inside DoFlow-owned worktrees and on refs under the two DoFlow branch
|
|
30
|
+
// prefixes; the worktree list of `root` is changed only through `WorktreeManager`.
|
|
31
|
+
|
|
32
|
+
const fs = require('node:fs');
|
|
33
|
+
const path = require('node:path');
|
|
34
|
+
const { spawnSync } = require('node:child_process');
|
|
35
|
+
const { WorktreeManager, BASE_FILE } = require('../worktree.js');
|
|
36
|
+
const { storeRules, withoutDeclaredStores } = require('../worker/worktree-check.js');
|
|
37
|
+
const { procStart } = require('../worker/process-tree.js');
|
|
38
|
+
const { readTaskState } = require('../task-state.js');
|
|
39
|
+
const { storeFileFor, mutateDispatch, readDispatch, transitionDispatch, setRunState, DispatchStoreError } = require('./store.js');
|
|
40
|
+
const { REPO_ENV, runVerification } = require('./verify-run.js');
|
|
41
|
+
const { endAttempt, completeAttempt, releaseAttempt, usageFor } = require('./attempt-end.js');
|
|
42
|
+
|
|
43
|
+
const WORKTREES_DIR = path.join('.doflow', 'state', 'dispatch-worktrees');
|
|
44
|
+
/** Bound on a git call that writes (IC-016). */
|
|
45
|
+
const WRITE_TIMEOUT_MS = 120000;
|
|
46
|
+
/** Bound on a read-only git call (IC-016). */
|
|
47
|
+
const READ_TIMEOUT_MS = 10000;
|
|
48
|
+
/** The identity every commit this module makes carries, set in the child env only (DEC-036). */
|
|
49
|
+
const DISPATCH_IDENTITY = Object.freeze({
|
|
50
|
+
GIT_AUTHOR_NAME: 'DoFlow dispatch',
|
|
51
|
+
GIT_AUTHOR_EMAIL: 'dispatch@doflow.invalid',
|
|
52
|
+
GIT_COMMITTER_NAME: 'DoFlow dispatch',
|
|
53
|
+
GIT_COMMITTER_EMAIL: 'dispatch@doflow.invalid',
|
|
54
|
+
});
|
|
55
|
+
const TEMP_INDEX = 'index.doflow-dispatch';
|
|
56
|
+
const SHA = /^[0-9a-f]{40,64}$/;
|
|
57
|
+
/** Dispositions whose worktree is removed once its worker cleanup is verified, or it had no worker (IC-021). */
|
|
58
|
+
const RELEASED_ENDS = Object.freeze(['cancelled', 'taken-over', 'stale', 'interrupted', 'released']);
|
|
59
|
+
/** Run states after which a failed attempt's kept worktree is removed (IC-021). */
|
|
60
|
+
const RUN_ENDS = Object.freeze(['completed', 'cancelled']);
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/** A git failure in this module's contract: `reason` a fixed token, `exitCode` git's exit status. */
|
|
65
|
+
class IntegrationError extends Error {
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
constructor(reason , exitCode = null) {
|
|
70
|
+
super(`integration-error: ${reason}${exitCode === null ? '' : ` (git exit ${exitCode})`}`);
|
|
71
|
+
this.name = 'IntegrationError';
|
|
72
|
+
this.code = 'integration-error';
|
|
73
|
+
this.reason = reason;
|
|
74
|
+
this.exitCode = exitCode;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `env` without the variables that would point git at another repository (081 `REPO_ENV`). */
|
|
79
|
+
function gitEnv(env = process.env, extra = {}) {
|
|
80
|
+
const out = { ...env };
|
|
81
|
+
for (const key of REPO_ENV) delete out[key];
|
|
82
|
+
return { ...out, ...extra };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readEnv(env ) {
|
|
86
|
+
return gitEnv(env, { GIT_OPTIONAL_LOCKS: '0' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One bounded `git -C <cwd>` call. Never throws: `{ ok, status, stdout }`, `status` the exit code
|
|
90
|
+
* as a string, else the signal or spawn error code. */
|
|
91
|
+
function runGit(cwd , args , { env, timeout, input } ) {
|
|
92
|
+
const r = spawnSync('git', ['-C', cwd, ...args], {
|
|
93
|
+
env, timeout, encoding: 'utf8', input: input ?? '', maxBuffer: 64 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'],
|
|
94
|
+
});
|
|
95
|
+
const status = r.status ?? r.signal ?? (r.error )?.code ?? 'unknown';
|
|
96
|
+
return { ok: r.status === 0 && !r.error, status: String(status), stdout: r.stdout || '' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const readGit = (cwd , args , env ) => runGit(cwd, args, { env: readEnv(env), timeout: READ_TIMEOUT_MS });
|
|
100
|
+
|
|
101
|
+
/** stdout of a git call that must succeed, trimmed, else an IntegrationError naming `reason`. */
|
|
102
|
+
function must(res , reason ) {
|
|
103
|
+
if (!res.ok) throw new IntegrationError(reason, res.status);
|
|
104
|
+
return res.stdout.trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sha(res , reason ) {
|
|
108
|
+
const out = must(res, reason);
|
|
109
|
+
if (!SHA.test(out)) throw new IntegrationError(reason, res.status);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── worktrees (IC-016) ───────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
function worktreesDir(root ) {
|
|
116
|
+
return path.join(root, WORKTREES_DIR);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The manager for one worktree kind: `doflow/<slug>/` for integration, `doflow-attempt/<slug>/` for
|
|
120
|
+
* attempt and child worktrees. */
|
|
121
|
+
function managerFor(root , slug , kind , env = process.env) {
|
|
122
|
+
const prefix = kind === 'integration' ? `doflow/${slug}/` : `doflow-attempt/${slug}/`;
|
|
123
|
+
return new WorktreeManager(root, worktreesDir(root), { branchPrefix: prefix, timeoutMs: WRITE_TIMEOUT_MS, env: gitEnv(env) });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function created(manager , id , base ) {
|
|
127
|
+
if (typeof base !== 'string' || !SHA.test(base)) throw new DispatchStoreError('usage', `worktree base '${base}' is not a commit id`);
|
|
128
|
+
const worktree = manager.create(id, base);
|
|
129
|
+
return { worktree, branch: manager.branchFor(id), base };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** IC-016: the run's integration worktree `p-<id>` on `doflow/<slug>/<p-id>`, cut from `base`
|
|
133
|
+
* (the caller's HEAD at start; read from `root` when omitted). `head` starts at the base. */
|
|
134
|
+
function createIntegrationWorktree({ root, slug, dispatchRunId, base = null, env = process.env } ) {
|
|
135
|
+
const from = base ?? sha(readGit(root, ['rev-parse', '--verify', '-q', 'HEAD^{commit}'], env), 'head-unreadable');
|
|
136
|
+
const made = created(managerFor(root, slug, 'integration', env), dispatchRunId, from);
|
|
137
|
+
return { ...made, head: from };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** IC-016: the attempt worktree `d-<id>` on `doflow-attempt/<slug>/<d-id>`, cut from `base` (the
|
|
141
|
+
* integration HEAD recorded in the admitted slot). */
|
|
142
|
+
function createAttemptWorktree({ root, slug, dispatchId, base, env = process.env } ) {
|
|
143
|
+
return created(managerFor(root, slug, 'attempt', env), dispatchId, base);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** IC-016: the child worktree `<d-id>.c<n>`, cut from the granter's attempt base. */
|
|
147
|
+
function createChildWorktree({ root, slug, dispatchId, n, base, env = process.env } ) {
|
|
148
|
+
if (!Number.isInteger(n) || n < 1) throw new DispatchStoreError('usage', `child number '${n}' is not a positive whole number`);
|
|
149
|
+
return created(managerFor(root, slug, 'attempt', env), `${dispatchId}.c${n}`, base);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── change lists ─────────────────────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/** IC-011: `git diff --name-status -z -M a b` in `cwd` as `[{ path, change, from }]`, `change` the
|
|
155
|
+
* status letter without its score. */
|
|
156
|
+
function diffNameStatus(a , b , { cwd, env = process.env } ) {
|
|
157
|
+
const out = must(readGit(cwd, ['diff', '--name-status', '-z', '-M', a, b], env), 'diff-failed');
|
|
158
|
+
const fields = out.split('\0').filter((f) => f !== '');
|
|
159
|
+
const changes = [];
|
|
160
|
+
let i = 0;
|
|
161
|
+
while (i < fields.length) {
|
|
162
|
+
const change = fields[i][0];
|
|
163
|
+
const renamed = change === 'R' || change === 'C';
|
|
164
|
+
changes.splice(changes.length, 0, renamed
|
|
165
|
+
? { path: fields[i + 2], change, from: fields[i + 1] }
|
|
166
|
+
: { path: fields[i + 1], change, from: null });
|
|
167
|
+
i += renamed ? 3 : 2;
|
|
168
|
+
}
|
|
169
|
+
return changes;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function nulList(text ) {
|
|
173
|
+
return text.split('\0').filter((f) => f !== '');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** IC-017 step 1: changed and untracked paths of `worktree` against `base`, less the base pointer
|
|
177
|
+
* and every path under a declared DoFlow state store. */
|
|
178
|
+
function attemptPaths(worktree , base , env ) {
|
|
179
|
+
const tracked = nulList(must(readGit(worktree, ['diff', '--name-only', '-z', '--no-renames', base], env), 'diff-failed'));
|
|
180
|
+
const untracked = nulList(must(readGit(worktree, ['ls-files', '--others', '--exclude-standard', '-z'], env), 'ls-files-failed'));
|
|
181
|
+
const rules = storeRules();
|
|
182
|
+
if (rules === null) throw new IntegrationError('storage-registry-unreadable');
|
|
183
|
+
const entries = {};
|
|
184
|
+
for (const rel of [...tracked, ...untracked]) if (rel !== BASE_FILE) entries[rel] = '';
|
|
185
|
+
return Object.keys(withoutDeclaredStores(entries, rules)).sort();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* IC-017: one commit `C` with the single parent `base` holding the attempt's changes, or
|
|
190
|
+
* `commit: null` for an empty change (DEC-049). `changes` is `diffNameStatus(base, C)`. Throws an
|
|
191
|
+
* IntegrationError for a git failure.
|
|
192
|
+
*/
|
|
193
|
+
function attemptCommit({ worktree, base, dispatchId, task, token, env = process.env } ) {
|
|
194
|
+
if (typeof base !== 'string' || !SHA.test(base)) throw new DispatchStoreError('usage', `attempt base '${base}' is not a commit id`);
|
|
195
|
+
const paths = attemptPaths(worktree, base, env);
|
|
196
|
+
if (paths.length === 0) return { base, commit: null, changes: [] };
|
|
197
|
+
|
|
198
|
+
const indexPath = path.resolve(worktree, must(readGit(worktree, ['rev-parse', '--git-path', TEMP_INDEX], env), 'git-path-failed'));
|
|
199
|
+
const writeEnv = gitEnv(env, { ...DISPATCH_IDENTITY, GIT_INDEX_FILE: indexPath, GIT_LITERAL_PATHSPECS: '1' });
|
|
200
|
+
const write = (args , reason , input ) => must(runGit(worktree, args, { env: writeEnv, timeout: WRITE_TIMEOUT_MS, input }), reason);
|
|
201
|
+
let commit = null;
|
|
202
|
+
try {
|
|
203
|
+
fs.rmSync(indexPath, { force: true });
|
|
204
|
+
write(['read-tree', base], 'read-tree-failed');
|
|
205
|
+
write(['add', '-A', '--pathspec-from-file=-', '--pathspec-file-nul'], 'add-failed', `${paths.join('\0')}\0`);
|
|
206
|
+
const tree = write(['write-tree'], 'write-tree-failed');
|
|
207
|
+
const baseTree = sha(readGit(worktree, ['rev-parse', `${base}^{tree}`], env), 'rev-parse-failed');
|
|
208
|
+
if (tree !== baseTree) {
|
|
209
|
+
commit = write(['commit-tree', tree, '-p', base, '-m', `dispatch ${dispatchId} task ${task} token ${token}`], 'commit-tree-failed');
|
|
210
|
+
if (!SHA.test(commit)) throw new IntegrationError('commit-tree-failed');
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
fs.rmSync(indexPath, { force: true });
|
|
214
|
+
}
|
|
215
|
+
if (commit === null) return { base, commit: null, changes: [] };
|
|
216
|
+
return { base, commit, changes: diffNameStatus(base, commit, { cwd: worktree, env }) };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── integration worktree checks and undo (IC-018 step 2, IC-020) ─────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/** IC-018 step 2: the integration worktree is on its branch at `preHead` and lists nothing but the
|
|
222
|
+
* base pointer. `{ clean, reason, paths }`. */
|
|
223
|
+
function integrationClean({ worktree, branch, preHead, env = process.env } ) {
|
|
224
|
+
const ref = readGit(worktree, ['symbolic-ref', '-q', 'HEAD'], env);
|
|
225
|
+
if (!ref.ok || ref.stdout.trim() !== `refs/heads/${branch}`) return { clean: false, reason: 'not-integration-branch', paths: 0 };
|
|
226
|
+
const head = readGit(worktree, ['rev-parse', '--verify', '-q', 'HEAD'], env);
|
|
227
|
+
if (!head.ok || head.stdout.trim() !== preHead) return { clean: false, reason: 'head-moved', paths: 0 };
|
|
228
|
+
const status = readGit(worktree, ['--no-optional-locks', 'status', '--porcelain=v1', '-z', '--untracked-files=all'], env);
|
|
229
|
+
if (!status.ok) return { clean: false, reason: 'status-failed', paths: 0 };
|
|
230
|
+
const listed = nulList(status.stdout).filter((f) => f.length > 3 && f.slice(3) !== BASE_FILE);
|
|
231
|
+
return listed.length ? { clean: false, reason: 'dirty', paths: listed.length } : { clean: true, reason: null, paths: 0 };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* IC-020: resets the integration worktree to `preHead`. Nothing runs unless HEAD is the run's
|
|
236
|
+
* integration branch (`not-integration-branch`) and points at `preHead` or at `pickedHead`, the
|
|
237
|
+
* commit this integration made (`head-moved`: another writer moved it, and resetting would
|
|
238
|
+
* overwrite that move). After the reset, HEAD must equal `preHead` and status list nothing but the
|
|
239
|
+
* base pointer, else `undo-failed`. `{ ok, ran, reason, exitCode }`.
|
|
240
|
+
*/
|
|
241
|
+
function undo({ worktree, branch, preHead, pickedHead = null, env = process.env } ) {
|
|
242
|
+
const ref = readGit(worktree, ['symbolic-ref', '-q', 'HEAD'], env);
|
|
243
|
+
if (!ref.ok || ref.stdout.trim() !== `refs/heads/${branch}`) return { ok: false, ran: false, reason: 'not-integration-branch', exitCode: null };
|
|
244
|
+
const head = readGit(worktree, ['rev-parse', '--verify', '-q', 'HEAD'], env).stdout.trim();
|
|
245
|
+
if (head !== preHead && (pickedHead === null || head !== pickedHead)) return { ok: false, ran: false, reason: 'head-moved', exitCode: null };
|
|
246
|
+
const reset = runGit(worktree, ['reset', '-q', '--hard', preHead], { env: gitEnv(env), timeout: WRITE_TIMEOUT_MS });
|
|
247
|
+
const after = integrationClean({ worktree, branch, preHead, env });
|
|
248
|
+
if (!reset.ok || !after.clean) return { ok: false, ran: true, reason: 'undo-failed', exitCode: reset.ok ? null : reset.status };
|
|
249
|
+
return { ok: true, ran: true, reason: null, exitCode: null };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── staleness (IC-023, read before the lock) ─────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
/** IC-023's stale reason for `dispatch` against an 080 ledger read just before, or null. */
|
|
255
|
+
function staleReason(dispatch , ledger , runKey ) {
|
|
256
|
+
const entry = ledger && ledger.tasks ? ledger.tasks[dispatch.task] : null;
|
|
257
|
+
const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
|
|
258
|
+
const attempt = attempts.find((a) => a.token === dispatch.token);
|
|
259
|
+
if (!attempt) return 'token-mismatch';
|
|
260
|
+
if (attempt.state === 'cancelled') return 'attempt-cancelled';
|
|
261
|
+
if (attempt.state !== 'active') return 'attempt-ended';
|
|
262
|
+
const run = ledger.runs && runKey && Object.hasOwn(ledger.runs, runKey) ? ledger.runs[runKey] : null;
|
|
263
|
+
if (run && Number.isInteger(dispatch.epoch) && run.epoch !== dispatch.epoch) return 'epoch';
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function readLedger(ctx ) {
|
|
268
|
+
return readTaskState(ctx.fsImpl || fs, ctx.ledgerFile) || { tasks: {}, runs: {} };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Whether `ledger` holds `(task, token)`'s 080 attempt `active`. */
|
|
272
|
+
function activeAttempt(ledger , task , token ) {
|
|
273
|
+
const entry = ledger && ledger.tasks ? ledger.tasks[task] : null;
|
|
274
|
+
return Boolean(entry && Array.isArray(entry.attempts) && entry.attempts.some((a ) => a.token === token && a.state === 'active'));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── IC-018 ───────────────────────────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
const INTEGRABLE_RUN_STATES = Object.freeze(['running', 'blocked']);
|
|
280
|
+
/** IC-030 step 3: the run states a cancel has marked. */
|
|
281
|
+
const CANCEL_MARKED = Object.freeze(['cancelling', 'cancelled']);
|
|
282
|
+
|
|
283
|
+
function oldestCollected(store ) {
|
|
284
|
+
const ready = (Object.values(store.dispatches) ).filter((d) => d.phase === 'collected' && d.outcome === 'offered');
|
|
285
|
+
ready.sort((a, b) => String(a.collectedAt ?? '').localeCompare(String(b.collectedAt ?? ''))
|
|
286
|
+
|| String(a.admittedAt ?? '').localeCompare(String(b.admittedAt ?? '')) || a.dispatchId.localeCompare(b.dispatchId));
|
|
287
|
+
return ready[0] ?? null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Records an integration gap and blocks the run, unless the run holds a state another writer owns
|
|
291
|
+
* (a cancel's, a lost driver's `resume-required`, IC-009): the gap is still recorded, the state is not moved. */
|
|
292
|
+
function blockRun(store , gap ) {
|
|
293
|
+
setRunState(store, 'blocked');
|
|
294
|
+
store.run.integrationGap = { cause: 'no-progress', blocks: [], ...gap };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Ends a stale dispatch in the decide callback: release while its attempt is active (the epoch
|
|
298
|
+
* case), else end it with no 080 call, the attempt having ended outside the loop. */
|
|
299
|
+
function endStale(store , dispatch , reason , ctx , now ) {
|
|
300
|
+
const usage = usageFor(dispatch, null, []);
|
|
301
|
+
if (reason === 'epoch') {
|
|
302
|
+
return releaseAttempt({ store, dispatchId: dispatch.dispatchId, reason: `stale:${reason}`, disposition: 'stale', usage, ctx });
|
|
303
|
+
}
|
|
304
|
+
Object.assign(dispatch, { phase: 'ended', disposition: 'stale', staleReason: reason, endedAt: now.toISOString(), usage });
|
|
305
|
+
return { write: true, result: { ended: true, dispatchId: dispatch.dispatchId, disposition: 'stale' } };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* IC-018, steps 1 to 6 (`integrateNext` without the cleanup), for the oldest offered `collected` dispatch of `slug`'s store under `root`.
|
|
310
|
+
* `ctx` is B.3's 080 context (`ledgerFile`, `roots`, `slug`, `env`, `cwd`, `fsImpl`, `now`); `env`
|
|
311
|
+
* is the environment git and the verification inherit, less the repository variables. `holder` is
|
|
312
|
+
* the lease holder `{ pid, start }` (this process by default). `beforeUpdateRef` is a test seam
|
|
313
|
+
* called between `commit-tree` and `update-ref`, and `afterLease` one called between steps 1 and 2;
|
|
314
|
+
* the loop passes neither.
|
|
315
|
+
*
|
|
316
|
+
* Resolves `{ outcome, dispatchId, task, token, ... }`, `outcome` one of `none`, `not-running`,
|
|
317
|
+
* `lease-held`, `stale`, `integration-dirty`, `conflict`, `integration-error`, `verification`,
|
|
318
|
+
* `finish-refused`, `completed`, or `lastError` when an 080 call threw or refused.
|
|
319
|
+
*/
|
|
320
|
+
async function integrateOnce({
|
|
321
|
+
root, slug, ctx, env = process.env, holder = null, now = () => new Date(), beforeUpdateRef = null, afterLease = null,
|
|
322
|
+
storeFile = null, verify = runVerification,
|
|
323
|
+
} ) {
|
|
324
|
+
const file = storeFile ?? storeFileFor(root, slug);
|
|
325
|
+
const lockCtx = () => ({ ...ctx, now: now() });
|
|
326
|
+
|
|
327
|
+
// Step 1, in the lock, on the 080 ledger read just before.
|
|
328
|
+
const ledger = readLedger(ctx);
|
|
329
|
+
const lessee = holder ?? { pid: process.pid, start: procStart(process.pid) };
|
|
330
|
+
const taken = mutateDispatch(file, (store ) => {
|
|
331
|
+
const run = store.run;
|
|
332
|
+
if (!run || !run.integration || !INTEGRABLE_RUN_STATES.includes(run.state)) return { write: false, result: { outcome: 'not-running', state: run ? run.state : null } };
|
|
333
|
+
if (store.lease) return { write: false, result: { outcome: 'lease-held', dispatchId: store.lease.dispatchId } };
|
|
334
|
+
const dispatch = oldestCollected(store);
|
|
335
|
+
if (!dispatch) return { write: false, result: { outcome: 'none' } };
|
|
336
|
+
const reason = staleReason(dispatch, ledger, dispatch.parentRunKey ?? run.runKey ?? null);
|
|
337
|
+
if (reason) {
|
|
338
|
+
const ended = endStale(store, dispatch, reason, lockCtx(), now());
|
|
339
|
+
return { write: ended.write, result: { outcome: ended.result.ended ? 'stale' : 'lastError', staleReason: reason, dispatchId: dispatch.dispatchId, end: ended.result } };
|
|
340
|
+
}
|
|
341
|
+
store.lease = { holder: { pid: lessee.pid, start: lessee.start ?? null }, dispatchId: dispatch.dispatchId, preHead: run.integration.head, takenAt: now().toISOString() };
|
|
342
|
+
dispatch.phase = 'integrating';
|
|
343
|
+
return {
|
|
344
|
+
write: true,
|
|
345
|
+
result: {
|
|
346
|
+
outcome: 'taken', dispatch: structuredClone(dispatch), integration: structuredClone(run.integration),
|
|
347
|
+
verify: run.verify ? structuredClone(run.verify) : null,
|
|
348
|
+
dispatchRunId: run.dispatchRunId ?? null,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
if (taken.outcome !== 'taken') return taken;
|
|
353
|
+
|
|
354
|
+
const { dispatch, integration, verify: declared } = taken;
|
|
355
|
+
const { dispatchId, task, token } = dispatch;
|
|
356
|
+
const preHead = integration.head;
|
|
357
|
+
const where = { worktree: integration.worktree, branch: integration.branch, env };
|
|
358
|
+
const base = { dispatchId, task, token };
|
|
359
|
+
|
|
360
|
+
/** One locked write that clears the lease after `apply(store, dispatch)`, only while the lease is
|
|
361
|
+
* still this dispatch's and the dispatch still `integrating` (RT-11). Otherwise a cancel or resume
|
|
362
|
+
* settled this integration meanwhile: only `evidence(store)` (the verification record) is written,
|
|
363
|
+
* so another lease is never cleared and an ended dispatch never edited. */
|
|
364
|
+
const settle = (apply , evidence = null) => mutateDispatch(file, (store ) => {
|
|
365
|
+
const d = store.dispatches[dispatchId];
|
|
366
|
+
if (!store.lease || store.lease.dispatchId !== dispatchId || !d || d.phase !== 'integrating') {
|
|
367
|
+
const lost = { ...base, outcome: 'lease-lost', lease: store.lease ? store.lease.dispatchId : null, phase: d ? d.phase : null };
|
|
368
|
+
if (!evidence) return { write: false, result: lost };
|
|
369
|
+
evidence(store);
|
|
370
|
+
return { write: true, result: lost };
|
|
371
|
+
}
|
|
372
|
+
const result = apply(store, d);
|
|
373
|
+
store.lease = null;
|
|
374
|
+
return { write: true, result };
|
|
375
|
+
});
|
|
376
|
+
/** IC-020 undo, then one locked write releasing the attempt, ending it `released` and blocking the
|
|
377
|
+
* run `integration-error`. */
|
|
378
|
+
const releaseWithError = (reason , exitCode , pickedHead ) => {
|
|
379
|
+
const undone = undo({ ...where, preHead, pickedHead });
|
|
380
|
+
return settle((store, d) => {
|
|
381
|
+
const end = releaseAttempt({ store, dispatchId, reason: 'integration-error', usage: usageFor(d, null, []), ctx: lockCtx() });
|
|
382
|
+
// A release 080 threw or refused on a still-active attempt keeps `lastError` and the dispatch
|
|
383
|
+
// live, back at `collected` with the lease cleared (IC-024, RT-9); one refused because the
|
|
384
|
+
// attempt already ended outside the loop still ends the dispatch.
|
|
385
|
+
if (!end.result.ended && d.phase === 'integrating' && activeAttempt(readLedger(ctx), task, token)) d.phase = 'collected';
|
|
386
|
+
else if (d.phase !== 'ended') Object.assign(d, { phase: 'ended', disposition: 'released', endedAt: now().toISOString(), releaseReason: 'integration-error' });
|
|
387
|
+
blockRun(store, { task, kind: 'integration-error', reason, exitCode, ...(undone.ok ? {} : { undo: undone.reason, undoFailed: true }) });
|
|
388
|
+
return { ...base, outcome: 'integration-error', reason, exitCode, undo: undone, end: end.result };
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
/** After a failed 080 end (thrown or refused, `lastError` recorded), the dispatch goes back to
|
|
392
|
+
* `collected` so the next pass retries it (IC-024), unless no pass will: its attempt is no longer
|
|
393
|
+
* active (stale, IC-023) or a cancel marked the run (IC-030 step 6). Then the dispatch ends here,
|
|
394
|
+
* `stale` or `released`, so a cancelled run holds no non-ended dispatch. Read in the lock. */
|
|
395
|
+
const retryLater = (store , d , end = null) => {
|
|
396
|
+
if ((end && end.result.ended) || d.phase === 'ended') return;
|
|
397
|
+
const stale = staleReason(d, readLedger(ctx), d.parentRunKey ?? store.run.runKey ?? null);
|
|
398
|
+
if (stale) {
|
|
399
|
+
endStale(store, d, stale, lockCtx(), now());
|
|
400
|
+
if (d.phase === 'ended') return;
|
|
401
|
+
}
|
|
402
|
+
if (CANCEL_MARKED.includes(store.run.state)) {
|
|
403
|
+
const released = releaseAttempt({ store, dispatchId, reason: `cancelled: ${store.run.state}`, usage: usageFor(d, null, []), ctx: lockCtx() });
|
|
404
|
+
if (!released.result.ended && d.phase !== 'ended') Object.assign(d, { phase: 'ended', disposition: 'released', endedAt: now().toISOString(), releaseReason: 'run-cancelled' });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
d.phase = 'collected';
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// RT-5: a throw from steps 2 to 6 (a git spawn, the verification, a store write) must not strand
|
|
411
|
+
// the lease under a driver that then ends cleanly. The catch undoes this integration and returns
|
|
412
|
+
// the dispatch to `collected` with the lease cleared, when HEAD allows it (IC-020), then rethrows;
|
|
413
|
+
// the pass records the throw and the run becomes `resume-required` (IC-009).
|
|
414
|
+
let pickedHead = null;
|
|
415
|
+
try {
|
|
416
|
+
// Step 2, outside: the integration worktree is on its branch at preHead and clean.
|
|
417
|
+
if (afterLease) await afterLease({ dispatchId, worktree: integration.worktree, preHead });
|
|
418
|
+
const check = integrationClean({ ...where, preHead });
|
|
419
|
+
if (!check.clean) {
|
|
420
|
+
return settle((store, d) => {
|
|
421
|
+
retryLater(store, d);
|
|
422
|
+
blockRun(store, { task, kind: 'integration-dirty', reason: check.reason, paths: check.paths });
|
|
423
|
+
return { ...base, outcome: 'integration-dirty', reason: check.reason, paths: check.paths };
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Steps 3 and 4, outside: the pick, unless the change is empty (DEC-049).
|
|
428
|
+
if (dispatch.commit) {
|
|
429
|
+
const pick = runGit(integration.worktree, ['cherry-pick', '--no-commit', dispatch.commit], { env: gitEnv(env, DISPATCH_IDENTITY), timeout: WRITE_TIMEOUT_MS });
|
|
430
|
+
if (!pick.ok) {
|
|
431
|
+
const conflicted = nulList(readGit(integration.worktree, ['diff', '--name-only', '--diff-filter=U', '-z'], env).stdout);
|
|
432
|
+
if (conflicted.length === 0) return releaseWithError('cherry-pick-failed', pick.status, null);
|
|
433
|
+
const undone = undo({ ...where, preHead });
|
|
434
|
+
return settle((store, d) => {
|
|
435
|
+
const end = endAttempt({ store, dispatchId, failureClass: 'conflict', detail: `${conflicted.length} conflicted path(s)`, usage: usageFor(d, null, []), ctx: lockCtx() });
|
|
436
|
+
retryLater(store, d, end);
|
|
437
|
+
if (!undone.ok) blockRun(store, { task, kind: 'integration-error', reason: undone.reason, exitCode: undone.exitCode, undoFailed: true });
|
|
438
|
+
return { ...base, outcome: 'conflict', conflicted: conflicted.length, undo: undone, end: end.result };
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
const identityEnv = gitEnv(env, DISPATCH_IDENTITY);
|
|
442
|
+
const tree = runGit(integration.worktree, ['write-tree'], { env: identityEnv, timeout: WRITE_TIMEOUT_MS });
|
|
443
|
+
const preTree = readGit(integration.worktree, ['rev-parse', `${preHead}^{tree}`], env);
|
|
444
|
+
if (!tree.ok || !preTree.ok) return releaseWithError('write-tree-failed', tree.ok ? preTree.status : tree.status, null);
|
|
445
|
+
if (tree.stdout.trim() !== preTree.stdout.trim()) {
|
|
446
|
+
const made = runGit(integration.worktree, ['commit-tree', tree.stdout.trim(), '-p', preHead, '-m', `${task}: dispatch ${dispatchId} token ${token}`], { env: identityEnv, timeout: WRITE_TIMEOUT_MS });
|
|
447
|
+
const commit = made.stdout.trim();
|
|
448
|
+
if (!made.ok || !SHA.test(commit)) return releaseWithError('commit-tree-failed', made.status, null);
|
|
449
|
+
if (beforeUpdateRef) await beforeUpdateRef({ worktree: integration.worktree, branch: integration.branch, preHead, commit });
|
|
450
|
+
const moved = runGit(integration.worktree, ['update-ref', `refs/heads/${integration.branch}`, commit, preHead], { env: identityEnv, timeout: WRITE_TIMEOUT_MS });
|
|
451
|
+
if (!moved.ok) return releaseWithError('update-ref-failed', moved.status, null);
|
|
452
|
+
pickedHead = commit;
|
|
453
|
+
mutateDispatch(file, (store ) => {
|
|
454
|
+
if (!store.lease || store.lease.dispatchId !== dispatchId) return { write: false, result: null };
|
|
455
|
+
store.lease.pickedHead = commit;
|
|
456
|
+
return { write: true, result: null };
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Step 5, outside: verification on the integration HEAD.
|
|
462
|
+
if (!declared || !Array.isArray(declared.argv)) return releaseWithError('verification-undeclared', null, pickedHead);
|
|
463
|
+
const verification = await verify({
|
|
464
|
+
root, slug, taskId: task, worktree: integration.worktree, argv: declared.argv, argvSha256: declared.argvSha256 ?? null,
|
|
465
|
+
seconds: declared.timeoutSeconds, env,
|
|
466
|
+
dispatchRunId: taken.dispatchRunId ?? null,
|
|
467
|
+
onSpawn: (proc ) => mutateDispatch(file, (store ) => {
|
|
468
|
+
store.run.verifying = proc;
|
|
469
|
+
return { write: true, result: null };
|
|
470
|
+
}),
|
|
471
|
+
});
|
|
472
|
+
const passed = verification.exitCode === 0 && verification.timedOut === false;
|
|
473
|
+
const record = (store ) => {
|
|
474
|
+
store.run.verifying = null;
|
|
475
|
+
store.run.verifications = [...(Array.isArray(store.run.verifications) ? store.run.verifications : []), verification];
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// Step 6, in the lock.
|
|
479
|
+
if (!passed) {
|
|
480
|
+
const undone = undo({ ...where, preHead, pickedHead });
|
|
481
|
+
return settle((store, d) => {
|
|
482
|
+
record(store);
|
|
483
|
+
const end = endAttempt({ store, dispatchId, failureClass: 'verification', detail: verification.timedOut ? 'timed out' : `exit ${verification.exitCode ?? verification.error}`, usage: usageFor(d, null, []), evidenceId: verification.evidenceId, ctx: lockCtx() });
|
|
484
|
+
retryLater(store, d, end);
|
|
485
|
+
if (!undone.ok) blockRun(store, { task, kind: 'integration-error', reason: undone.reason, exitCode: undone.exitCode, undoFailed: true });
|
|
486
|
+
return { ...base, outcome: 'verification', verification, undo: undone, end: end.result };
|
|
487
|
+
}, record);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const after = readLedger(ctx);
|
|
491
|
+
const finished = mutateDispatch(file, (store ) => {
|
|
492
|
+
const d = store.dispatches[dispatchId];
|
|
493
|
+
record(store);
|
|
494
|
+
const stale = staleReason(d, after, d.parentRunKey ?? store.run.runKey ?? null);
|
|
495
|
+
const end = stale ? null : completeAttempt({ store, dispatchId, evidenceId: verification.evidenceId, usage: usageFor(d, null, []), integratedCommit: pickedHead, ctx: lockCtx() });
|
|
496
|
+
if (end && end.result.ended) {
|
|
497
|
+
store.run.integration.head = pickedHead ?? preHead;
|
|
498
|
+
store.run.integrationGap = null;
|
|
499
|
+
store.lease = null;
|
|
500
|
+
return { write: true, result: { ...base, outcome: 'completed', integratedCommit: pickedHead, head: pickedHead ?? preHead, verification, end: end.result } };
|
|
501
|
+
}
|
|
502
|
+
if (store.lease) store.lease.undoPending = true;
|
|
503
|
+
const why = stale ? `stale:${stale}` : (end.result.reason || end.result.outcome || 'refused');
|
|
504
|
+
return { write: true, result: { outcome: 'refused', why } };
|
|
505
|
+
});
|
|
506
|
+
if (finished.outcome === 'completed') return finished;
|
|
507
|
+
|
|
508
|
+
const undone = undo({ ...where, preHead, pickedHead });
|
|
509
|
+
return settle((store, d) => {
|
|
510
|
+
const reason = `finish-refused:${finished.why}`;
|
|
511
|
+
let end = null;
|
|
512
|
+
if (after.tasks?.[task]?.attempts?.find((a ) => a.token === token)?.state === 'active') {
|
|
513
|
+
end = releaseAttempt({ store, dispatchId, reason, usage: usageFor(d, null, []), ctx: lockCtx() }).result;
|
|
514
|
+
}
|
|
515
|
+
if (d.phase !== 'ended') Object.assign(d, { phase: 'ended', disposition: 'released', endedAt: now().toISOString(), releaseReason: reason });
|
|
516
|
+
if (!undone.ok) blockRun(store, { task, kind: 'integration-error', reason: undone.reason, exitCode: undone.exitCode, undoFailed: true });
|
|
517
|
+
return { ...base, outcome: 'finish-refused', reason, verification, undo: undone, end };
|
|
518
|
+
});
|
|
519
|
+
} catch (error) {
|
|
520
|
+
try {
|
|
521
|
+
const held = readDispatch(file).lease;
|
|
522
|
+
// RR-2: an attempt 080 already finished COMPLETED (a store write that threw after the finish
|
|
523
|
+
// applied) keeps its commit; the lease stays for IC-032's lease rule, which adopts it.
|
|
524
|
+
const attempt = readLedger(ctx).tasks?.[task]?.attempts?.find((a ) => a.token === token);
|
|
525
|
+
const completed = Boolean(attempt && attempt.state === 'finished' && attempt.result === 'COMPLETED');
|
|
526
|
+
if (held && held.dispatchId === dispatchId && !completed) {
|
|
527
|
+
const undone = undo({ ...where, preHead, pickedHead: held.pickedHead ?? pickedHead });
|
|
528
|
+
if (undone.ok) {
|
|
529
|
+
mutateDispatch(file, (store ) => {
|
|
530
|
+
if (!store.lease || store.lease.dispatchId !== dispatchId) return { write: false, result: null };
|
|
531
|
+
store.lease = null;
|
|
532
|
+
const d = store.dispatches[dispatchId];
|
|
533
|
+
if (d && d.phase === 'integrating') d.phase = 'collected';
|
|
534
|
+
return { write: true, result: null };
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} catch {
|
|
539
|
+
// The lease stays for IC-032's lease rule; the original throw is the one reported.
|
|
540
|
+
}
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* IC-018 for one dispatch (`integrateOnce`), then IC-021's cleanup rule for the dispatch it ended:
|
|
547
|
+
* a completed one's attempt worktree is removed, a failed one kept (`removal`).
|
|
548
|
+
*/
|
|
549
|
+
async function integrateNext(options ) {
|
|
550
|
+
const result = await integrateOnce(options);
|
|
551
|
+
if (!result || typeof result.dispatchId !== 'string' || !result.outcome || ['lease-held', 'integration-dirty'].includes(result.outcome)) return result;
|
|
552
|
+
const file = options.storeFile ?? storeFileFor(options.root, options.slug);
|
|
553
|
+
const dispatch = readDispatch(file).dispatches[result.dispatchId];
|
|
554
|
+
if (!dispatch || dispatch.phase !== 'ended') return result;
|
|
555
|
+
const removal = await removeDispatchWorktree({ root: options.root, slug: options.slug, dispatchId: result.dispatchId, env: options.env, storeFile: file });
|
|
556
|
+
return { ...result, removal };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ── cleanup (IC-021) ─────────────────────────────────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
/** IC-021's table: whether `dispatch`'s attempt worktree is removed now, and why it is kept if not. */
|
|
562
|
+
function removalDue(dispatch , run ) {
|
|
563
|
+
if (dispatch.phase !== 'ended') return { due: false, reason: 'not-ended' };
|
|
564
|
+
if (dispatch.removal && dispatch.removal.verified === true) return { due: false, reason: 'removed' };
|
|
565
|
+
// IC-021's last row covers every disposition: a worker run's worktree waits for verified cleanup (RT-8).
|
|
566
|
+
if (dispatch.workerRunId) {
|
|
567
|
+
const cleanup = dispatch.cleanup ? dispatch.cleanup.cleanup ?? dispatch.cleanup : null;
|
|
568
|
+
if (!cleanup || cleanup.status !== 'verified') return { due: false, reason: 'worker-cleanup-unverified' };
|
|
569
|
+
}
|
|
570
|
+
if (dispatch.disposition === 'completed') return { due: true, reason: null };
|
|
571
|
+
if (RELEASED_ENDS.includes(dispatch.disposition)) return { due: true, reason: null };
|
|
572
|
+
if (dispatch.disposition === 'failed') {
|
|
573
|
+
return run && RUN_ENDS.includes(run.state) ? { due: true, reason: null } : { due: false, reason: 'failed-kept' };
|
|
574
|
+
}
|
|
575
|
+
return { due: false, reason: 'unknown-disposition' };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* IC-021: removes an ended dispatch's attempt worktree when the table says so, then stores the
|
|
580
|
+
* `Removal` add-only on the dispatch (`verified` may only move from false to true). The removal runs
|
|
581
|
+
* outside every lock. Resolves `{ removed, kept, reason, removal }`.
|
|
582
|
+
*/
|
|
583
|
+
async function removeDispatchWorktree({ root, slug, dispatchId, env = process.env, storeFile = null } ) {
|
|
584
|
+
const file = storeFile ?? storeFileFor(root, slug);
|
|
585
|
+
const current = readDispatch(file);
|
|
586
|
+
const dispatch = current.dispatches[dispatchId];
|
|
587
|
+
if (!dispatch) return { removed: false, kept: false, reason: 'unknown-dispatch', removal: null };
|
|
588
|
+
const due = removalDue(dispatch, current.run);
|
|
589
|
+
if (!due.due) return { removed: false, kept: due.reason !== 'removed', reason: due.reason, removal: dispatch.removal ?? null };
|
|
590
|
+
const removal = managerFor(root, slug, 'attempt', env).removeVerified(dispatchId);
|
|
591
|
+
const stored = transitionDispatch(file, dispatchId, ['ended'], (d ) => {
|
|
592
|
+
if (!d.removal || (d.removal.verified === false && removal.verified)) d.removal = removal;
|
|
593
|
+
});
|
|
594
|
+
return { removed: removal.verified, kept: !removal.verified, reason: removal.verified ? null : 'removal-unverified', removal, stored: stored.applied };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
module.exports = {
|
|
598
|
+
WORKTREES_DIR,
|
|
599
|
+
WRITE_TIMEOUT_MS,
|
|
600
|
+
READ_TIMEOUT_MS,
|
|
601
|
+
DISPATCH_IDENTITY,
|
|
602
|
+
IntegrationError,
|
|
603
|
+
gitEnv,
|
|
604
|
+
managerFor,
|
|
605
|
+
createIntegrationWorktree,
|
|
606
|
+
createAttemptWorktree,
|
|
607
|
+
createChildWorktree,
|
|
608
|
+
diffNameStatus,
|
|
609
|
+
attemptCommit,
|
|
610
|
+
integrationClean,
|
|
611
|
+
integrateNext,
|
|
612
|
+
undo,
|
|
613
|
+
staleReason,
|
|
614
|
+
activeAttempt,
|
|
615
|
+
removalDue,
|
|
616
|
+
removeDispatchWorktree,
|
|
617
|
+
};
|