@khoavu882/doflow 2.0.0-beta.4 → 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 -3
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- 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-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- 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/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- 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 +21 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
- package/core/harnesses/kiro/hooks/session-start.sh +12 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
- 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 +65 -40
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
- package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
- package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +72 -80
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +60 -33
- 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 +1542 -277
- package/core/registry/lifecycle.json +2837 -37
- 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 +701 -30
- 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 +154 -35
- 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 +24 -23
- 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 +17 -59
- 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 +28 -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 +7 -9
- package/core/shared/skills/do-plan/SKILL.md +29 -19
- 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 +226 -113
- package/src/adapters/claude/index.js +39 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +40 -5
- package/src/adapters/codex/index.js +100 -22
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +294 -48
- 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 +38 -13
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +120 -0
- package/src/adapters/pi/index.js +48 -16
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +3 -2
- 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 +3 -2
- package/src/cli/index.js +338 -33
- package/src/cli/runtime-commands.js +243 -52
- 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 +74 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +434 -86
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +686 -32
- 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 +514 -0
- 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 +27 -12
- package/src/runtime/cli-result.js +11 -4
- package/src/runtime/cli.js +221 -277
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +100 -0
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -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 +1106 -245
- 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-fields.js +39 -0
- package/src/runtime/evidence-ledger.js +41 -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 +77 -10
- 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 +3 -3
- 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 +127 -16
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +402 -73
- package/src/runtime/retrieval-record.js +293 -0
- 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 +63 -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 +290 -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 +202 -56
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +440 -65
- 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/.antigravity-plugin/hooks.json +0 -39
- 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 -21
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
- 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 -5
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- 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 -210
- package/src/adapters/gemini/index.js +0 -390
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Feature 082 C.4 (IC-029, IC-030, IC-031, IC-032; DEC-009, DEC-013, DEC-019, DEC-030, DEC-035,
|
|
4
|
+
// DEC-040, DEC-044, DEC-045, DEC-047, DEC-050): the user controls on a dispatch run.
|
|
5
|
+
//
|
|
6
|
+
// `pause`, `inspect`, `cancel`, `takeOver` and `resume` compose 080 and 081 (DEC-030): the 080 run's
|
|
7
|
+
// control is changed only by the in-process 080 `pause`, `cancel`, `release` and `resume`, each
|
|
8
|
+
// inside the dispatch lock (lock order dispatch then ledger, IC-006), and a worker run is ended only
|
|
9
|
+
// through 081's `cancelRun` with the canceller's own `env` and `cwd`, so 081 re-checks the caller's
|
|
10
|
+
// run key before its own signal (DEC-047). Nothing here signals a process: the one group dispatch
|
|
11
|
+
// code ends itself, a recorded verification, is ended through `verify-run.ts`'s `endVerification`.
|
|
12
|
+
// Drivers are read, never signalled (IC-009).
|
|
13
|
+
//
|
|
14
|
+
// Every control but `inspect` registers a driver (IC-009) after IC-001's identity checks, and writes
|
|
15
|
+
// its end on exit. `inspect` takes no lock and writes nothing.
|
|
16
|
+
//
|
|
17
|
+
// Resume (IC-032) settles before it resumes (DEC-013, DEC-045): each dispatch whose owner is gone
|
|
18
|
+
// is settled by its phase, a lost lease by the lease rule, every 081 run left behind by an ended
|
|
19
|
+
// dispatch is cancelled, unnamed worktrees are listed and never deleted, and only then does the
|
|
20
|
+
// in-process 080 resume run, so its report and digest already reflect the settlement.
|
|
21
|
+
//
|
|
22
|
+
// Every write goes through `store.ts` (`updateTaskState`); this module makes no fs write call.
|
|
23
|
+
// Git runs only through `WorktreeManager`'s bounded helper and `integrate.ts`.
|
|
24
|
+
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
const attempts = require('../task-attempts.js');
|
|
28
|
+
const { readTaskState } = require('../task-state.js');
|
|
29
|
+
const { maskLine } = require('../mask.js');
|
|
30
|
+
const { projectRoot } = require('../lifecycle/root.js');
|
|
31
|
+
const { BASE_FILE } = require('../worktree.js');
|
|
32
|
+
const { alive } = require('../worker/process-tree.js');
|
|
33
|
+
const { cancelRun, CANCEL_TAKEOVER_MS } = require('../worker/cancel.js');
|
|
34
|
+
const { statusView, settledStatus, findRun } = require('../worker/command.js');
|
|
35
|
+
const { readRuns, storeFileFor: runStoreFileFor } = require('../worker/run-store.js');
|
|
36
|
+
const { storeFileFor, readDispatch, mutateDispatch, transitionDispatch, setRunState, DispatchStoreError } = require('./store.js');
|
|
37
|
+
const { registerDriver, endDriver, MAIN_SESSION_MODES } = require('./admission.js');
|
|
38
|
+
const { endAttempt, releaseAttempt, usageFor } = require('./attempt-end.js');
|
|
39
|
+
const integrate = require('./integrate.js');
|
|
40
|
+
const { endVerification } = require('./verify-run.js');
|
|
41
|
+
const { checkCaller, measurement, exitedCleanup, LOOP_BY, ATTEMPT_SECONDS_DEFAULT } = require('./loop.js');
|
|
42
|
+
|
|
43
|
+
/** 080's `SAFE_NAME`, which `--by` must match. */
|
|
44
|
+
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._ -]*$/;
|
|
45
|
+
const ACK = /^sha256:[0-9a-f]{64}$/;
|
|
46
|
+
const RUN_ENDED = Object.freeze(['completed', 'cancelled']);
|
|
47
|
+
const LIVE_WORKER_STATES = Object.freeze(['starting', 'running']);
|
|
48
|
+
const LOST_WORKER_REASON = 'resume: worker run lost';
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
function usage(message ) {
|
|
53
|
+
return new DispatchStoreError('usage', message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function refused(reason , extra = {}) {
|
|
57
|
+
return { disposition: 'refused', reason, ...extra };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const iso = (at ) => at.toISOString();
|
|
61
|
+
|
|
62
|
+
function requireBy(by ) {
|
|
63
|
+
if (typeof by !== 'string' || !SAFE_NAME.test(by)) throw usage('--by is required: the name of the agent or person using the control');
|
|
64
|
+
return by;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The `--reason` text, masked once here (RT-17), so every store and every 080 or 081 call that
|
|
68
|
+
* receives it (cancel, take over, pause, resume, `cancelRun`) holds the masked text only. */
|
|
69
|
+
function requireReason(reason , what ) {
|
|
70
|
+
const text = typeof reason === 'string' ? reason.trim() : '';
|
|
71
|
+
if (!text) throw usage(`--reason is required: ${what}`);
|
|
72
|
+
return maskLine(text).text;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One invocation's paths, identity inputs and clock. */
|
|
76
|
+
function contextFor(options ) {
|
|
77
|
+
const env = options.env || process.env;
|
|
78
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
79
|
+
const slug = options.slug;
|
|
80
|
+
if (typeof slug !== 'string') throw usage('a dispatch control needs --slug');
|
|
81
|
+
const root = options.root ? path.resolve(options.root) : projectRoot(cwd);
|
|
82
|
+
const clock = typeof options.now === 'function'
|
|
83
|
+
? () => { const at = options.now(); return at instanceof Date ? at : new Date(at); }
|
|
84
|
+
: () => new Date();
|
|
85
|
+
const ledgerFile = attempts.storeFile(root, slug);
|
|
86
|
+
const roots = [...new Set([cwd, root])];
|
|
87
|
+
const c = {
|
|
88
|
+
env, cwd, slug, root, clock, ledgerFile, roots,
|
|
89
|
+
file: storeFileFor(root, slug),
|
|
90
|
+
runStore: runStoreFileFor(root, slug),
|
|
91
|
+
platform: options.platform || process.platform,
|
|
92
|
+
};
|
|
93
|
+
c.ctx = () => ({ ledgerFile, roots, slug, env, cwd, fsImpl: fs, now: clock() });
|
|
94
|
+
c.common = () => ({ file: ledgerFile, fsImpl: fs, roots, slug, env, cwd, now: clock() });
|
|
95
|
+
c.ledger = () => readTaskState(fs, ledgerFile) || { tasks: {}, runs: {} };
|
|
96
|
+
c.git = (args , cwdOf ) => integrate.managerFor(root, slug, 'integration', env).git(args, cwdOf, { allowFailure: true });
|
|
97
|
+
return c;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Runs `body(driver)` under one registered driver whose end is always written (IC-009). */
|
|
101
|
+
async function withDriver(c , body ) {
|
|
102
|
+
const reg = registerDriver({ file: c.file, now: c.clock() });
|
|
103
|
+
try {
|
|
104
|
+
return await body(reg.driver);
|
|
105
|
+
} finally {
|
|
106
|
+
endDriver({ file: c.file, driver: reg.driver, now: c.clock() });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sameDriver = (a , b ) => !!a && !!b && a.pid === b.pid && (a.start ?? null) === (b.start ?? null)
|
|
111
|
+
&& (a.registeredAt ?? null) === (b.registeredAt ?? null);
|
|
112
|
+
|
|
113
|
+
/** The driver `who` (`{ pid, start }`) still has a registered, unended, not-lost record other than
|
|
114
|
+
* this invocation's own `mine`. */
|
|
115
|
+
function ownerLive(store , who , mine ) {
|
|
116
|
+
if (!who || !Number.isInteger(who.pid)) return false;
|
|
117
|
+
return (store.drivers ).some((d) => d && !d.endedAt && !d.lost && d.pid === who.pid
|
|
118
|
+
&& (d.start ?? null) === (who.start ?? null) && !sameDriver(d, mine));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The parts of a `cancelRun` result a dispatch or spawn request keeps. */
|
|
122
|
+
function cancelSummary(result ) {
|
|
123
|
+
if (!result) return null;
|
|
124
|
+
if (result.disposition !== 'cancelled') return { disposition: result.disposition, reason: result.reason ?? null, endState: result.endState ?? null, cleanup: result.cleanup ?? null };
|
|
125
|
+
return { disposition: 'cancelled', endState: result.endState, cleanup: result.cleanup, attemptCancel: result.attemptCancel, attemptCancelled: result.attemptCancelled };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 081 `cancelRun` with the canceller's own env and cwd; a throw is returned as `error`. */
|
|
129
|
+
async function endWorker(c , runId , by , reason ) {
|
|
130
|
+
try {
|
|
131
|
+
return await cancelRun({ file: c.runStore, runId, by, reason, source: 'user', env: c.env, cwd: c.cwd, platform: c.platform, now: c.clock });
|
|
132
|
+
} catch (error ) {
|
|
133
|
+
return { disposition: 'error', reason: error && error.code ? error.code : 'cancel-threw' };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Ends every listed 081 run that has not ended, then records each result add-only: a dispatch's own
|
|
139
|
+
* run as its `cleanup` while absent, a child run on its spawn request. `targets` maps a run id to
|
|
140
|
+
* `{ dispatchId, requestId }`. Returns IC-030's worker rows.
|
|
141
|
+
*/
|
|
142
|
+
async function endWorkers(c , targets , by , reason ) {
|
|
143
|
+
const runs = readRuns(c.runStore);
|
|
144
|
+
const rows = [];
|
|
145
|
+
const results = new Map ();
|
|
146
|
+
for (const [runId, target] of targets) {
|
|
147
|
+
const run = runs[runId];
|
|
148
|
+
if (!run || run.phase === 'ended') continue;
|
|
149
|
+
const res = await endWorker(c, runId, by, reason);
|
|
150
|
+
results.set(runId, { target, res });
|
|
151
|
+
rows.push({
|
|
152
|
+
dispatchId: target.dispatchId, workerRunId: runId, endState: res.endState ?? null, cleanup: res.cleanup ?? null,
|
|
153
|
+
attemptCancelled: res.disposition === 'cancelled' ? Boolean(res.attemptCancelled) : null,
|
|
154
|
+
refused: res.disposition === 'cancelled' ? null : res.reason ?? res.disposition,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (results.size) {
|
|
158
|
+
mutateDispatch(c.file, (s ) => {
|
|
159
|
+
for (const [runId, { target, res }] of results) {
|
|
160
|
+
const summary = cancelSummary(res);
|
|
161
|
+
const r = target.requestId ? s.spawnRequests[target.requestId] : null;
|
|
162
|
+
if (r) {
|
|
163
|
+
r.cancel = summary;
|
|
164
|
+
if (res.disposition === 'cancelled') {
|
|
165
|
+
if (r.endState == null) Object.assign(r, { endState: res.endState, endedAt: iso(c.clock()) });
|
|
166
|
+
r.cleanup = res.cleanup ?? null;
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const d = s.dispatches[target.dispatchId];
|
|
171
|
+
if (d && d.workerRunId === runId && d.cleanup == null) d.cleanup = summary;
|
|
172
|
+
}
|
|
173
|
+
return { write: true, result: null };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return rows;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The launched child runs of `dispatchId` with no recorded end, as cancel targets. */
|
|
180
|
+
function childTargets(s , dispatchId , into ) {
|
|
181
|
+
for (const r of Object.values(s.spawnRequests) ) {
|
|
182
|
+
if (!r.childRunId || (dispatchId && r.granterDispatchId !== dispatchId)) continue;
|
|
183
|
+
into.set(r.childRunId, { dispatchId: r.granterDispatchId, requestId: r.requestId });
|
|
184
|
+
}
|
|
185
|
+
return into;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Every 081 run of this slug with no end whose `(task, token)` is a dispatch's of this run and
|
|
189
|
+
* that `keep(dispatch)` selects, as cancel targets. */
|
|
190
|
+
function strayTargets(s , runs , keep , into ) {
|
|
191
|
+
const byKey = new Map ();
|
|
192
|
+
for (const d of Object.values(s.dispatches) ) if (Number.isInteger(d.token)) byKey.set(`${d.task}\u0000${d.token}`, d);
|
|
193
|
+
for (const run of Object.values(runs) ) {
|
|
194
|
+
if (run.phase === 'ended' || into.has(run.runId)) continue;
|
|
195
|
+
if (s.run && run.caller && run.caller.runKey !== s.run.runKey) continue;
|
|
196
|
+
const d = byKey.get(`${run.task}\u0000${run.token}`);
|
|
197
|
+
if (!d || !keep(d)) continue;
|
|
198
|
+
into.set(run.runId, { dispatchId: d.dispatchId, requestId: run.childOf ? run.request ?? null : null });
|
|
199
|
+
}
|
|
200
|
+
return into;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A dispatch's worker cleanup: its recorded one, else what 081 recorded for its ended run, else
|
|
204
|
+
* what an `exited` run's group reading shows (`exitedCleanup`: verified only when observed). */
|
|
205
|
+
function workerCleanupOf(c , d , runs ) {
|
|
206
|
+
if (d.cleanup) return d.cleanup.cleanup ?? d.cleanup;
|
|
207
|
+
const run = d.workerRunId ? runs[d.workerRunId] : null;
|
|
208
|
+
if (!run || run.phase !== 'ended') return null;
|
|
209
|
+
if (run.cleanup) return run.cleanup;
|
|
210
|
+
if (run.end && run.end.state === 'exited') return exitedCleanup(run, c.platform);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Stores a `Removal` add-only on an ended dispatch (`verified` may only move from false to true). */
|
|
215
|
+
function storeRemoval(c , dispatchId , removal ) {
|
|
216
|
+
transitionDispatch(c.file, dispatchId, ['ended'], (d ) => {
|
|
217
|
+
if (!d.removal || (d.removal.verified === false && removal.verified)) d.removal = removal;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── the lease rule (IC-032 step 4, R4) ───────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/** The integration worktree's uncommitted paths (renames give both names), the base pointer
|
|
224
|
+
* excluded; null when git could not list them. */
|
|
225
|
+
function uncommitted(c , worktree ) {
|
|
226
|
+
const res = c.git(['--no-optional-locks', 'status', '--porcelain=v1', '-z', '--untracked-files=all'], worktree);
|
|
227
|
+
if (!res.ok) return null;
|
|
228
|
+
const fields = res.stdout.split('\0').filter((f ) => f !== '');
|
|
229
|
+
const out = [];
|
|
230
|
+
for (let i = 0; i < fields.length; i += 1) {
|
|
231
|
+
const entry = fields[i];
|
|
232
|
+
if (entry.length < 4) continue;
|
|
233
|
+
out.push(entry.slice(3));
|
|
234
|
+
if (entry[0] === 'R' || entry[0] === 'C') { out.push(fields[i + 1]); i += 1; }
|
|
235
|
+
}
|
|
236
|
+
return out.filter((p) => p !== BASE_FILE);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* IC-032's lease rule for a lease whose holder is lost; null when there is no lease or its holder
|
|
241
|
+
* reads alive. First a live recorded verification group is ended through `endVerification`. Then
|
|
242
|
+
* `adopted` (080 COMPLETED, `pickedHead` set and the integration HEAD at it), `undone` (HEAD at
|
|
243
|
+
* `preHead` or `pickedHead` and every uncommitted path a change path: IC-020's undo, the active
|
|
244
|
+
* attempt released `interrupted`), else `blocked` with gap `integration-dirty`, touching nothing.
|
|
245
|
+
*/
|
|
246
|
+
async function leaseRule(c ) {
|
|
247
|
+
let s = readDispatch(c.file);
|
|
248
|
+
const lease = s.lease;
|
|
249
|
+
if (!lease) return null;
|
|
250
|
+
const holder = lease.holder || {};
|
|
251
|
+
if (alive({ pid: holder.pid, pgid: holder.pid, start: holder.start ?? null }, { platform: c.platform }) === true) return null;
|
|
252
|
+
const dispatchId = lease.dispatchId;
|
|
253
|
+
|
|
254
|
+
const verifying = s.run.verifying;
|
|
255
|
+
let verification = null;
|
|
256
|
+
if (verifying) {
|
|
257
|
+
const live = alive({ pid: verifying.pid, pgid: verifying.pgid ?? verifying.pid, start: verifying.start ?? null }, { platform: c.platform });
|
|
258
|
+
if (live === true) verification = await endVerification(verifying, { platform: c.platform });
|
|
259
|
+
mutateDispatch(c.file, (st ) => {
|
|
260
|
+
if (!st.run || !st.run.verifying || st.run.verifying.pid !== verifying.pid) return { write: false, result: null };
|
|
261
|
+
st.run.verifying = null;
|
|
262
|
+
return { write: true, result: null };
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
s = readDispatch(c.file);
|
|
267
|
+
const d = s.dispatches[dispatchId];
|
|
268
|
+
const integration = s.run.integration;
|
|
269
|
+
const ledger = c.ledger();
|
|
270
|
+
const entry = d && ledger.tasks ? ledger.tasks[d.task] : null;
|
|
271
|
+
const attempt = entry && Array.isArray(entry.attempts) ? entry.attempts.find((a ) => a.token === d.token) : null;
|
|
272
|
+
const headRead = c.git(['rev-parse', '--verify', '-q', 'HEAD'], integration.worktree);
|
|
273
|
+
const head = headRead.ok ? headRead.stdout.trim() : null;
|
|
274
|
+
const { preHead } = lease;
|
|
275
|
+
const pickedHead = lease.pickedHead ?? null;
|
|
276
|
+
const base = { dispatchId, verification };
|
|
277
|
+
const block = (reason ) => {
|
|
278
|
+
mutateDispatch(c.file, (st ) => {
|
|
279
|
+
setRunState(st, 'blocked');
|
|
280
|
+
st.run.integrationGap = { task: d ? d.task : null, kind: 'integration-dirty', cause: 'no-progress', reason, blocks: [] };
|
|
281
|
+
return { write: true, result: null };
|
|
282
|
+
});
|
|
283
|
+
return { ...base, action: 'blocked', reason };
|
|
284
|
+
};
|
|
285
|
+
if (!d) return block('lease-dispatch-missing');
|
|
286
|
+
|
|
287
|
+
if (attempt && attempt.state === 'finished' && attempt.result === 'COMPLETED'
|
|
288
|
+
&& ((pickedHead && head === pickedHead) || (pickedHead === null && head === preHead))) {
|
|
289
|
+
// An empty change (or empty pick) finished COMPLETED leaves HEAD at preHead (RT-9).
|
|
290
|
+
mutateDispatch(c.file, (st ) => {
|
|
291
|
+
const x = st.dispatches[dispatchId];
|
|
292
|
+
st.run.integration.head = pickedHead ?? preHead;
|
|
293
|
+
st.run.integrationGap = null;
|
|
294
|
+
st.lease = null;
|
|
295
|
+
if (x && x.phase !== 'ended') {
|
|
296
|
+
Object.assign(x, {
|
|
297
|
+
phase: 'ended', disposition: 'completed', failureClass: null, evidenceId: attempt.evidenceId ?? null,
|
|
298
|
+
integratedCommit: pickedHead, endedAt: iso(c.clock()), usage: usageFor(x, null, []),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return { write: true, result: null };
|
|
302
|
+
});
|
|
303
|
+
await integrate.removeDispatchWorktree({ root: c.root, slug: c.slug, dispatchId, env: c.env, storeFile: c.file });
|
|
304
|
+
return { ...base, action: 'adopted', reason: 'finished-completed' };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const paths = uncommitted(c, integration.worktree);
|
|
308
|
+
const changePaths = new Set ((Array.isArray(d.changes) ? d.changes : []).flatMap((x ) => [x.path, x.from].filter(Boolean)));
|
|
309
|
+
const atOwnHead = head !== null && (head === preHead || (pickedHead !== null && head === pickedHead));
|
|
310
|
+
if (!atOwnHead) return block('head-moved');
|
|
311
|
+
if (paths === null) return block('status-failed');
|
|
312
|
+
if (!paths.every((p) => changePaths.has(p))) return block('dirty');
|
|
313
|
+
|
|
314
|
+
const undone = integrate.undo({ worktree: integration.worktree, branch: integration.branch, preHead, pickedHead, env: c.env });
|
|
315
|
+
if (!undone.ok) return block(undone.reason);
|
|
316
|
+
mutateDispatch(c.file, (st ) => {
|
|
317
|
+
const x = st.dispatches[dispatchId];
|
|
318
|
+
st.lease = null;
|
|
319
|
+
if (!x || x.phase === 'ended') return { write: true, result: null };
|
|
320
|
+
const active = attempt && attempt.state === 'active';
|
|
321
|
+
const end = active ? endAttempt({ store: st, dispatchId, failureClass: 'interrupted', detail: 'lease holder lost', ctx: c.ctx() }) : null;
|
|
322
|
+
// A release 080 threw or refused on a still-active attempt keeps `lastError` and the dispatch
|
|
323
|
+
// live, back at `collected` with the lease cleared, so the next pass retries (IC-024, RT-9).
|
|
324
|
+
if (end && !end.result.ended && integrate.activeAttempt(c.ledger(), x.task, x.token)) {
|
|
325
|
+
if (x.phase === 'integrating') x.phase = 'collected';
|
|
326
|
+
} else if (x.phase !== 'ended') {
|
|
327
|
+
Object.assign(x, { phase: 'ended', disposition: 'interrupted', failureClass: 'interrupted', endedAt: iso(c.clock()), usage: usageFor(x, null, []) });
|
|
328
|
+
}
|
|
329
|
+
return { write: true, result: end ? end.result : null };
|
|
330
|
+
});
|
|
331
|
+
return { ...base, action: 'undone', reason: 'lease holder lost' };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── IC-029: pause and inspect ────────────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
const activeOf = (s ) => (Object.values(s.dispatches) ).filter((d) => d.phase !== 'ended')
|
|
337
|
+
.map((d) => ({ dispatchId: d.dispatchId, task: d.task, token: d.token ?? null, phase: d.phase, mode: d.mode }));
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* IC-029 pause: IC-001's identity checks, then the in-process 080 `pause` inside the dispatch lock.
|
|
341
|
+
* `{ disposition: 'paused', dispatchRunId, runKey, active }`; 080's refusals (`control-unchanged`,
|
|
342
|
+
* `run-cancelled`) pass through. Admission and child launch then start nothing; collection,
|
|
343
|
+
* integration, verification and timeouts still run.
|
|
344
|
+
*/
|
|
345
|
+
async function pause(options = {}) {
|
|
346
|
+
const c = contextFor(options);
|
|
347
|
+
const by = requireBy(options.by);
|
|
348
|
+
const reason = requireReason(options.reason, 'why the dispatch run is being paused');
|
|
349
|
+
const caller = checkCaller({ env: c.env, cwd: c.cwd, file: c.file });
|
|
350
|
+
if (caller.refusal) return caller.refusal;
|
|
351
|
+
return withDriver(c, async () => mutateDispatch(c.file, (s ) => {
|
|
352
|
+
const res = attempts.pause({ ...c.common(), by, reason });
|
|
353
|
+
if (res.disposition !== 'paused') return { write: false, result: res };
|
|
354
|
+
return { write: false, result: { disposition: 'paused', dispatchRunId: s.run.dispatchRunId, runKey: res.runKey, active: activeOf(s) } };
|
|
355
|
+
}));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** IC-029's DispatchView for one dispatch. */
|
|
359
|
+
function viewOf(c , d , runs , ledger ) {
|
|
360
|
+
const run = d.workerRunId ? runs[d.workerRunId] : null;
|
|
361
|
+
const status = run ? statusView(run, ledger, { platform: c.platform, nowMs: c.clock().getTime() }) : null;
|
|
362
|
+
const ms = d.claimedAt && d.endedAt ? Date.parse(d.endedAt) - Date.parse(d.claimedAt) : null;
|
|
363
|
+
return {
|
|
364
|
+
dispatchId: d.dispatchId, parentRunKey: d.parentRunKey ?? null, task: d.task, token: d.token ?? null, epoch: d.epoch ?? null,
|
|
365
|
+
phase: d.phase, mode: d.mode, harness: d.harness ?? null, disposition: d.disposition ?? null, failureClass: d.failureClass ?? null,
|
|
366
|
+
workerRun: d.workerRunId ? { id: d.workerRunId, state: status ? status.state : 'missing', reason: status ? status.reason : 'run-missing', cleanup: status ? status.cleanup : null } : null,
|
|
367
|
+
worktree: d.attempt && d.attempt.worktree
|
|
368
|
+
? { path: d.attempt.worktree, branch: d.attempt.branch, base: d.attempt.base, exists: fs.existsSync(d.attempt.worktree), removal: d.removal ?? null }
|
|
369
|
+
: null,
|
|
370
|
+
usage: d.usage ?? null, durationMs: Number.isFinite(ms) ? ms : null,
|
|
371
|
+
undeclared: Array.isArray(d.undeclared) ? d.undeclared : [],
|
|
372
|
+
envelopes: { handoff: d.envelopes ? d.envelopes.handoff ?? null : null, result: d.envelopes ? d.envelopes.result ?? null : null },
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* IC-029 inspect: IC-001's identity checks, then the store, 080 `inspect` and 081 `statusView` for
|
|
378
|
+
* each dispatch's worker run (which runs `ps`). Takes no lock and writes nothing. Every verification
|
|
379
|
+
* argv element is masked by `maskLine`.
|
|
380
|
+
*/
|
|
381
|
+
function inspect(options = {}) {
|
|
382
|
+
const c = contextFor(options);
|
|
383
|
+
const caller = checkCaller({ env: c.env, cwd: c.cwd, file: c.file });
|
|
384
|
+
if (caller.refusal) return caller.refusal;
|
|
385
|
+
const s = caller.store;
|
|
386
|
+
const run = s.run;
|
|
387
|
+
const view = attempts.inspect({ file: c.ledgerFile, fsImpl: fs, roots: c.roots, slug: c.slug, env: c.env, cwd: c.cwd });
|
|
388
|
+
const ledger = c.ledger();
|
|
389
|
+
const runs = readRuns(c.runStore);
|
|
390
|
+
const verify = run.verify || {};
|
|
391
|
+
const argv = Array.isArray(verify.argv) ? verify.argv.map((a ) => maskLine(String(a)).text) : [];
|
|
392
|
+
const verifications = Array.isArray(run.verifications) ? run.verifications : [];
|
|
393
|
+
const gaps = run.blockedReport && Array.isArray(run.blockedReport.gaps) ? run.blockedReport.gaps : (run.integrationGap ? [run.integrationGap] : []);
|
|
394
|
+
return {
|
|
395
|
+
disposition: 'inspected', dispatchRunId: run.dispatchRunId, slug: c.slug, runKey: run.runKey,
|
|
396
|
+
state: run.state, control: view.run.control, epoch: view.run.epoch ?? run.epoch ?? null,
|
|
397
|
+
concurrency: run.concurrency, mode: run.mode,
|
|
398
|
+
verification: {
|
|
399
|
+
declared: Array.isArray(verify.argv), argv, argc: Array.isArray(verify.argv) ? verify.argv.length : 0,
|
|
400
|
+
sha256: verify.argvSha256 ?? null, seconds: verify.timeoutSeconds ?? null, last: verifications.length ? verifications[verifications.length - 1] : null,
|
|
401
|
+
},
|
|
402
|
+
integration: run.integration
|
|
403
|
+
? { worktree: run.integration.worktree, branch: run.integration.branch, base: run.integration.base, head: run.integration.head, lease: s.lease ?? null }
|
|
404
|
+
: null,
|
|
405
|
+
dispatches: (Object.values(s.dispatches) ).map((d) => viewOf(c, d, runs, ledger)),
|
|
406
|
+
drivers: (s.drivers ).map((d) => ({ pid: d.pid, start: d.start ?? null, registeredAt: d.registeredAt ?? null, endedAt: d.endedAt ?? null, lost: d.lost ?? null })),
|
|
407
|
+
spawnRequests: Object.values(s.spawnRequests),
|
|
408
|
+
gaps,
|
|
409
|
+
measurement: measurement(s),
|
|
410
|
+
tasks: view.tasks.map((t ) => ({ id: t.id, state: t.state, reason: t.reason, budget: { used: t.budget.used, limit: t.budget.limit } })),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** IC-029's text form: one line per dispatch, then the run, verification and measurement lines. No
|
|
415
|
+
* envelope or output content. */
|
|
416
|
+
function inspectLines(result ) {
|
|
417
|
+
if (result.disposition !== 'inspected') return [`${result.disposition} ${result.reason ?? ''}`.trim()];
|
|
418
|
+
const lines = result.dispatches.map((d ) => {
|
|
419
|
+
const worker = d.workerRun ? ` worker ${d.workerRun.id} ${d.workerRun.state}` : '';
|
|
420
|
+
return `${d.dispatchId} ${d.task} token ${d.token ?? '-'} ${d.phase} ${d.mode}${worker}`;
|
|
421
|
+
});
|
|
422
|
+
const v = result.verification;
|
|
423
|
+
const t = result.measurement.tokens.total;
|
|
424
|
+
lines.push(`run ${result.dispatchRunId} ${result.state} control ${result.control} epoch ${result.epoch} concurrency ${result.concurrency} mode ${result.mode}`);
|
|
425
|
+
lines.push(`verification ${v.declared ? `argc ${v.argc} sha256 ${v.sha256 ?? '-'} ${v.seconds}s` : 'undeclared'}${v.last ? ` last exit ${v.last.exitCode}` : ''}`);
|
|
426
|
+
lines.push(`measurement tokens measured ${t.measured} estimated ${t.estimated} peak workers ${result.measurement.peak.workers} worktrees ${result.measurement.peak.worktrees}`);
|
|
427
|
+
return lines;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── IC-030: cancel ───────────────────────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
/** IC-030 step 2: `run-ended`, `cancel-in-progress`, or null. */
|
|
433
|
+
function cancelRefusal(run , at ) {
|
|
434
|
+
if (RUN_ENDED.includes(run.state)) return refused('run-ended', { state: run.state });
|
|
435
|
+
if (run.state === 'cancelling' && run.cancel) {
|
|
436
|
+
const requested = Date.parse(run.cancel.at);
|
|
437
|
+
if (Number.isFinite(requested) && at.getTime() - requested < CANCEL_TAKEOVER_MS) return refused('cancel-in-progress', { at: run.cancel.at });
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** IC-030 step 8: removes every ended attempt and child worktree whose worker cleanup is `verified`
|
|
443
|
+
* or that had no worker, failed ones included; `dropped` are admitted slots step 7 dropped. */
|
|
444
|
+
function removeWorktrees(c , dropped ) {
|
|
445
|
+
const removed = [];
|
|
446
|
+
const kept = [];
|
|
447
|
+
const manager = integrate.managerFor(c.root, c.slug, 'attempt', c.env);
|
|
448
|
+
const s = readDispatch(c.file);
|
|
449
|
+
const runs = readRuns(c.runStore);
|
|
450
|
+
for (const d of Object.values(s.dispatches) ) {
|
|
451
|
+
if (d.phase !== 'ended' || !d.attempt || !d.attempt.worktree || (d.removal && d.removal.verified)) continue;
|
|
452
|
+
const cleanup = workerCleanupOf(c, d, runs);
|
|
453
|
+
if (d.workerRunId && !(cleanup && cleanup.status === 'verified')) { kept.push({ path: d.attempt.worktree, reason: 'worker-cleanup-unverified' }); continue; }
|
|
454
|
+
const due = integrate.removalDue({ ...d, cleanup: d.cleanup ?? (cleanup ? { cleanup } : null) }, { state: 'cancelled' });
|
|
455
|
+
if (!due.due) { kept.push({ path: d.attempt.worktree, reason: due.reason }); continue; }
|
|
456
|
+
const removal = manager.removeVerified(d.dispatchId);
|
|
457
|
+
storeRemoval(c, d.dispatchId, removal);
|
|
458
|
+
if (removal.verified) removed.push(removal.path);
|
|
459
|
+
else kept.push({ path: removal.path, reason: 'removal-unverified' });
|
|
460
|
+
}
|
|
461
|
+
for (const id of dropped) {
|
|
462
|
+
const wt = manager.pathFor(id);
|
|
463
|
+
if (!fs.existsSync(wt)) continue;
|
|
464
|
+
const removal = manager.removeVerified(id);
|
|
465
|
+
if (removal.verified) removed.push(removal.path);
|
|
466
|
+
else kept.push({ path: removal.path, reason: 'removal-unverified' });
|
|
467
|
+
}
|
|
468
|
+
const done = {};
|
|
469
|
+
for (const r of Object.values(s.spawnRequests) ) {
|
|
470
|
+
if (!r.worktree || (r.removal && r.removal.verified)) continue;
|
|
471
|
+
// DEC-047 / IC-021: verified only when observed; an `exited` child read through `exitedCleanup`.
|
|
472
|
+
const run = r.childRunId ? runs[r.childRunId] : null;
|
|
473
|
+
const cleanup = r.cleanup ?? (run && run.phase === 'ended' ? run.cleanup ?? (run.end && run.end.state === 'exited' ? exitedCleanup(run, c.platform) : null) : null);
|
|
474
|
+
if (!(cleanup && cleanup.status === 'verified')) { kept.push({ path: r.worktree, reason: 'worker-cleanup-unverified' }); continue; }
|
|
475
|
+
const removal = manager.removeVerified(r.requestId);
|
|
476
|
+
done[r.requestId] = removal;
|
|
477
|
+
if (removal.verified) removed.push(removal.path);
|
|
478
|
+
else kept.push({ path: removal.path, reason: 'removal-unverified' });
|
|
479
|
+
}
|
|
480
|
+
if (Object.keys(done).length) {
|
|
481
|
+
mutateDispatch(c.file, (st ) => {
|
|
482
|
+
for (const [id, removal] of Object.entries(done) ) {
|
|
483
|
+
const r = st.spawnRequests[id];
|
|
484
|
+
if (r && (!r.removal || (r.removal.verified === false && removal.verified))) r.removal = removal;
|
|
485
|
+
}
|
|
486
|
+
return { write: true, result: null };
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
return { removed, kept };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* IC-030 cancel. In order: IC-001's identity checks; `run-ended` or `cancel-in-progress`; the run
|
|
494
|
+
* `cancelling`; every worker, child and matching stray 081 run ended through `cancelRun` with the
|
|
495
|
+
* canceller's own env and cwd; the in-process 080 run cancel; an `integrating` dispatch left to a
|
|
496
|
+
* live lease holder, else IC-032's lease rule; every other non-ended dispatch `ended` `cancelled`
|
|
497
|
+
* (admitted slots dropped); worktree removal; the run `cancelled`. `cleanupVerified` is true only
|
|
498
|
+
* when every worker cleanup and every removal is `verified`.
|
|
499
|
+
*/
|
|
500
|
+
async function cancel(options = {}) {
|
|
501
|
+
const c = contextFor(options);
|
|
502
|
+
const by = requireBy(options.by);
|
|
503
|
+
const reason = requireReason(options.reason, 'why the dispatch run is being cancelled');
|
|
504
|
+
const caller = checkCaller({ env: c.env, cwd: c.cwd, file: c.file });
|
|
505
|
+
if (caller.refusal) return caller.refusal;
|
|
506
|
+
const early = cancelRefusal(caller.store.run, c.clock());
|
|
507
|
+
if (early) return early;
|
|
508
|
+
|
|
509
|
+
return withDriver(c, async () => {
|
|
510
|
+
// Step 3.
|
|
511
|
+
const at = c.clock();
|
|
512
|
+
const marked = mutateDispatch(c.file, (s ) => {
|
|
513
|
+
const busy = cancelRefusal(s.run, at);
|
|
514
|
+
if (busy) return { write: false, result: busy };
|
|
515
|
+
setRunState(s, 'cancelling', 'cancel');
|
|
516
|
+
s.run.cancel = { at: iso(at), by, reason };
|
|
517
|
+
return { write: true, result: null };
|
|
518
|
+
});
|
|
519
|
+
if (marked) return marked;
|
|
520
|
+
|
|
521
|
+
// Step 4, outside every lock.
|
|
522
|
+
let s = readDispatch(c.file);
|
|
523
|
+
const targets = new Map ();
|
|
524
|
+
for (const d of Object.values(s.dispatches) ) if (d.phase !== 'ended' && d.workerRunId) targets.set(d.workerRunId, { dispatchId: d.dispatchId, requestId: null });
|
|
525
|
+
childTargets(s, null, targets);
|
|
526
|
+
strayTargets(s, readRuns(c.runStore), () => true, targets);
|
|
527
|
+
const workers = await endWorkers(c, targets, by, reason);
|
|
528
|
+
|
|
529
|
+
// Step 5.
|
|
530
|
+
const runResult = mutateDispatch(c.file, (st ) => {
|
|
531
|
+
let res ;
|
|
532
|
+
try {
|
|
533
|
+
res = attempts.cancel({ ...c.common(), by, reason });
|
|
534
|
+
} catch (error ) {
|
|
535
|
+
res = { disposition: 'error', reason: error && error.code ? error.code : 'cancel-threw' };
|
|
536
|
+
}
|
|
537
|
+
const kept = { disposition: res.disposition, reason: res.reason ?? null, cancelled: Array.isArray(res.cancelled) ? res.cancelled : [] };
|
|
538
|
+
st.run.cancelResult = kept;
|
|
539
|
+
return { write: true, result: kept };
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Step 6.
|
|
543
|
+
let integrationInProgress = null;
|
|
544
|
+
let lease = null;
|
|
545
|
+
s = readDispatch(c.file);
|
|
546
|
+
if (s.lease) {
|
|
547
|
+
const leased = s.lease.dispatchId;
|
|
548
|
+
lease = await leaseRule(c);
|
|
549
|
+
if (!lease || lease.action === 'blocked') integrationInProgress = leased;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Step 7.
|
|
553
|
+
const runsNow = readRuns(c.runStore);
|
|
554
|
+
const dropped = mutateDispatch(c.file, (st ) => {
|
|
555
|
+
const out = [];
|
|
556
|
+
for (const d of Object.values(st.dispatches) ) {
|
|
557
|
+
if (d.phase === 'ended' || d.dispatchId === integrationInProgress) continue;
|
|
558
|
+
if (d.phase === 'admitted') { delete st.dispatches[d.dispatchId]; out.push(d.dispatchId); continue; }
|
|
559
|
+
Object.assign(d, {
|
|
560
|
+
phase: 'ended', disposition: 'cancelled', endedAt: iso(c.clock()),
|
|
561
|
+
usage: usageFor(d, d.workerRunId ? runsNow[d.workerRunId] ?? null : null, []),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return { write: true, result: out };
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Step 7, second net: a worker or child a launch started after step 4 read the run store, whose
|
|
568
|
+
// record write the cancel mark then refused, is ended too (IC-030: every worker is cancelled).
|
|
569
|
+
const late = new Map ();
|
|
570
|
+
const after = readDispatch(c.file);
|
|
571
|
+
childTargets(after, null, late);
|
|
572
|
+
strayTargets(after, readRuns(c.runStore), () => true, late);
|
|
573
|
+
for (const id of targets.keys()) late.delete(id);
|
|
574
|
+
if (late.size) workers.push(...await endWorkers(c, late, by, reason));
|
|
575
|
+
|
|
576
|
+
// Step 8, outside.
|
|
577
|
+
const worktrees = removeWorktrees(c, dropped);
|
|
578
|
+
|
|
579
|
+
// Step 9.
|
|
580
|
+
mutateDispatch(c.file, (st ) => {
|
|
581
|
+
setRunState(st, 'cancelled', 'cancel');
|
|
582
|
+
st.run.endedAt = iso(c.clock());
|
|
583
|
+
return { write: true, result: null };
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const cleanupVerified = workers.every((w) => w.refused === null && w.cleanup && w.cleanup.status === 'verified')
|
|
587
|
+
&& worktrees.kept.length === 0 && integrationInProgress === null;
|
|
588
|
+
return {
|
|
589
|
+
disposition: 'cancelled', dispatchRunId: s.run.dispatchRunId, workers, run: runResult,
|
|
590
|
+
integrationInProgress, lease: lease ? { dispatchId: lease.dispatchId, action: lease.action } : null, worktrees, cleanupVerified,
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ── IC-031: take over ────────────────────────────────────────────────────────────────────────────
|
|
596
|
+
|
|
597
|
+
/** IC-031 step 5's refusal for a dispatch phase, or null for `claimed` and `launched`. */
|
|
598
|
+
function phaseRefusal(d ) {
|
|
599
|
+
if (d.phase === 'claimed' || d.phase === 'launched') return null;
|
|
600
|
+
if (d.phase === 'admitted') return refused('not-claimed', { dispatchId: d.dispatchId, phase: d.phase });
|
|
601
|
+
return refused('integration-in-progress', { dispatchId: d.dispatchId, phase: d.phase });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* IC-031 take over: after IC-031's refusal table, one locked write releases the attempt (reason
|
|
606
|
+
* `takeover: <reason>`, usage the worker's when it reported, else the reservation as estimated),
|
|
607
|
+
* ends the dispatch `taken-over` and holds the task for a main-session attempt; then, outside the
|
|
608
|
+
* lock, `cancelRun` ends its worker run and children, whose 080 cancel is refused
|
|
609
|
+
* `no-active-attempt` because the release came first (DEC-035).
|
|
610
|
+
*/
|
|
611
|
+
async function takeOver(options = {}) {
|
|
612
|
+
const c = contextFor(options);
|
|
613
|
+
const by = requireBy(options.by);
|
|
614
|
+
const reason = requireReason(options.reason, 'why the attempt is being taken over');
|
|
615
|
+
const task = options.task;
|
|
616
|
+
if (typeof task !== 'string' || !task) throw usage('--task is required: the plan task to take over');
|
|
617
|
+
const caller = checkCaller({ env: c.env, cwd: c.cwd, file: c.file });
|
|
618
|
+
if (caller.refusal) return caller.refusal;
|
|
619
|
+
const plan = attempts.loadPlan({ roots: c.roots, slug: c.slug, fsImpl: fs });
|
|
620
|
+
if (!plan.graph.tasks.some((t ) => t.id === task)) return refused('unknown-task', { task });
|
|
621
|
+
const d = (Object.values(caller.store.dispatches) ).find((x) => x.task === task && x.phase !== 'ended');
|
|
622
|
+
if (!d) return refused('no-active-dispatch', { task });
|
|
623
|
+
if (d.mode !== 'subagent') return refused('already-main-session', { task, dispatchId: d.dispatchId, mode: d.mode });
|
|
624
|
+
const early = phaseRefusal(d);
|
|
625
|
+
if (early) return early;
|
|
626
|
+
|
|
627
|
+
return withDriver(c, async () => {
|
|
628
|
+
const runs = readRuns(c.runStore);
|
|
629
|
+
const workerRun = d.workerRunId ? runs[d.workerRunId] ?? null : null;
|
|
630
|
+
const children = (Object.values(caller.store.spawnRequests) )
|
|
631
|
+
.filter((r) => r.granterDispatchId === d.dispatchId && r.childRunId && runs[r.childRunId]).map((r) => runs[r.childRunId]);
|
|
632
|
+
const spent = usageFor(d, workerRun, children);
|
|
633
|
+
const at = c.clock();
|
|
634
|
+
const out = mutateDispatch(c.file, (s ) => {
|
|
635
|
+
const x = s.dispatches[d.dispatchId];
|
|
636
|
+
if (!x || x.phase === 'ended') return { write: false, result: refused('no-active-dispatch', { task }) };
|
|
637
|
+
const late = phaseRefusal(x);
|
|
638
|
+
if (late) return { write: false, result: late };
|
|
639
|
+
const end = releaseAttempt({ store: s, dispatchId: x.dispatchId, reason: `takeover: ${reason}`, disposition: 'taken-over', usage: spent, ctx: c.ctx() });
|
|
640
|
+
if (!end.result.ended) return { write: end.write, result: refused('release-refused', { task, dispatchId: x.dispatchId, cause: end.result.reason ?? end.result.outcome ?? null }) };
|
|
641
|
+
x.takeover = { at: iso(at), by, reason };
|
|
642
|
+
s.run.held = { ...(s.run.held || {}), [task]: { at: iso(at), by } };
|
|
643
|
+
return { write: true, result: { ended: true, mode: s.run.mode === 'manual' ? 'manual' : 'sequential' } };
|
|
644
|
+
});
|
|
645
|
+
if (!out.ended) return out;
|
|
646
|
+
|
|
647
|
+
// Outside the lock: the worker run and its children.
|
|
648
|
+
const targets = new Map ();
|
|
649
|
+
if (d.workerRunId) targets.set(d.workerRunId, { dispatchId: d.dispatchId, requestId: null });
|
|
650
|
+
childTargets(readDispatch(c.file), d.dispatchId, targets);
|
|
651
|
+
const rows = await endWorkers(c, targets, by, `takeover: ${reason}`);
|
|
652
|
+
let workerRunOut = null;
|
|
653
|
+
if (d.workerRunId) {
|
|
654
|
+
const row = rows.find((r) => r.workerRunId === d.workerRunId);
|
|
655
|
+
const after = readRuns(c.runStore)[d.workerRunId];
|
|
656
|
+
workerRunOut = row
|
|
657
|
+
? { id: d.workerRunId, endState: row.endState, cleanup: row.cleanup }
|
|
658
|
+
: { id: d.workerRunId, endState: after && after.end ? after.end.state : null, cleanup: after ? after.cleanup ?? null : null };
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
disposition: 'taken-over', dispatchId: d.dispatchId, task, token: d.token, released: true,
|
|
662
|
+
workerRun: workerRunOut, held: { task, mode: out.mode },
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── IC-032: resume ───────────────────────────────────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
/** IC-032 step 3 for one dispatch: its rows in table order, re-read after each. */
|
|
670
|
+
async function settleOne(c , dispatchId , mine , settled ) {
|
|
671
|
+
const record = (phase , action , reason ) => settled.push({ dispatchId, phase, action, reason });
|
|
672
|
+
let s = readDispatch(c.file);
|
|
673
|
+
let d = s.dispatches[dispatchId];
|
|
674
|
+
if (!d || d.phase === 'ended') return;
|
|
675
|
+
|
|
676
|
+
if (d.phase === 'admitted') {
|
|
677
|
+
if (ownerLive(s, d.admittedBy, mine)) { record('admitted', 'left', 'driver-live'); return; }
|
|
678
|
+
const entry = c.ledger().tasks[d.task];
|
|
679
|
+
const active = entry && Array.isArray(entry.attempts) ? entry.attempts.find((a ) => a.state === 'active') : null;
|
|
680
|
+
const wt = d.attempt ? d.attempt.worktree : null;
|
|
681
|
+
if (active && active.runKey === s.run.runKey && wt && active.execWorktree && path.resolve(active.execWorktree) === path.resolve(wt)) {
|
|
682
|
+
const adopted = mutateDispatch(c.file, (st ) => {
|
|
683
|
+
const x = st.dispatches[dispatchId];
|
|
684
|
+
if (!x || x.phase !== 'admitted') return { write: false, result: false };
|
|
685
|
+
Object.assign(x, {
|
|
686
|
+
phase: 'claimed', token: active.token, epoch: active.epoch ?? null, claimedAt: active.startedAt ?? iso(c.clock()),
|
|
687
|
+
claimedBy: x.admittedBy, reservation: { tokens: active.reserve ? active.reserve.tokens ?? null : null },
|
|
688
|
+
});
|
|
689
|
+
return { write: true, result: true };
|
|
690
|
+
});
|
|
691
|
+
if (!adopted) return;
|
|
692
|
+
record('admitted', 'adopted', `claim token ${active.token}`);
|
|
693
|
+
} else {
|
|
694
|
+
const dropped = mutateDispatch(c.file, (st ) => {
|
|
695
|
+
const x = st.dispatches[dispatchId];
|
|
696
|
+
if (!x || x.phase !== 'admitted') return { write: false, result: false };
|
|
697
|
+
delete st.dispatches[dispatchId];
|
|
698
|
+
return { write: true, result: true };
|
|
699
|
+
});
|
|
700
|
+
if (!dropped) return;
|
|
701
|
+
const removal = wt && fs.existsSync(wt) ? integrate.managerFor(c.root, c.slug, 'attempt', c.env).removeVerified(dispatchId) : null;
|
|
702
|
+
record('admitted', 'dropped', removal ? (removal.verified ? 'worktree-removed' : 'worktree-kept') : 'no-worktree');
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
s = readDispatch(c.file);
|
|
708
|
+
d = s.dispatches[dispatchId];
|
|
709
|
+
if (d && d.phase === 'claimed') {
|
|
710
|
+
if (ownerLive(s, d.claimedBy, mine)) { record('claimed', 'left', 'driver-live'); return; }
|
|
711
|
+
const run = findRun(c.runStore, { task: d.task, token: d.token });
|
|
712
|
+
if (run) {
|
|
713
|
+
const at = Date.parse(run.createdAt ?? run.startedAt ?? '');
|
|
714
|
+
const launchedAt = Number.isFinite(at) ? new Date(at) : c.clock();
|
|
715
|
+
const seconds = Number.isInteger(s.run.attemptSeconds) ? s.run.attemptSeconds : ATTEMPT_SECONDS_DEFAULT;
|
|
716
|
+
const moved = mutateDispatch(c.file, (st ) => {
|
|
717
|
+
const x = st.dispatches[dispatchId];
|
|
718
|
+
if (!x || x.phase !== 'claimed') return { write: false, result: false };
|
|
719
|
+
Object.assign(x, {
|
|
720
|
+
phase: 'launched', workerRunId: run.runId, launchedAt: iso(launchedAt),
|
|
721
|
+
deadline: x.mode === 'subagent' ? iso(new Date(launchedAt.getTime() + seconds * 1000)) : null,
|
|
722
|
+
workerIdentity: run.identity ? { ...run.identity } : null,
|
|
723
|
+
});
|
|
724
|
+
return { write: true, result: true };
|
|
725
|
+
});
|
|
726
|
+
if (!moved) return;
|
|
727
|
+
record('claimed', 'adopted', `worker run ${run.runId}`);
|
|
728
|
+
} else {
|
|
729
|
+
const ended = mutateDispatch(c.file, (st ) => {
|
|
730
|
+
const x = st.dispatches[dispatchId];
|
|
731
|
+
if (!x || x.phase !== 'claimed') return { write: false, result: { ended: false } };
|
|
732
|
+
return endAttempt({ store: st, dispatchId, failureClass: 'interrupted', detail: null, ctx: c.ctx() });
|
|
733
|
+
});
|
|
734
|
+
record('claimed', ended.ended ? 'released' : 'error', ended.ended ? 'interrupted' : ended.reason || 'release-refused');
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
s = readDispatch(c.file);
|
|
740
|
+
d = s.dispatches[dispatchId];
|
|
741
|
+
if (!d || d.phase !== 'launched') {
|
|
742
|
+
if (d && d.phase !== 'ended' && d.phase !== 'integrating') record(d.phase, 'left', 'owner-not-needed');
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (MAIN_SESSION_MODES.includes(d.mode) || !d.workerRunId) { record('launched', 'left', 'main-session'); return; }
|
|
746
|
+
const { view } = settledStatus(c.runStore, d.workerRunId, c.ledger(), { platform: c.platform, nowMs: c.clock().getTime() });
|
|
747
|
+
if (!view || view.state !== 'unknown') {
|
|
748
|
+
record('launched', 'left', view ? (LIVE_WORKER_STATES.includes(view.state) ? view.state : `ended:${view.state}`) : 'run-missing');
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const res = await endWorker(c, d.workerRunId, LOOP_BY, LOST_WORKER_REASON);
|
|
752
|
+
if (res.disposition !== 'cancelled') { record('launched', 'left', `cancel-${res.reason || res.disposition}`); return; }
|
|
753
|
+
const after = readRuns(c.runStore)[d.workerRunId] ?? null;
|
|
754
|
+
mutateDispatch(c.file, (st ) => {
|
|
755
|
+
const x = st.dispatches[dispatchId];
|
|
756
|
+
if (!x || x.phase !== 'launched') return { write: false, result: null };
|
|
757
|
+
Object.assign(x, {
|
|
758
|
+
phase: 'ended', disposition: 'cancelled', failureClass: 'lost', endedAt: iso(c.clock()),
|
|
759
|
+
usage: usageFor(x, after, []), cleanup: cancelSummary(res),
|
|
760
|
+
});
|
|
761
|
+
return { write: true, result: null };
|
|
762
|
+
});
|
|
763
|
+
record('launched', 'cancelled', 'lost');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** IC-021 for the dispatches settlement ended, so the acknowledged call's report sees the same
|
|
767
|
+
* worktrees the first call's did, whatever a pass between the two would have removed. */
|
|
768
|
+
async function removeSettled(c , settled ) {
|
|
769
|
+
const ended = new Set(settled.filter((x) => ['released', 'cancelled', 'undone', 'adopted'].includes(x.action)).map((x) => x.dispatchId));
|
|
770
|
+
for (const id of ended) {
|
|
771
|
+
const d = readDispatch(c.file).dispatches[id];
|
|
772
|
+
if (d && d.phase === 'ended') await integrate.removeDispatchWorktree({ root: c.root, slug: c.slug, dispatchId: id, env: c.env, storeFile: c.file });
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** IC-032 step 6: unnamed dispatch worktree directories and registered worktrees on this slug's
|
|
777
|
+
* DoFlow branches, by path. Nothing is deleted. */
|
|
778
|
+
function orphansOf(c ) {
|
|
779
|
+
const named = new Set ();
|
|
780
|
+
const nameOf = (p ) => { if (typeof p === 'string' && p) named.add(path.basename(p)); };
|
|
781
|
+
const storesDir = path.dirname(c.file);
|
|
782
|
+
const storeNames = fs.existsSync(storesDir) ? fs.readdirSync(storesDir).filter((f ) => f.endsWith('.json')) : [];
|
|
783
|
+
for (const name of storeNames) {
|
|
784
|
+
let st ;
|
|
785
|
+
try { st = readDispatch(path.join(storesDir, name)); } catch { continue; }
|
|
786
|
+
if (st.run) {
|
|
787
|
+
if (st.run.dispatchRunId) named.add(st.run.dispatchRunId);
|
|
788
|
+
if (st.run.integration) nameOf(st.run.integration.worktree);
|
|
789
|
+
}
|
|
790
|
+
for (const d of Object.values(st.dispatches) ) { named.add(d.dispatchId); if (d.attempt) nameOf(d.attempt.worktree); }
|
|
791
|
+
for (const r of Object.values(st.spawnRequests) ) { if (r.requestId) named.add(r.requestId); nameOf(r.worktree); }
|
|
792
|
+
}
|
|
793
|
+
const real = (p ) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
794
|
+
const out = new Map ();
|
|
795
|
+
const dir = path.join(c.root, integrate.WORKTREES_DIR);
|
|
796
|
+
if (fs.existsSync(dir)) {
|
|
797
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
798
|
+
if (e.isDirectory() && !named.has(e.name)) out.set(real(path.join(dir, e.name)), path.join(dir, e.name));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const listed = c.git(['worktree', 'list', '--porcelain'], c.root);
|
|
802
|
+
if (listed.ok) {
|
|
803
|
+
const prefixes = [`refs/heads/doflow/${c.slug}/`, `refs/heads/doflow-attempt/${c.slug}/`];
|
|
804
|
+
for (const block of listed.stdout.split('\n\n')) {
|
|
805
|
+
const lines = block.split('\n');
|
|
806
|
+
const wt = lines.find((l ) => l.startsWith('worktree '));
|
|
807
|
+
const branch = lines.find((l ) => l.startsWith('branch '));
|
|
808
|
+
if (!wt || !branch) continue;
|
|
809
|
+
const ref = branch.slice('branch '.length);
|
|
810
|
+
const prefix = prefixes.find((p) => ref.startsWith(p));
|
|
811
|
+
if (!prefix) continue;
|
|
812
|
+
const p = wt.slice('worktree '.length);
|
|
813
|
+
if (named.has(ref.slice(prefix.length)) || named.has(path.basename(p))) continue;
|
|
814
|
+
const key = real(p);
|
|
815
|
+
if (!out.has(key)) out.set(key, p);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return [...out.values()].sort();
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* IC-032 resume. IC-001's identity checks and `run-ended`; then, under one driver: settlement of
|
|
823
|
+
* every dispatch by its phase, the lease rule for a lost lease holder, `cancelRun` for each 081 run
|
|
824
|
+
* an ended dispatch left behind, the orphan list, `endedAt` on each lost driver, and last the
|
|
825
|
+
* in-process 080 `resume`. Without `ack`: `ack-required` with 080's report and digest. With it:
|
|
826
|
+
* 080's `resumed` sets the run `running` in the same dispatch-locked write; `report-changed`
|
|
827
|
+
* passes through.
|
|
828
|
+
*/
|
|
829
|
+
async function resume(options = {}) {
|
|
830
|
+
const c = contextFor(options);
|
|
831
|
+
const ack = options.ack === undefined || options.ack === null ? null : String(options.ack);
|
|
832
|
+
if (ack !== null && !ACK.test(ack)) throw usage(`--ack expects the digest resume reported (sha256:<64 hex>), got '${ack}'`);
|
|
833
|
+
const by = ack !== null ? requireBy(options.by) : (options.by === undefined || options.by === null ? null : requireBy(options.by));
|
|
834
|
+
const reason = options.reason === undefined || options.reason === null ? null : requireReason(options.reason, 'why the run is being resumed');
|
|
835
|
+
const caller = checkCaller({ env: c.env, cwd: c.cwd, file: c.file });
|
|
836
|
+
if (caller.refusal) return caller.refusal;
|
|
837
|
+
if (RUN_ENDED.includes(caller.store.run.state)) return refused('run-ended', { state: caller.store.run.state });
|
|
838
|
+
|
|
839
|
+
return withDriver(c, async (mine ) => {
|
|
840
|
+
// Steps 2 and 3: settlement, phase by phase.
|
|
841
|
+
const settled = [];
|
|
842
|
+
for (const id of Object.keys(readDispatch(c.file).dispatches)) await settleOne(c, id, mine, settled);
|
|
843
|
+
|
|
844
|
+
// Step 4: a lost lease.
|
|
845
|
+
let lease = null;
|
|
846
|
+
const leased = readDispatch(c.file).lease;
|
|
847
|
+
if (leased) {
|
|
848
|
+
const ruled = await leaseRule(c);
|
|
849
|
+
lease = ruled ? { dispatchId: ruled.dispatchId, action: ruled.action } : null;
|
|
850
|
+
if (ruled) settled.push({ dispatchId: ruled.dispatchId, phase: 'integrating', action: ruled.action, reason: ruled.reason });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Step 5: 081 runs left behind by an ended dispatch.
|
|
854
|
+
const s = readDispatch(c.file);
|
|
855
|
+
const strays = strayTargets(s, readRuns(c.runStore), (d) => d.phase === 'ended', new Map());
|
|
856
|
+
const rows = await endWorkers(c, strays, LOOP_BY, 'resume: dispatch ended');
|
|
857
|
+
for (const r of rows) settled.push({ dispatchId: r.dispatchId, phase: 'ended', action: r.refused ? 'cancel-refused' : 'worker-cancelled', reason: r.refused ?? r.workerRunId });
|
|
858
|
+
await removeSettled(c, settled);
|
|
859
|
+
|
|
860
|
+
// Step 6.
|
|
861
|
+
const orphans = orphansOf(c);
|
|
862
|
+
|
|
863
|
+
// Step 7: lost drivers get their end, add-only.
|
|
864
|
+
mutateDispatch(c.file, (st ) => {
|
|
865
|
+
let changed = false;
|
|
866
|
+
for (const d of st.drivers ) {
|
|
867
|
+
if (!d || !d.lost || d.endedAt) continue;
|
|
868
|
+
Object.assign(d, { endedAt: iso(c.clock()), endReason: 'lost' });
|
|
869
|
+
changed = true;
|
|
870
|
+
}
|
|
871
|
+
return { write: changed, result: null };
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// Step 8: 080's resume.
|
|
875
|
+
const base = { settled, lease, orphans };
|
|
876
|
+
if (ack === null) {
|
|
877
|
+
const res = attempts.resume({ ...c.common(), by, reason });
|
|
878
|
+
if (res.disposition !== 'ack-required') return { ...res, ...base };
|
|
879
|
+
return { disposition: 'ack-required', ...base, report: res.report, digest: res.digest };
|
|
880
|
+
}
|
|
881
|
+
const res = mutateDispatch(c.file, (st ) => {
|
|
882
|
+
const answer = attempts.resume({ ...c.common(), ack, by, reason });
|
|
883
|
+
if (answer.disposition !== 'resumed') return { write: false, result: answer };
|
|
884
|
+
if (st.run.state === 'resume-required') {
|
|
885
|
+
// A gap the lease rule recorded while the run was held stays a blocked run (IC-018).
|
|
886
|
+
setRunState(st, st.run.integrationGap ? 'blocked' : 'running', 'resume');
|
|
887
|
+
st.run.resumeReason = null;
|
|
888
|
+
}
|
|
889
|
+
st.run.resumedAt = iso(c.clock());
|
|
890
|
+
return { write: true, result: answer };
|
|
891
|
+
});
|
|
892
|
+
if (res.disposition !== 'resumed') return { ...res, ...base };
|
|
893
|
+
return { disposition: 'resumed', ...base, report: res.report, digest: res.digest, epoch: res.epoch };
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
module.exports = {
|
|
898
|
+
pause,
|
|
899
|
+
inspect,
|
|
900
|
+
inspectLines,
|
|
901
|
+
cancel,
|
|
902
|
+
takeOver,
|
|
903
|
+
resume,
|
|
904
|
+
};
|