@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,1835 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Feature 082 C.3 (IC-003, IC-004, IC-005, IC-022, IC-023, IC-025, IC-026, IC-027, IC-028, IC-033
|
|
4
|
+
// launch; DEC-001, DEC-006, DEC-010, DEC-011, DEC-013, DEC-017, DEC-029, DEC-035, DEC-039, DEC-048,
|
|
5
|
+
// DEC-052): the dispatch loop.
|
|
6
|
+
//
|
|
7
|
+
// `step` is one bounded pass in IC-003's fixed order: reconcile, collect, integrate one, time out,
|
|
8
|
+
// launch children, admit, decide. No lock is held from one phase to the next, nor across a git
|
|
9
|
+
// call, a spawn, a signal or a wait (IC-006): every locked write is one `mutateDispatch` whose
|
|
10
|
+
// decide callback makes only the in-process 080 calls of `attempt-end.ts` and `admission.ts`. The
|
|
11
|
+
// pass's last locked write appends its step record. `run` repeats `step` (IC-004) under one driver,
|
|
12
|
+
// waits 500 ms after a pass that did nothing, and stops on a terminal outcome, a perform yield, a
|
|
13
|
+
// pause or `--max-seconds`; on SIGINT or SIGTERM it finishes the phase in progress, writes its
|
|
14
|
+
// driver end and reports exit 130 or 143.
|
|
15
|
+
//
|
|
16
|
+
// Workers are launched only through 081's `executeWorkerCommand` (`run` with `edit: true`, the
|
|
17
|
+
// handoff envelope as `--prompt-file`), outside every lock; a worker run is ended only through 081's
|
|
18
|
+
// `cancelRun`. This module signals nothing and calls no escalation (DEC-047): verification groups
|
|
19
|
+
// are `verify-run.ts`'s. Mode selection (IC-022) applies the static gate (DEC-052) from the declared
|
|
20
|
+
// worker block before any discover or binary lookup, so a harness whose `isolatedEdit` is not
|
|
21
|
+
// `supported` never has its binary looked up.
|
|
22
|
+
//
|
|
23
|
+
// Every write goes through `store.ts` (`updateTaskState`); this module makes no fs write call.
|
|
24
|
+
//
|
|
25
|
+
// Feature 106 A.2 (IC-003, IC-004): the quota fallback ladder. A `quota` end marks its harness
|
|
26
|
+
// exhausted for the rest of the run, in the write that ends it. Selection walks the run's ladder (or
|
|
27
|
+
// registry order) skipping exhausted harnesses; when the task would change rung (after its own quota
|
|
28
|
+
// end, or past an exhausted rung), the task's owner and the run's mode decide: a hold (`quota`,
|
|
29
|
+
// `frontier-no-step`, `ladder-exhausted`), or in `auto` (or once, for a proposal
|
|
30
|
+
// `resume --fallback auto` accepted) a step to the chosen rung. Feature 120 IC-009 / IC-010 checks
|
|
31
|
+
// current enabled workers at selection and launch; in-session fallback needs both stored and
|
|
32
|
+
// current permission. Invalid settings hold transiently; unset settings preserve the old path.
|
|
33
|
+
// A held task is not admitted while
|
|
34
|
+
// held; a hold is a decide-phase gap with cause `quota` until its task SUCCEEDED. Every quota, step
|
|
35
|
+
// and hold is one event in `run.fallback.events`, one `fallback` StepAction, and for a quota or step
|
|
36
|
+
// a copy on its dispatch. No harness, provider or model id is named here.
|
|
37
|
+
|
|
38
|
+
const fs = require('node:fs');
|
|
39
|
+
const path = require('node:path');
|
|
40
|
+
const crypto = require('node:crypto');
|
|
41
|
+
const attempts = require('../task-attempts.js');
|
|
42
|
+
const { runKeyFor, identityRefusal } = require('../task-run.js');
|
|
43
|
+
const { readTaskState } = require('../task-state.js');
|
|
44
|
+
const { maskLine, maskBody } = require('../mask.js');
|
|
45
|
+
const { projectRoot } = require('../lifecycle/root.js');
|
|
46
|
+
const { loadRegistry } = require('../../registry/index.js');
|
|
47
|
+
const { knownWorkerIds } = require('../../registry/worker-ids.js');
|
|
48
|
+
const { workerCatalogue } = require('../worker/catalogue.js');
|
|
49
|
+
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
50
|
+
const readiness = require('../worker/readiness.js');
|
|
51
|
+
const { executeWorkerCommand, settledStatus } = require('../worker/command.js');
|
|
52
|
+
const { cancelRun } = require('../worker/cancel.js');
|
|
53
|
+
const { activeWorkers, workerGate, inSessionGate } = require('../worker/active.js');
|
|
54
|
+
const { resolveTier, readBindings, resolveBinding, bindingGap } = require('../worker/models.js');
|
|
55
|
+
const { resultOfRun } = require('../worker/supervisor.js');
|
|
56
|
+
const { readRuns, storeFileFor: runStoreFileFor } = require('../worker/run-store.js');
|
|
57
|
+
const {
|
|
58
|
+
storeFileFor, readDispatch, mutateDispatch, setRunState, fallbackOf, inSessionOf, appendFallbackEvent, HELD_RUN_STATES, DispatchStoreError, DISPATCH_ID,
|
|
59
|
+
} = require('./store.js');
|
|
60
|
+
const { registerDriver, endDriver, admit, dispatchModelFor, MAIN_SESSION_MODES, BUDGET_REFUSALS } = require('./admission.js');
|
|
61
|
+
const integrate = require('./integrate.js');
|
|
62
|
+
const { classFor, endAttempt, releaseAttempt, usageFor, SIGNALS } = require('./attempt-end.js');
|
|
63
|
+
const { buildHandoff, buildResult, writeEnvelope, cutUtf8, normalizeFile } = require('./envelope.js');
|
|
64
|
+
const { runVerification } = require('./verify-run.js');
|
|
65
|
+
const { REQUEST_ID } = require('./grants.js');
|
|
66
|
+
|
|
67
|
+
/** IC-003's outcomes. */
|
|
68
|
+
const OUTCOMES = Object.freeze(['continue', 'perform', 'paused', 'completed', 'blocked', 'cancelled', 'resume-required']);
|
|
69
|
+
/** IC-004: the outcomes after which `run` returns. */
|
|
70
|
+
const RUN_STOPS = Object.freeze(['completed', 'blocked', 'cancelled', 'resume-required', 'perform', 'paused']);
|
|
71
|
+
const IDLE_WAIT_MS = 500;
|
|
72
|
+
const RUN_MAX_SECONDS_DEFAULT = 600;
|
|
73
|
+
const RUN_MAX_SECONDS_LIMIT = 14400;
|
|
74
|
+
/** IC-025: 081's own timeout fires this long after the loop's deadline. */
|
|
75
|
+
const BACKSTOP_SECONDS = 120;
|
|
76
|
+
const ATTEMPT_SECONDS_DEFAULT = 1680;
|
|
77
|
+
const LOOP_BY = 'doflow-dispatch';
|
|
78
|
+
const SIGNAL_EXITS = Object.freeze({ SIGINT: 130, SIGTERM: 143 });
|
|
79
|
+
/** IC-022 / DEC-052: the declared-capability requirements checked before any discover. */
|
|
80
|
+
const STATIC_GATE = Object.freeze([
|
|
81
|
+
['one-shot', 'oneShot'], ['structured-output', 'structuredOutput'], ['isolated-edit', 'isolatedEdit'], ['spawn-denial', 'spawnDenial'],
|
|
82
|
+
]);
|
|
83
|
+
/** IC-027: the gap causes in report order. */
|
|
84
|
+
const CAUSE_ORDER = Object.freeze(['input', 'authorization', 'budget', 'quota', 'no-progress']);
|
|
85
|
+
/** 106 IC-005: the run's fallback modes; `off` is the fail-safe. */
|
|
86
|
+
const FALLBACK_MODES = Object.freeze(['off', 'ask', 'auto']);
|
|
87
|
+
const LIVE_WORKER_STATES = Object.freeze(['starting', 'running']);
|
|
88
|
+
/** IC-010: the three chain artifacts every handoff points at, after the declared files. */
|
|
89
|
+
const CHAIN_ARTIFACTS = Object.freeze(['intention/requirement.md', 'design/design.md', 'plan.md']);
|
|
90
|
+
const SUMMARY_BYTES = 8192;
|
|
91
|
+
const SUMMARY_KEPT_BYTES = 2000;
|
|
92
|
+
const BLOCKERS_MAX = 20;
|
|
93
|
+
const BLOCKER_CHARS = 200;
|
|
94
|
+
const REASON_CHARS = 200;
|
|
95
|
+
const BASES = Object.freeze(['measured', 'estimated']);
|
|
96
|
+
/** IC-030 step 3: the run states a cancel has marked. From then on no pass moves the run's state. */
|
|
97
|
+
const CANCEL_MARKED = Object.freeze(['cancelling', 'cancelled']);
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
function usage(message ) {
|
|
102
|
+
return new DispatchStoreError('usage', message);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const defaultSleep = (ms ) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
106
|
+
const iso = (at ) => at.toISOString();
|
|
107
|
+
const sha256 = (bytes ) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
108
|
+
|
|
109
|
+
function refused(reason , extra = {}) {
|
|
110
|
+
return { disposition: 'refused', reason, ...extra };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `reason` as a StepAction reason: masked, at most 200 characters after its token. */
|
|
114
|
+
function clipReason(reason ) {
|
|
115
|
+
const text = maskLine(String(reason ?? '')).text.replace(/\s+/g, ' ').trim();
|
|
116
|
+
const chars = Array.from(text);
|
|
117
|
+
return chars.length <= REASON_CHARS ? text : chars.slice(0, REASON_CHARS).join('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── the caller (IC-001 identity checks, shared with C.5) ─────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* IC-001's identity checks for every action but `start` and `spawn`: the caller is not solo or a
|
|
124
|
+
* worker session, a dispatch run exists and its stored `runKey` is the caller's. Returns
|
|
125
|
+
* `{ key, store }`, or `{ refusal }` with nothing written.
|
|
126
|
+
*/
|
|
127
|
+
function checkCaller({ env, cwd, file } ) {
|
|
128
|
+
const key = runKeyFor(env, cwd);
|
|
129
|
+
const who = identityRefusal(key.identity);
|
|
130
|
+
if (who) return { refusal: refused(who.reason, { runKey: key.runKey }) };
|
|
131
|
+
const current = readDispatch(file);
|
|
132
|
+
if (!current.run) return { refusal: refused('no-dispatch-run') };
|
|
133
|
+
if (current.run.runKey !== key.runKey) return { refusal: refused('foreign-run', { runKey: current.run.runKey, callerRunKey: key.runKey }) };
|
|
134
|
+
if (current.run.billingCap !== undefined && current.run.billingCap !== null && current.run.billingCap !== 'free') {
|
|
135
|
+
return { refusal: refused('billing-cap-invalid', { runKey: current.run.runKey, billingCap: current.run.billingCap }) };
|
|
136
|
+
}
|
|
137
|
+
return { key, store: current };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── mode selection (IC-022) ──────────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/** DEC-052 / IC-012: static capability gaps plus the run's optional billing cap; nothing runs. */
|
|
143
|
+
function staticGate(worker , billingCap , billing ) {
|
|
144
|
+
const gaps = [];
|
|
145
|
+
if (billingCap !== undefined && billingCap !== null && billingCap !== 'free') {
|
|
146
|
+
gaps.push({ requirement: 'billing', result: 'failed', reason: 'billing:cap-invalid' });
|
|
147
|
+
} else if (billingCap === 'free') {
|
|
148
|
+
// `billing` is what evaluateHarnesses judged under the cap: `{ value, source }`, or an invalid
|
|
149
|
+
// user record (`value: 'record-invalid'`, `recordReason`). Without it the registry value decides.
|
|
150
|
+
const judged = billing ?? { value: worker && worker.billing !== undefined ? worker.billing : 'unknown' };
|
|
151
|
+
if (judged.value !== 'free') {
|
|
152
|
+
gaps.push({
|
|
153
|
+
requirement: 'billing', result: 'failed', reason: `billing:${judged.value}`,
|
|
154
|
+
...(judged.source ? { source: judged.source } : {}),
|
|
155
|
+
...(judged.recordReason !== undefined ? { recordReason: judged.recordReason } : {}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const cli = worker ? worker.cli : null;
|
|
160
|
+
if (!cli || cli.status !== 'supported') {
|
|
161
|
+
gaps.push({ requirement: 'cli', result: cli && cli.status === 'unsupported' ? 'failed' : 'unknown', reason: (cli && cli.reason) || (cli ? cli.status : 'not-declared') });
|
|
162
|
+
}
|
|
163
|
+
for (const [requirement, key] of STATIC_GATE) {
|
|
164
|
+
const r = readiness.capabilityRequirement(requirement, worker ? worker[key] : null);
|
|
165
|
+
if (r.result !== 'met') gaps.push({ requirement: r.requirement, result: r.result, reason: r.reason });
|
|
166
|
+
}
|
|
167
|
+
return gaps;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 081 run step 11 for one harness that passed the static gate: discover, then effective probe
|
|
171
|
+
* facts, with the edit baseline and the run's needs. Writes nothing. */
|
|
172
|
+
function evaluateEdit({ harness, worker, needs, registry, coverage, env, cwd, root, now, platform } ) {
|
|
173
|
+
const body = readiness.select({ descriptors: { [harness]: worker }, registry, coverage, harness, needs, edit: true, env, cwd, root, now, platform });
|
|
174
|
+
const c = body.candidates[0];
|
|
175
|
+
return { eligible: Boolean(c && c.eligible), gaps: c ? c.gaps : [] };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** IC-010: unset settings preserve eligibility, including fixture-only worker ids. */
|
|
179
|
+
const gateOf = (active , id ) => (active && active.state !== 'unset' ? workerGate(active, id) : null);
|
|
180
|
+
|
|
181
|
+
/** Every candidate harness in registry order (or only `harness`, or only the `ladder`'s, 106 IC-004),
|
|
182
|
+
* each `{ harness, eligible, gaps }`. IC-010 rejects disabled workers before evaluating them. */
|
|
183
|
+
function evaluateHarnesses({ active = null, harness = null, ladder = null, needs = [], billingCap, descriptors, registry, coverage, env, cwd, root, now = Date.now, platform = process.platform, evaluateImpl = evaluateEdit } ) {
|
|
184
|
+
const reg = registry || loadRegistry({ repoRoot: REPO_ROOT });
|
|
185
|
+
const cov = coverage === undefined ? reg.coverage : coverage;
|
|
186
|
+
const entries = readiness.workerDescriptors({ descriptors, registry: reg })
|
|
187
|
+
.filter((e ) => (!harness || e.harness === harness) && (!Array.isArray(ladder) || ladder.includes(e.harness)));
|
|
188
|
+
if (billingCap !== undefined && billingCap !== null && billingCap !== 'free') {
|
|
189
|
+
return entries.map((entry ) => ({
|
|
190
|
+
harness: entry.harness,
|
|
191
|
+
eligible: false,
|
|
192
|
+
gaps: [{ requirement: 'billing', result: 'failed', reason: 'billing:cap-invalid' }],
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
// The user's billing record matters only under the cap, and is read once per pass. Required here,
|
|
196
|
+
// not at load: the settings modules stay off every path that never runs a capped dispatch.
|
|
197
|
+
const capped = billingCap === 'free' ? require('../settings/worker-billing.js') : null;
|
|
198
|
+
const record = capped ? capped.readWorkerBilling(env, { workerIds: knownWorkerIds({ registry: reg }).ids }) : null;
|
|
199
|
+
return entries.map((entry ) => {
|
|
200
|
+
const closed = gateOf(active, entry.harness);
|
|
201
|
+
if (closed) return { harness: entry.harness, eligible: false, gaps: [{ requirement: 'enabled', result: 'failed', reason: closed.reason }] };
|
|
202
|
+
const billing = !record ? undefined
|
|
203
|
+
: record.state === 'invalid' ? { value: 'record-invalid', source: 'user-record', recordReason: record.reason }
|
|
204
|
+
: capped.effectiveBilling(entry.worker ? entry.worker.billing : undefined, entry.harness, record);
|
|
205
|
+
const gaps = staticGate(entry.worker, billingCap, billing);
|
|
206
|
+
// A candidate that cleared the billing gate under the cap names where its `free` came from.
|
|
207
|
+
const passed = billing && billing.value === 'free'
|
|
208
|
+
? { billing: { value: 'free', source: billing.source, ...(billing.asserted !== undefined ? { asserted: billing.asserted } : {}) } } : {};
|
|
209
|
+
if (gaps.length) return { harness: entry.harness, eligible: false, gaps, ...passed };
|
|
210
|
+
const judged = evaluateImpl({ harness: entry.harness, worker: entry.worker, needs, registry: reg, coverage: cov, env, cwd, root, now, platform });
|
|
211
|
+
return { harness: entry.harness, eligible: Boolean(judged.eligible), gaps: judged.gaps || [], ...passed };
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Owners whose work is review (D-ORCH review decision C-2; 118 IC-009: the `reviewer` agent, so a
|
|
216
|
+
* `quality-guardian` testing task may step). Archetype ids from the plan's `owner`, not harness,
|
|
217
|
+
* provider or model ids. */
|
|
218
|
+
const REVIEW_OWNERS = Object.freeze(['reviewer']);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 106 IC-004 / review C-2, R-1: why a task of plan owner `owner` and task-line tier `tier` never
|
|
222
|
+
* steps to another rung, or null when it may. The tier resolves as brief-tools resolves it: a task
|
|
223
|
+
* line's tier that `dispatchTiers.tiers` lists wins, else the owner's (an invalid one is ignored).
|
|
224
|
+
* A resolved `frontier` is `frontier`, whatever the owner. The owner is matched case-insensitively
|
|
225
|
+
* against `dispatchTiers.byOwner`: one that is missing or not listed (or a registry with no
|
|
226
|
+
* `dispatchTiers`) is `unknown`, a review archetype is `review`. Each holds `frontier-no-step`.
|
|
227
|
+
*/
|
|
228
|
+
function noStepReason(registry , owner , tier = null) {
|
|
229
|
+
const tiers = registry && registry.dispatchTiers;
|
|
230
|
+
const byOwner = tiers && tiers.byOwner;
|
|
231
|
+
const taskTier = typeof tier === 'string' && tiers && Array.isArray(tiers.tiers) && tiers.tiers.includes(tier.trim()) ? tier.trim() : null;
|
|
232
|
+
if (taskTier === 'frontier') return 'frontier';
|
|
233
|
+
const key = typeof owner === 'string' ? owner.trim().toLowerCase() : '';
|
|
234
|
+
const match = key && byOwner ? Object.keys(byOwner).find((name) => name.toLowerCase() === key) : undefined;
|
|
235
|
+
if (match === undefined) return 'unknown';
|
|
236
|
+
if ((taskTier ?? byOwner[match]) === 'frontier') return 'frontier';
|
|
237
|
+
if (REVIEW_OWNERS.includes(match.toLowerCase())) return 'review';
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* IC-022: the mode one attempt of `task` runs in. `held` (a take over) is `manual` under policy
|
|
243
|
+
* `manual`, else `sequential`; policies `manual` and `sequential` are themselves; under `auto` a
|
|
244
|
+
* task whose last failure class is `launch` runs `sequential`, else the first eligible harness runs
|
|
245
|
+
* it as `subagent`, else `sequential`. Returns `{ mode, harness, selection: { policy, chosen,
|
|
246
|
+
* candidates } }`. `cache` (an object) holds the harness evaluation for one pass.
|
|
247
|
+
*
|
|
248
|
+
* 106 IC-004, under `auto` with the run's `fallback`: candidates follow `fallback.ladder` when set
|
|
249
|
+
* (only its harnesses); the chosen rung is the first eligible one not exhausted. `last` is the
|
|
250
|
+
* task's highest-token ended dispatch with a harness (`lastWorkerDispatch`). The rung the task
|
|
251
|
+
* moves `from` (review C-1): after a quota end, that dispatch's harness, so any next harness is a
|
|
252
|
+
* change; else none when the task already runs on the chosen rung; else the first exhausted rung
|
|
253
|
+
* passed. With no `from` the rule above holds exactly. With one: a task that may not step
|
|
254
|
+
* (`noStepReason` over its `owner` and task-line `tier`) holds `frontier-no-step`; no rung left holds
|
|
255
|
+
* `ladder-exhausted`; mode `off` holds `quota` with no proposal and `ask` proposing the chosen rung,
|
|
256
|
+
* unless in `ask` the task accepted exactly that rung (`fallback.accepted`, review C-14 and R-2: one
|
|
257
|
+
* step, `accepted: true`); `auto` runs the chosen
|
|
258
|
+
* rung with `selection.fallback = { kind: 'step', from, to }`. A hold returns `{ hold: { reason,
|
|
259
|
+
* from, proposed }, selection }`. The `launch` shortcut runs first only when nothing is exhausted
|
|
260
|
+
* and `last` is not a quota end (review C-3), else only with no `from`. Pure: the caller persists.
|
|
261
|
+
* IC-010 intersects stored and current in-session permission; settings holds are transient, and a
|
|
262
|
+
* set list may extend an exhausted ladder to sequential under the same quota-step rules.
|
|
263
|
+
*/
|
|
264
|
+
function selectMode({ policy = 'auto', held = false, lastFailureClass = null, cache = null, fallback = null, owner = null, tier = null, task = null, last = null, ...rest } ) {
|
|
265
|
+
const fixed = (mode ) => ({ mode, harness: null, selection: { policy, chosen: mode, candidates: [] } });
|
|
266
|
+
const active = rest.active;
|
|
267
|
+
const live = active && active.state !== 'unset' ? active : null;
|
|
268
|
+
const blocked = () => {
|
|
269
|
+
const closed = live ? inSessionGate(live) : null;
|
|
270
|
+
return closed ? closed.reason : !inSessionOf({ fallback }) ? 'no-enabled-worker' : null;
|
|
271
|
+
};
|
|
272
|
+
const settingsHold = (reason ) => ({
|
|
273
|
+
hold: { reason, from: null, proposed: null, transient: true },
|
|
274
|
+
selection: { policy, chosen: 'held', candidates: [] },
|
|
275
|
+
});
|
|
276
|
+
if (held) return fixed(policy === 'manual' ? 'manual' : 'sequential');
|
|
277
|
+
if (policy === 'manual' || policy === 'sequential') return fixed(policy);
|
|
278
|
+
if (policy !== 'auto') throw usage(`mode policy '${policy}' is not auto, sequential or manual`);
|
|
279
|
+
if (live && live.state === 'invalid') return settingsHold('workers-enabled-invalid');
|
|
280
|
+
const ladder = fallback && Array.isArray(fallback.ladder) ? fallback.ladder : null;
|
|
281
|
+
const exhausted = fallback && Array.isArray(fallback.exhausted) ? fallback.exhausted : [];
|
|
282
|
+
const afterQuota = Boolean(last && last.failureClass === 'quota' && last.harness);
|
|
283
|
+
if (rest.billingCap !== undefined && rest.billingCap !== null && rest.billingCap !== 'free') {
|
|
284
|
+
return { hold: { reason: 'billing-cap-invalid', from: null, proposed: null }, selection: { policy, chosen: 'held', candidates: [] } };
|
|
285
|
+
}
|
|
286
|
+
if (lastFailureClass === 'launch' && !exhausted.length && !afterQuota) {
|
|
287
|
+
const b = blocked();
|
|
288
|
+
return b ? settingsHold(b) : fixed('sequential');
|
|
289
|
+
}
|
|
290
|
+
let candidates = cache && cache.candidates;
|
|
291
|
+
if (!candidates) {
|
|
292
|
+
candidates = evaluateHarnesses({ ...rest, ladder });
|
|
293
|
+
if (cache) cache.candidates = candidates;
|
|
294
|
+
}
|
|
295
|
+
// IC-012: the cached readiness is shared; bindings gate a fresh array for this task's tier.
|
|
296
|
+
if (rest.bindings !== undefined) {
|
|
297
|
+
const reg = rest.registry || loadRegistry({ repoRoot: REPO_ROOT });
|
|
298
|
+
const resolvedTier = resolveTier({ dispatchTiers: reg.dispatchTiers, owner, tier, missing: [] }).tier;
|
|
299
|
+
const entries = readiness.workerDescriptors({ descriptors: rest.descriptors, registry: reg });
|
|
300
|
+
candidates = candidates.map((c ) => {
|
|
301
|
+
if (!c.eligible) return c;
|
|
302
|
+
const descriptor = entries.find((entry ) => entry.harness === c.harness)?.worker;
|
|
303
|
+
const gap = bindingGap(resolveBinding({ worker: c.harness, tier: resolvedTier, bindings: rest.bindings, descriptor }));
|
|
304
|
+
return gap ? { ...c, eligible: false, gaps: [...c.gaps, gap] } : c;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (ladder) candidates = ladder.map((id ) => candidates.find((c ) => c.harness === id)).filter(Boolean);
|
|
308
|
+
let passed = null;
|
|
309
|
+
let chosen = null;
|
|
310
|
+
for (const c of candidates) {
|
|
311
|
+
if (exhausted.includes(c.harness)) { passed = passed ?? c.harness; continue; }
|
|
312
|
+
if (c.eligible) { chosen = c; break; }
|
|
313
|
+
}
|
|
314
|
+
let from = passed;
|
|
315
|
+
if (afterQuota) from = last.harness;
|
|
316
|
+
else if (last && chosen && last.harness === chosen.harness) from = null;
|
|
317
|
+
const selection = {
|
|
318
|
+
policy,
|
|
319
|
+
chosen: chosen ? chosen.harness : 'sequential',
|
|
320
|
+
...(!chosen && rest.billingCap === 'free' ? { reason: 'no-eligible-worker' } : {}),
|
|
321
|
+
candidates: structuredClone(candidates),
|
|
322
|
+
};
|
|
323
|
+
if (from === null) {
|
|
324
|
+
if (lastFailureClass === 'launch') {
|
|
325
|
+
const b = blocked();
|
|
326
|
+
return b ? settingsHold(b) : fixed('sequential');
|
|
327
|
+
}
|
|
328
|
+
if (!chosen) {
|
|
329
|
+
const b = blocked();
|
|
330
|
+
if (b) return settingsHold(b);
|
|
331
|
+
}
|
|
332
|
+
return chosen ? { mode: 'subagent', harness: chosen.harness, selection } : { mode: 'sequential', harness: null, selection };
|
|
333
|
+
}
|
|
334
|
+
const hold = (reason , proposed ) => ({ hold: { reason, from, proposed }, selection: { ...selection, chosen: 'held' } });
|
|
335
|
+
if (noStepReason(rest.registry || loadRegistry({ repoRoot: REPO_ROOT }), owner, tier) !== null) return hold('frontier-no-step', null);
|
|
336
|
+
const viaSession = !chosen && live && live.state === 'set' && blocked() === null;
|
|
337
|
+
if (!chosen && !viaSession) return hold('ladder-exhausted', null);
|
|
338
|
+
const target = chosen ? chosen.harness : 'sequential';
|
|
339
|
+
const step = (accepted ) => ({
|
|
340
|
+
mode: chosen ? 'subagent' : 'sequential', harness: chosen ? chosen.harness : null,
|
|
341
|
+
selection: { ...selection, fallback: { kind: 'step', from, to: target, ...(accepted ? { accepted: true } : {}) } },
|
|
342
|
+
});
|
|
343
|
+
const mode = fallback && FALLBACK_MODES.includes(fallback.mode) ? fallback.mode : 'off';
|
|
344
|
+
if (mode === 'auto') return step(false);
|
|
345
|
+
// Review R-2: an acceptance steps only to the rung it accepted, and only in `ask` (an `off` hold
|
|
346
|
+
// proposes nothing, and `resume --fallback off` revokes acceptances); any other chosen rung holds
|
|
347
|
+
// again with the new proposal, and the hold's write drops the acceptance.
|
|
348
|
+
const accepted = fallback && fallback.accepted && typeof fallback.accepted === 'object' && task !== null && Object.hasOwn(fallback.accepted, task)
|
|
349
|
+
? fallback.accepted[task] : undefined;
|
|
350
|
+
if (mode === 'ask' && accepted === target) return step(true);
|
|
351
|
+
return hold('quota', mode === 'ask' ? target : null);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** The failure class of `task`'s most recent failed dispatch (highest token), or null. A `quota` end
|
|
355
|
+
* released its attempt and is skipped (106 IC-003). */
|
|
356
|
+
function lastFailure(current , task ) {
|
|
357
|
+
let last = null;
|
|
358
|
+
for (const d of Object.values(current.dispatches) ) {
|
|
359
|
+
if (d.task !== task || d.phase !== 'ended' || !Number.isInteger(d.token) || d.failureClass === 'quota') continue;
|
|
360
|
+
if (!last || d.token > last.token) last = d;
|
|
361
|
+
}
|
|
362
|
+
return last && last.disposition === 'failed' ? last.failureClass ?? null : null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** 106 review C-1: `task`'s highest-token ended dispatch that ran on a harness, or null. */
|
|
366
|
+
function lastWorkerDispatch(current , task ) {
|
|
367
|
+
let last = null;
|
|
368
|
+
for (const d of Object.values(current.dispatches) ) {
|
|
369
|
+
if (d.task !== task || d.phase !== 'ended' || !Number.isInteger(d.token) || !d.harness) continue;
|
|
370
|
+
if (!last || d.token > last.token) last = d;
|
|
371
|
+
}
|
|
372
|
+
return last;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── measurement (IC-028) ─────────────────────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
const emptyTokens = () => ({ measured: 0, estimated: 0 });
|
|
378
|
+
const emptyDuration = () => ({ count: 0, totalMs: 0, maxMs: 0 });
|
|
379
|
+
|
|
380
|
+
function addDuration(d , ms ) {
|
|
381
|
+
if (!Number.isFinite(ms) || ms < 0) return;
|
|
382
|
+
d.count += 1;
|
|
383
|
+
d.totalMs += ms;
|
|
384
|
+
d.maxMs = Math.max(d.maxMs, ms);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Current worker and worktree counts (IC-028 peaks). */
|
|
388
|
+
function liveCounts(s ) {
|
|
389
|
+
const ds = Object.values(s.dispatches) ;
|
|
390
|
+
const rs = Object.values(s.spawnRequests) ;
|
|
391
|
+
const workers = ds.filter((d) => d.phase === 'launched' && d.mode === 'subagent' && d.workerRunId).length
|
|
392
|
+
+ rs.filter((r) => r.state === 'launched' && r.endState == null).length;
|
|
393
|
+
const worktrees = ds.filter((d) => d.phase !== 'admitted' && d.attempt && d.attempt.worktree && !(d.removal && d.removal.verified)).length
|
|
394
|
+
+ rs.filter((r) => r.worktree && !(r.removal && r.removal.verified)).length;
|
|
395
|
+
return { workers, worktrees };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Raises the stored peaks to the current counts; called inside the loop's locked writes. */
|
|
399
|
+
function bumpPeak(s ) {
|
|
400
|
+
if (!s.run) return;
|
|
401
|
+
const now = liveCounts(s);
|
|
402
|
+
const peak = s.run.peak || { workers: 0, worktrees: 0 };
|
|
403
|
+
s.run.peak = { workers: Math.max(peak.workers || 0, now.workers), worktrees: Math.max(peak.worktrees || 0, now.worktrees) };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** IC-028 from the store: tokens by source and basis, durations and peaks. Pure. */
|
|
407
|
+
function measurement(s ) {
|
|
408
|
+
const tokens = { main: emptyTokens(), worker: emptyTokens(), child: emptyTokens(), total: emptyTokens() };
|
|
409
|
+
const add = (bucket , u ) => {
|
|
410
|
+
if (!u || !Number.isInteger(u.tokens) || !BASES.includes(u.basis)) return;
|
|
411
|
+
tokens[bucket][u.basis] += u.tokens;
|
|
412
|
+
tokens.total[u.basis] += u.tokens;
|
|
413
|
+
};
|
|
414
|
+
const durationsMs = { attempt: emptyDuration(), integration: emptyDuration(), verification: emptyDuration(), run: null };
|
|
415
|
+
for (const d of Object.values(s.dispatches) ) {
|
|
416
|
+
const parts = d.usageParts || (d.usage ? { [d.mode === 'subagent' ? 'worker' : 'main']: d.usage } : null);
|
|
417
|
+
if (parts) for (const [bucket, u] of Object.entries(parts)) add(bucket, u);
|
|
418
|
+
if (d.phase === 'ended' && d.claimedAt && d.endedAt) addDuration(durationsMs.attempt, Date.parse(d.endedAt) - Date.parse(d.claimedAt));
|
|
419
|
+
}
|
|
420
|
+
const run = s.run || {};
|
|
421
|
+
const integration = run.integrationDurations;
|
|
422
|
+
if (integration && Number.isInteger(integration.count)) durationsMs.integration = { count: integration.count, totalMs: integration.totalMs, maxMs: integration.maxMs };
|
|
423
|
+
for (const v of Array.isArray(run.verifications) ? run.verifications : []) addDuration(durationsMs.verification, v && v.durationMs);
|
|
424
|
+
const endAt = run.completedAt || run.endedAt;
|
|
425
|
+
if (run.createdAt && endAt) durationsMs.run = Date.parse(endAt) - Date.parse(run.createdAt);
|
|
426
|
+
const peak = run.peak ? { workers: run.peak.workers || 0, worktrees: run.peak.worktrees || 0 } : liveCounts(s);
|
|
427
|
+
return { tokens, durationsMs, peak };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** IC-008 usage of a subagent dispatch with its parts: the worker's own tokens and its children's. */
|
|
431
|
+
function usageParts(d , workerRun , children ) {
|
|
432
|
+
const total = usageFor(d, workerRun, children);
|
|
433
|
+
if (d.mode !== 'subagent') return { usage: total, parts: { main: total } };
|
|
434
|
+
const own = usageFor(d, workerRun, []);
|
|
435
|
+
const ownMeasured = Boolean(workerRun) && own.basis === 'measured';
|
|
436
|
+
if (!ownMeasured || !children.length) return { usage: total, parts: { worker: total, child: { tokens: 0, basis: 'measured' } } };
|
|
437
|
+
return { usage: total, parts: { worker: own, child: { tokens: total.tokens - own.tokens, basis: total.basis } } };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── the pass context ─────────────────────────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/** One invocation's settings and per-pass scratch. */
|
|
443
|
+
function passFor(options ) {
|
|
444
|
+
const env = options.env || process.env;
|
|
445
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
446
|
+
const slug = options.slug;
|
|
447
|
+
if (typeof slug !== 'string') throw usage('the dispatch loop needs --slug');
|
|
448
|
+
const root = options.root ? path.resolve(options.root) : projectRoot(cwd);
|
|
449
|
+
const file = storeFileFor(root, slug);
|
|
450
|
+
const clock = typeof options.now === 'function'
|
|
451
|
+
? () => { const at = options.now(); return at instanceof Date ? at : new Date(at); }
|
|
452
|
+
: () => new Date();
|
|
453
|
+
const ledgerFile = attempts.storeFile(root, slug);
|
|
454
|
+
const roots = [...new Set([cwd, root])];
|
|
455
|
+
const p = {
|
|
456
|
+
env, cwd, slug, root, file, clock, ledgerFile, roots,
|
|
457
|
+
stateRoot: path.join(root, '.doflow', 'state'),
|
|
458
|
+
runStore: runStoreFileFor(root, slug),
|
|
459
|
+
platform: options.platform || process.platform,
|
|
460
|
+
by: options.by || LOOP_BY,
|
|
461
|
+
descriptors: options.descriptors,
|
|
462
|
+
registry: options.registry || null,
|
|
463
|
+
coverage: options.coverage,
|
|
464
|
+
evaluateImpl: options.evaluateImpl || evaluateEdit,
|
|
465
|
+
verify: options.verify || runVerification,
|
|
466
|
+
shouldStop: typeof options.shouldStop === 'function' ? options.shouldStop : () => false,
|
|
467
|
+
// 106 IC-004 / D3: the mode a run with no `run.fallback` (started before 106) is read with; the
|
|
468
|
+
// command passes the resolved `dispatch.fallback` setting, anything else is the fail-safe `off`.
|
|
469
|
+
fallbackDefault: FALLBACK_MODES.includes(options.fallbackDefault) ? options.fallbackDefault : 'off',
|
|
470
|
+
actions: [] ,
|
|
471
|
+
integrationMs: [] ,
|
|
472
|
+
gaps: [] ,
|
|
473
|
+
perform: null ,
|
|
474
|
+
startedAt: clock(),
|
|
475
|
+
cache: {},
|
|
476
|
+
errors: new Set (),
|
|
477
|
+
};
|
|
478
|
+
p.ctx = () => ({ ledgerFile, roots, slug, env, cwd, fsImpl: fs, now: clock() });
|
|
479
|
+
p.reg = () => {
|
|
480
|
+
if (!p.registry) p.registry = loadRegistry({ repoRoot: REPO_ROOT });
|
|
481
|
+
return p.registry;
|
|
482
|
+
};
|
|
483
|
+
p.activeNow = () => activeWorkers({ registry: p.reg(), env: p.env, projectRoot: p.root });
|
|
484
|
+
p.bindingsNow = () => readBindings({ env: p.env, projectRoot: p.root });
|
|
485
|
+
p.act = (name , d , token , detail = null, ms = 0) => {
|
|
486
|
+
p.actions.push({
|
|
487
|
+
action: name, dispatchId: d && d.dispatchId ? d.dispatchId : null, task: d && d.task ? d.task : null,
|
|
488
|
+
token: d && Number.isInteger(d.token) ? d.token : null,
|
|
489
|
+
reason: detail === null || detail === undefined || detail === '' ? token : `${token}:${clipReason(detail)}`, ms,
|
|
490
|
+
});
|
|
491
|
+
};
|
|
492
|
+
return p;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function readLedger(p ) {
|
|
496
|
+
return readTaskState(fs, p.ledgerFile) || { tasks: {}, runs: {} };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function control(ledger , runKey ) {
|
|
500
|
+
const run = ledger && ledger.runs && runKey && Object.hasOwn(ledger.runs, runKey) ? ledger.runs[runKey] : null;
|
|
501
|
+
return run ? run.control : 'none';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** One locked move of dispatch `id` from one of `phases`, `apply(dispatch, store)` changing it in
|
|
505
|
+
* place; the peaks are raised in the same write. With `open`, the move is also refused while the run
|
|
506
|
+
* holds a state no launch may start under (cancel-marked, `resume-required` or `completed`, IC-009,
|
|
507
|
+
* IC-030), read inside the lock. `{ applied, dispatch, phase }`. */
|
|
508
|
+
function moveDispatch(p , id , phases , apply , { open = false } = {}) {
|
|
509
|
+
return mutateDispatch(p.file, (s ) => {
|
|
510
|
+
const d = s.dispatches[id];
|
|
511
|
+
if (!d || !phases.includes(d.phase)) return { write: false, result: { applied: false, phase: d ? d.phase : null } };
|
|
512
|
+
if (open && (!s.run || HELD_RUN_STATES.includes(s.run.state))) return { write: false, result: { applied: false, phase: `run-${s.run ? s.run.state : 'missing'}` } };
|
|
513
|
+
apply(d, s);
|
|
514
|
+
bumpPeak(s);
|
|
515
|
+
return { write: true, result: { applied: true, dispatch: structuredClone(d) } };
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Sets `cleanup` on a dispatch while absent (IC-006 add-only on an ended dispatch). */
|
|
520
|
+
function recordCleanup(p , id , cleanup ) {
|
|
521
|
+
return mutateDispatch(p.file, (s ) => {
|
|
522
|
+
const d = s.dispatches[id];
|
|
523
|
+
if (!d || d.cleanup != null) return { write: false, result: false };
|
|
524
|
+
d.cleanup = cleanup;
|
|
525
|
+
return { write: true, result: true };
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** The parts of a `cancelRun` result a dispatch keeps. */
|
|
530
|
+
function cancelSummary(result ) {
|
|
531
|
+
if (!result) return null;
|
|
532
|
+
if (result.disposition !== 'cancelled') return { disposition: result.disposition, reason: result.reason ?? null, endState: result.endState ?? null, cleanup: result.cleanup ?? null };
|
|
533
|
+
return { disposition: 'cancelled', endState: result.endState, cleanup: result.cleanup, attemptCancel: result.attemptCancel, attemptCancelled: result.attemptCancelled };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** 081 `cancelRun` for a run of this slug, with the loop's own env and cwd; never throws for a
|
|
537
|
+
* refusal. */
|
|
538
|
+
async function cancelWorker(p , runId , reason , source = 'user') {
|
|
539
|
+
try {
|
|
540
|
+
return await cancelRun({ file: p.runStore, runId, by: LOOP_BY, reason, source, env: p.env, cwd: p.cwd, platform: p.platform, now: p.clock });
|
|
541
|
+
} catch (error ) {
|
|
542
|
+
return { disposition: 'error', reason: error && error.code ? error.code : 'cancel-threw' };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** The child runs a granter's spawn requests launched, read now. */
|
|
547
|
+
function childRunsOf(p , s , dispatchId ) {
|
|
548
|
+
const runs = readRuns(p.runStore);
|
|
549
|
+
return (Object.values(s.spawnRequests) )
|
|
550
|
+
.filter((r) => r.granterDispatchId === dispatchId && r.childRunId && runs[r.childRunId])
|
|
551
|
+
.map((r) => runs[r.childRunId]);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Cancels every child of `dispatchId` whose run has not ended (IC-023, IC-033) and records each. */
|
|
555
|
+
async function cancelChildren(p , dispatchId , reason ) {
|
|
556
|
+
const s = readDispatch(p.file);
|
|
557
|
+
const runs = readRuns(p.runStore);
|
|
558
|
+
for (const r of Object.values(s.spawnRequests) ) {
|
|
559
|
+
if (r.granterDispatchId !== dispatchId || r.state !== 'launched' || r.endState != null || !r.childRunId) continue;
|
|
560
|
+
const run = runs[r.childRunId];
|
|
561
|
+
if (run && run.phase !== 'ended') {
|
|
562
|
+
const res = await cancelWorker(p, r.childRunId, reason);
|
|
563
|
+
p.act('cleanup', { dispatchId, task: s.dispatches[dispatchId]?.task }, 'child-cancelled', `${r.requestId} ${res.disposition}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
settleChildren(p);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Records the end of every launched child run that has ended (endState, report, cleanup). */
|
|
570
|
+
function settleChildren(p ) {
|
|
571
|
+
const s = readDispatch(p.file);
|
|
572
|
+
const runs = readRuns(p.runStore);
|
|
573
|
+
const ended = {};
|
|
574
|
+
for (const r of Object.values(s.spawnRequests) ) {
|
|
575
|
+
if (r.state !== 'launched' || r.endState != null || !r.childRunId) continue;
|
|
576
|
+
const run = runs[r.childRunId];
|
|
577
|
+
if (run && run.phase === 'ended') ended[r.requestId] = run;
|
|
578
|
+
}
|
|
579
|
+
if (!Object.keys(ended).length) return;
|
|
580
|
+
mutateDispatch(p.file, (store ) => {
|
|
581
|
+
for (const [id, run] of Object.entries(ended) ) {
|
|
582
|
+
const r = store.spawnRequests[id];
|
|
583
|
+
if (!r || r.endState != null) continue;
|
|
584
|
+
r.endState = run.end ? run.end.state : 'unknown';
|
|
585
|
+
r.endedAt = run.end ? run.end.at : iso(p.clock());
|
|
586
|
+
r.report = run.report ? { status: run.report.status ?? null, usage: run.report.usage ?? null } : null;
|
|
587
|
+
// DEC-047 / IC-021: an `exited` child's cleanup is what its group reading shows, never assumed.
|
|
588
|
+
r.cleanup = run.cleanup ?? (run.end && run.end.state === 'exited' ? exitedCleanup(run, p.platform) : null);
|
|
589
|
+
}
|
|
590
|
+
bumpPeak(store);
|
|
591
|
+
return { write: true, result: null };
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ── phase 1: reconcile (IC-009, IC-023 staleness, IC-021 deferred removals) ─────────────────────
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* IC-023: ends a stale dispatch in one locked write (released when its attempt is still active,
|
|
599
|
+
* the `epoch` case; else ended with no 080 call), then, outside the lock, ends a worker run that has
|
|
600
|
+
* not ended through `cancelRun`, except in the `epoch` case, whose live worker runs on (FR-018).
|
|
601
|
+
*/
|
|
602
|
+
async function endStale(p , d , reason , workerRun ) {
|
|
603
|
+
const started = Date.now();
|
|
604
|
+
const ended = mutateDispatch(p.file, (s ) => {
|
|
605
|
+
const cur = s.dispatches[d.dispatchId];
|
|
606
|
+
if (!cur || cur.phase === 'ended' || cur.phase === 'integrating' || cur.phase === 'admitted') return { write: false, result: { ended: false } };
|
|
607
|
+
const spent = usageFor(cur, workerRun, []);
|
|
608
|
+
if (reason === 'epoch') {
|
|
609
|
+
const end = releaseAttempt({ store: s, dispatchId: cur.dispatchId, reason: `stale:${reason}`, disposition: 'stale', usage: spent, ctx: p.ctx() });
|
|
610
|
+
if (end.result.ended) cur.staleReason = reason;
|
|
611
|
+
bumpPeak(s);
|
|
612
|
+
return end;
|
|
613
|
+
}
|
|
614
|
+
Object.assign(cur, { phase: 'ended', disposition: 'stale', staleReason: reason, endedAt: iso(p.clock()), usage: spent });
|
|
615
|
+
bumpPeak(s);
|
|
616
|
+
return { write: true, result: { ended: true } };
|
|
617
|
+
});
|
|
618
|
+
p.act('stale', d, reason, ended.ended ? null : ended.reason || 'not-ended', Date.now() - started);
|
|
619
|
+
if (!ended.ended) {
|
|
620
|
+
if (ended.reason) p.errors.add(d.dispatchId);
|
|
621
|
+
return ended;
|
|
622
|
+
}
|
|
623
|
+
if (reason !== 'epoch' && d.workerRunId) {
|
|
624
|
+
const run = readRuns(p.runStore)[d.workerRunId];
|
|
625
|
+
if (run && run.phase !== 'ended') {
|
|
626
|
+
const res = await cancelWorker(p, d.workerRunId, `stale:${reason}`);
|
|
627
|
+
recordCleanup(p, d.dispatchId, cancelSummary(res));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (d.workerRunId) await cancelChildren(p, d.dispatchId, `stale:${reason}`);
|
|
631
|
+
return ended;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* DEC-047 / IC-021: the worker cleanup an `exited` 081 run shows, never assumed. `verified` only
|
|
636
|
+
* from an observed group reading: the supervisor read the group gone (`process.groupAlive: false`)
|
|
637
|
+
* or its escalation verified the leftover. On win32, with no reading, or with an `unknown` one, it is
|
|
638
|
+
* `unknown`; leftover survivors are `survivors`.
|
|
639
|
+
*/
|
|
640
|
+
function exitedCleanup(run , platform ) {
|
|
641
|
+
const proc = (run && run.process) || {};
|
|
642
|
+
const leftover = proc.leftover || null;
|
|
643
|
+
if (leftover && leftover.status === 'survivors') return { status: 'survivors', reason: leftover.reason ?? 'group-survived' };
|
|
644
|
+
if (platform === 'win32') return { status: 'unknown', reason: 'win32' };
|
|
645
|
+
if (proc.groupAlive === false || (leftover && leftover.status === 'verified')) return { status: 'verified', reason: 'exited' };
|
|
646
|
+
return { status: 'unknown', reason: proc.groupAlive === undefined ? 'group-unobserved' : 'group-liveness-unknown' };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** IC-021: records the worker cleanup of each ended dispatch whose run's end now shows it, read per
|
|
650
|
+
* dispatch and settled against the probe, as collect does (RT-7). */
|
|
651
|
+
function recordWorkerCleanups(p ) {
|
|
652
|
+
const s = readDispatch(p.file);
|
|
653
|
+
const ledger = readLedger(p);
|
|
654
|
+
for (const d of Object.values(s.dispatches) ) {
|
|
655
|
+
if (d.phase !== 'ended' || !d.attempt || !d.attempt.worktree || (d.removal && d.removal.verified)) continue;
|
|
656
|
+
if (d.workerRunId && d.cleanup == null) {
|
|
657
|
+
// A pass-wide snapshot read before a supervisor wrote its end and exited reads as lost.
|
|
658
|
+
const { run, view } = settledStatus(p.runStore, d.workerRunId, ledger, { platform: p.platform, nowMs: p.clock().getTime() });
|
|
659
|
+
const exited = view && run.phase === 'ended' && run.end && run.end.state === 'exited' ? exitedCleanup(run, p.platform) : null;
|
|
660
|
+
if (view && run.phase === 'ended' && view.cleanup && view.cleanup.status === 'verified') {
|
|
661
|
+
recordCleanup(p, d.dispatchId, { source: 'status', endState: view.state, cleanup: view.cleanup });
|
|
662
|
+
} else if (exited && exited.status !== 'survivors') {
|
|
663
|
+
recordCleanup(p, d.dispatchId, { source: 'status', endState: 'exited', cleanup: exited });
|
|
664
|
+
} else if (view && view.state === 'unknown' && view.reason === 'process-lost') {
|
|
665
|
+
recordCleanup(p, d.dispatchId, { source: 'status', endState: 'unknown', cleanup: { status: 'verified', reason: 'process-lost' } });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** IC-021: deferred removals of ended attempt and child worktrees. */
|
|
672
|
+
async function deferredRemovals(p ) {
|
|
673
|
+
recordWorkerCleanups(p);
|
|
674
|
+
let s = readDispatch(p.file);
|
|
675
|
+
for (const d of Object.values(s.dispatches) ) {
|
|
676
|
+
if (d.phase !== 'ended' || !d.attempt || !d.attempt.worktree) continue;
|
|
677
|
+
if (!integrate.removalDue(d, s.run).due) continue;
|
|
678
|
+
const started = Date.now();
|
|
679
|
+
const removed = await integrate.removeDispatchWorktree({ root: p.root, slug: p.slug, dispatchId: d.dispatchId, env: p.env, storeFile: p.file });
|
|
680
|
+
p.act('cleanup', d, removed.removed ? 'removed' : 'kept', removed.reason, Date.now() - started);
|
|
681
|
+
}
|
|
682
|
+
// Child worktrees follow their child run's end (IC-021).
|
|
683
|
+
const manager = integrate.managerFor(p.root, p.slug, 'attempt', p.env);
|
|
684
|
+
const runs = readRuns(p.runStore);
|
|
685
|
+
for (const r of Object.values(s.spawnRequests) ) {
|
|
686
|
+
if (!r.worktree || (r.removal && r.removal.verified)) continue;
|
|
687
|
+
// RR-5: a refused launch's request names its cancelled child run but never settles an endState;
|
|
688
|
+
// its cleanup is the ended run's, read as IC-030 step 8 reads it (DEC-047).
|
|
689
|
+
const run = r.endState == null && r.state !== 'launched' && r.childRunId ? runs[r.childRunId] : null;
|
|
690
|
+
const cleanup = r.endState != null ? r.cleanup
|
|
691
|
+
: run && run.phase === 'ended' ? run.cleanup ?? (run.end && run.end.state === 'exited' ? exitedCleanup(run, p.platform) : null) : null;
|
|
692
|
+
if (!(cleanup && cleanup.status === 'verified')) continue;
|
|
693
|
+
const removal = manager.removeVerified(r.requestId);
|
|
694
|
+
mutateDispatch(p.file, (store ) => {
|
|
695
|
+
const cur = store.spawnRequests[r.requestId];
|
|
696
|
+
if (!cur) return { write: false, result: null };
|
|
697
|
+
if (!cur.removal || (cur.removal.verified === false && removal.verified)) cur.removal = removal;
|
|
698
|
+
return { write: true, result: null };
|
|
699
|
+
});
|
|
700
|
+
p.act('cleanup', { dispatchId: r.granterDispatchId }, removal.verified ? 'child-removed' : 'child-kept', r.requestId);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function reconcile(p ) {
|
|
705
|
+
const started = Date.now();
|
|
706
|
+
const current = readDispatch(p.file);
|
|
707
|
+
const ledger = readLedger(p);
|
|
708
|
+
const runs = readRuns(p.runStore);
|
|
709
|
+
for (const d of Object.values(current.dispatches) ) {
|
|
710
|
+
if (!['claimed', 'launched', 'collected'].includes(d.phase) || !Number.isInteger(d.token)) continue;
|
|
711
|
+
const reason = integrate.staleReason(d, ledger, d.parentRunKey ?? current.run.runKey ?? null);
|
|
712
|
+
if (reason) await endStale(p, d, reason, d.workerRunId ? runs[d.workerRunId] ?? null : null);
|
|
713
|
+
}
|
|
714
|
+
settleChildren(p);
|
|
715
|
+
await deferredRemovals(p);
|
|
716
|
+
p.act('reconcile', null, 'reconciled', null, Date.now() - started);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ── phase 2: collect (IC-023, IC-024) ────────────────────────────────────────────────────────────
|
|
720
|
+
|
|
721
|
+
/** IC-024's signal for a withheld reason. */
|
|
722
|
+
function signalFor(reason , run ) {
|
|
723
|
+
if (reason === 'not-exited') return run && run.process && run.process.kind === 'not-started' ? 'not-started' : 'not-exited';
|
|
724
|
+
if (reason === 'read-only-violation' || reason === 'read-only-unverifiable') return 'isolation-unverifiable';
|
|
725
|
+
return Object.hasOwn(SIGNALS, reason) ? reason : 'report-unparsed';
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function outputPointers(run ) {
|
|
729
|
+
const o = run && run.output;
|
|
730
|
+
if (!o) return null;
|
|
731
|
+
const at = (x ) => (x && typeof x === 'object' ? x.path ?? x.pointer ?? null : x ?? null);
|
|
732
|
+
return { stdout: at(o.stdout), stderr: at(o.stderr) };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function blockersOf(run , withheld ) {
|
|
736
|
+
const denials = run && run.report && Array.isArray(run.report.denials) ? run.report.denials.map((x ) => (x && x.action) || String(x)) : [];
|
|
737
|
+
return [...denials, ...withheld].map((b) => Array.from(String(b)).slice(0, BLOCKER_CHARS).join('')).slice(0, BLOCKERS_MAX);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** IC-011's result envelope for a subagent dispatch, written before its phase changes. */
|
|
741
|
+
function writeResult(p , d , run , { outcome, staleReason = null, failureClass = null, commit = null, usage: spent, withheld = [] } ) {
|
|
742
|
+
const value = buildResult({
|
|
743
|
+
dispatchId: d.dispatchId, task: d.task, token: d.token, epoch: d.epoch, mode: d.mode, outcome, staleReason, failureClass,
|
|
744
|
+
summary: run && run.report && run.report.summary ? String(run.report.summary) : '',
|
|
745
|
+
status: run && run.report && run.report.status ? run.report.status : 'none',
|
|
746
|
+
base: d.attempt ? d.attempt.base : null, commit: commit ? commit.commit : null, changes: commit ? commit.changes : [],
|
|
747
|
+
files: p.filesOf ? p.filesOf(d.task) : [], evidence: [], blockers: blockersOf(run, withheld),
|
|
748
|
+
usage: { tokens: spent.tokens, basis: spent.basis, source: run && run.report && run.report.usage ? 'worker' : 'reservation' },
|
|
749
|
+
workerRun: run ? { id: run.runId, output: outputPointers(run) } : null,
|
|
750
|
+
isolation: run && run.isolation ? { status: run.isolation.status, reasons: run.isolation.reason ? [run.isolation.reason] : (run.isolation.reasons || []) } : null,
|
|
751
|
+
});
|
|
752
|
+
const written = writeEnvelope({ stateRoot: p.stateRoot, dispatchId: d.dispatchId, kind: 'result', value });
|
|
753
|
+
return { value, written };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** 106 IC-004: a fallback event's StepAction detail, `<task> <from> -> <to|none> (<reason>; mode
|
|
757
|
+
* <mode>; signals <basis>)`; the human line is `fallback <kind> ` before it. */
|
|
758
|
+
const fallbackLine = (e ) => `${e.task} ${e.from} -> ${e.to ?? 'none'} (${e.reason}; mode ${e.mode}; signals ${e.basis})`;
|
|
759
|
+
|
|
760
|
+
/** 106 IC-004: one StepAction per fallback event. */
|
|
761
|
+
function actFallback(p , e , token = null) {
|
|
762
|
+
p.act('fallback', { dispatchId: e.dispatchId, task: e.task, token }, e.kind, fallbackLine(e));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** 106 IC-004, inside the write that ended quota dispatch `cur`: its harness joins `exhausted` once,
|
|
766
|
+
* and one `quota` event goes to the run and onto the dispatch. Returns the event. */
|
|
767
|
+
function recordQuota(p , s , cur , run , at ) {
|
|
768
|
+
const f = fallbackOf(s.run, p.fallbackDefault);
|
|
769
|
+
if (!Array.isArray(f.exhausted)) f.exhausted = [];
|
|
770
|
+
if (cur.harness && !f.exhausted.includes(cur.harness)) f.exhausted.push(cur.harness);
|
|
771
|
+
const quota = run && run.report && run.report.quota ? run.report.quota : null;
|
|
772
|
+
const event = appendFallbackEvent(s.run, {
|
|
773
|
+
at: iso(at), task: cur.task, dispatchId: cur.dispatchId, kind: 'quota', from: cur.harness ?? null, to: null,
|
|
774
|
+
reason: quota && typeof quota.signal === 'string' && quota.signal ? quota.signal : 'quota', mode: f.mode,
|
|
775
|
+
basis: quota && typeof quota.basis === 'string' && quota.basis ? quota.basis : 'unverified',
|
|
776
|
+
}, p.fallbackDefault);
|
|
777
|
+
// Review C-5: a stepped dispatch keeps its step event before the quota one.
|
|
778
|
+
cur.fallback = [...(Array.isArray(cur.fallback) ? cur.fallback : []), event];
|
|
779
|
+
return event;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Ends dispatch `d` by IC-024's class for `signal`, only while its phase is one of `phases` (read
|
|
783
|
+
* inside the lock: a pass-wide snapshot can be older than another driver's lease); then, outside the
|
|
784
|
+
* lock, `cancelRun` when the class asks for it, and the granter's children. A `quota` end records
|
|
785
|
+
* its fallback state in the same write (106 IC-004). */
|
|
786
|
+
async function failDispatch(p , d , phases , signal , detail , run , parts , envelope = null) {
|
|
787
|
+
const started = Date.now();
|
|
788
|
+
const { failureClass, cancelRun: needCancel } = classFor(signal);
|
|
789
|
+
const at = p.clock();
|
|
790
|
+
const ended = mutateDispatch(p.file, (s ) => {
|
|
791
|
+
const cur = s.dispatches[d.dispatchId];
|
|
792
|
+
if (!cur || !phases.includes(cur.phase)) return { write: false, result: { ended: false, reason: 'phase-not-allowed', phase: cur ? cur.phase : null } };
|
|
793
|
+
if (parts) cur.usageParts = parts.parts;
|
|
794
|
+
if (envelope) cur.envelopes = { ...(cur.envelopes || {}), result: envelope.pointer };
|
|
795
|
+
const end = endAttempt({ store: s, dispatchId: cur.dispatchId, failureClass, detail, usage: parts ? parts.usage : undefined, ctx: p.ctx() });
|
|
796
|
+
if (end.result.ended && failureClass === 'quota') end.result.fallback = recordQuota(p, s, cur, run, at);
|
|
797
|
+
bumpPeak(s);
|
|
798
|
+
return end;
|
|
799
|
+
});
|
|
800
|
+
p.act('finish', d, ended.ended ? `failed:${failureClass}` : `finish-refused:${failureClass}`, ended.ended ? detail : ended.reason, Date.now() - started);
|
|
801
|
+
if (ended.fallback) actFallback(p, ended.fallback, ended.token ?? null);
|
|
802
|
+
if (!ended.ended) {
|
|
803
|
+
// A phase refusal wrote nothing and is no 080 error (no `lastError`).
|
|
804
|
+
if (ended.reason !== 'phase-not-allowed') p.errors.add(d.dispatchId);
|
|
805
|
+
return ended;
|
|
806
|
+
}
|
|
807
|
+
if (needCancel && d.workerRunId) {
|
|
808
|
+
const live = readRuns(p.runStore)[d.workerRunId];
|
|
809
|
+
if (live && live.phase !== 'ended') {
|
|
810
|
+
const res = await cancelWorker(p, d.workerRunId, failureClass === 'timeout' ? `timeout after ${detail}` : `${failureClass}: ${detail ?? ''}`.trim(), failureClass === 'timeout' ? 'timeout' : 'user');
|
|
811
|
+
recordCleanup(p, d.dispatchId, cancelSummary(res));
|
|
812
|
+
p.act('cleanup', d, 'worker-cancelled', res.disposition);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (d.workerRunId) await cancelChildren(p, d.dispatchId, `granter-ended:${failureClass}`);
|
|
816
|
+
return ended;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Releases an attempt whose attempt commit failed and blocks the run `integration-error` (IC-017). */
|
|
820
|
+
function releaseIntegrationError(p , d , error , parts ) {
|
|
821
|
+
mutateDispatch(p.file, (s ) => {
|
|
822
|
+
const cur = s.dispatches[d.dispatchId];
|
|
823
|
+
if (!cur || cur.phase === 'ended') return { write: false, result: null };
|
|
824
|
+
const end = releaseAttempt({ store: s, dispatchId: cur.dispatchId, reason: 'integration-error', usage: parts.usage, ctx: p.ctx() });
|
|
825
|
+
setRunState(s, 'blocked');
|
|
826
|
+
s.run.integrationGap = { task: cur.task, kind: 'integration-error', cause: 'no-progress', reason: error.reason || 'attempt-commit-failed', exitCode: error.exitCode ?? null, blocks: [] };
|
|
827
|
+
return end;
|
|
828
|
+
});
|
|
829
|
+
p.act('release', d, 'integration-error', error.reason || error.message);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async function collect(p ) {
|
|
833
|
+
const current = readDispatch(p.file);
|
|
834
|
+
const ledger = readLedger(p);
|
|
835
|
+
for (const d of Object.values(current.dispatches) ) {
|
|
836
|
+
// A main-session submit of `failure` fails at its next collection (IC-005).
|
|
837
|
+
if (d.phase === 'collected' && d.outcome === 'failed' && d.report && d.report.performer === 'main') {
|
|
838
|
+
await failDispatch(p, d, ['collected'], 'submit-failure', 'submit --status failure', null, { usage: usageFor(d, null, []), parts: d.usageParts || { main: usageFor(d, null, []) } });
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (d.phase !== 'launched' || d.mode !== 'subagent' || !d.workerRunId) continue;
|
|
842
|
+
const started = Date.now();
|
|
843
|
+
// Read per dispatch, after the earlier ones' git and cancels, and settled against the probe:
|
|
844
|
+
// a pass-wide snapshot read before a supervisor wrote its end and exited reads as lost.
|
|
845
|
+
const { run, view } = settledStatus(p.runStore, d.workerRunId, ledger, { platform: p.platform, nowMs: p.clock().getTime() });
|
|
846
|
+
if (view && LIVE_WORKER_STATES.includes(view.state)) continue;
|
|
847
|
+
if (!view || view.state === 'unknown') {
|
|
848
|
+
const children = childRunsOf(p, current, d.dispatchId);
|
|
849
|
+
await failDispatch(p, d, ['launched'], 'status-unknown', view ? view.reason : 'run-missing', run, usageParts(d, run, children));
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
const stale = integrate.staleReason(d, ledger, d.parentRunKey ?? current.run.runKey ?? null);
|
|
853
|
+
if (stale) {
|
|
854
|
+
writeResult(p, d, run, { outcome: 'stale', staleReason: stale, usage: usageFor(d, run, []) });
|
|
855
|
+
await endStale(p, d, stale, run);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
await cancelChildren(p, d.dispatchId, 'granter-collected');
|
|
859
|
+
const children = childRunsOf(p, readDispatch(p.file), d.dispatchId);
|
|
860
|
+
const parts = usageParts(d, run, children);
|
|
861
|
+
const { resultOffered, withheld } = resultOfRun(run);
|
|
862
|
+
if (!resultOffered) {
|
|
863
|
+
const signal = signalFor(withheld[0], run);
|
|
864
|
+
const { failureClass } = classFor(signal);
|
|
865
|
+
const { written } = writeResult(p, d, run, { outcome: 'failed', failureClass, usage: parts.usage, withheld });
|
|
866
|
+
await failDispatch(p, d, ['launched'], signal, withheld.join(', '), run, parts, written);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
let commit ;
|
|
870
|
+
try {
|
|
871
|
+
commit = integrate.attemptCommit({ worktree: d.attempt.worktree, base: d.attempt.base, dispatchId: d.dispatchId, task: d.task, token: d.token, env: p.env });
|
|
872
|
+
} catch (error ) {
|
|
873
|
+
if (!(error instanceof integrate.IntegrationError)) throw error;
|
|
874
|
+
releaseIntegrationError(p, d, error, parts);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const { value, written } = writeResult(p, d, run, { outcome: 'offered', commit, usage: parts.usage });
|
|
878
|
+
const moved = moveDispatch(p, d.dispatchId, ['launched'], (x ) => {
|
|
879
|
+
Object.assign(x, {
|
|
880
|
+
phase: 'collected', outcome: 'offered', collectedAt: iso(p.clock()), commit: commit.commit, changes: commit.changes,
|
|
881
|
+
undeclared: value.undeclared, usage: parts.usage, usageParts: parts.parts,
|
|
882
|
+
report: { status: run.report ? run.report.status : 'none', performer: 'worker' },
|
|
883
|
+
envelopes: { ...(x.envelopes || {}), result: written.pointer },
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
p.act('collect', d, moved.applied ? 'offered' : `moved:${moved.phase}`, commit.commit ? `${commit.changes.length} change(s)` : 'empty', Date.now() - started);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ── phase 3: integrate one (IC-017 to IC-020) ────────────────────────────────────────────────────
|
|
891
|
+
|
|
892
|
+
async function integrateOne(p ) {
|
|
893
|
+
const started = Date.now();
|
|
894
|
+
const r = await integrate.integrateNext({
|
|
895
|
+
root: p.root, slug: p.slug, ctx: p.ctx(), env: p.env, holder: { pid: p.driver.pid, start: p.driver.start ?? null },
|
|
896
|
+
now: p.clock, storeFile: p.file, verify: p.verify,
|
|
897
|
+
});
|
|
898
|
+
const ms = Date.now() - started;
|
|
899
|
+
if (!r || ['none', 'not-running'].includes(r.outcome)) return r;
|
|
900
|
+
if (r.outcome === 'lease-held') {
|
|
901
|
+
p.act('integrate', { dispatchId: r.dispatchId }, 'lease-held', null, ms);
|
|
902
|
+
return r;
|
|
903
|
+
}
|
|
904
|
+
const d = { dispatchId: r.dispatchId, task: r.task, token: r.token };
|
|
905
|
+
if (r.outcome !== 'stale') p.integrationMs.push(ms);
|
|
906
|
+
p.act('integrate', d, r.outcome, r.reason ?? r.why ?? null, ms);
|
|
907
|
+
if (r.verification) p.act('verify', d, r.verification.exitCode === 0 && !r.verification.timedOut ? 'passed' : 'failed', `evidence ${r.verification.evidenceId}`, r.verification.durationMs);
|
|
908
|
+
if (r.undo && r.undo.ran) p.act('undo', d, r.undo.ok ? 'undone' : 'undo-failed', r.undo.reason);
|
|
909
|
+
if (r.end) p.act(r.end.ended ? (r.end.disposition === 'completed' || r.end.disposition === 'failed' ? 'finish' : 'release') : 'error', d, r.end.disposition || r.end.outcome || 'ended', r.end.reason ?? null);
|
|
910
|
+
if (r.outcome === 'lastError' || (r.end && r.end.ended === false)) p.errors.add(r.dispatchId);
|
|
911
|
+
if (r.removal) p.act('cleanup', d, r.removal.removed ? 'removed' : 'kept', r.removal.reason);
|
|
912
|
+
return r;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ── phase 4: time out (IC-025) ───────────────────────────────────────────────────────────────────
|
|
916
|
+
|
|
917
|
+
async function timeouts(p ) {
|
|
918
|
+
const current = readDispatch(p.file);
|
|
919
|
+
const nowMs = p.clock().getTime();
|
|
920
|
+
const seconds = Number.isInteger(current.run.attemptSeconds) ? current.run.attemptSeconds : ATTEMPT_SECONDS_DEFAULT;
|
|
921
|
+
const runs = readRuns(p.runStore);
|
|
922
|
+
for (const d of Object.values(current.dispatches) ) {
|
|
923
|
+
if (d.phase !== 'launched' || d.mode !== 'subagent' || !d.deadline || Date.parse(d.deadline) > nowMs) continue;
|
|
924
|
+
const run = runs[d.workerRunId] ?? null;
|
|
925
|
+
const parts = usageParts(d, run, childRunsOf(p, current, d.dispatchId));
|
|
926
|
+
p.act('timeout', d, 'deadline', `${seconds}s`);
|
|
927
|
+
await failDispatch(p, d, ['launched'], 'deadline', `${seconds}s`, run, parts);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── phase 5: launch children (IC-033 launch) ─────────────────────────────────────────────────────
|
|
932
|
+
|
|
933
|
+
async function launchChildren(p ) {
|
|
934
|
+
const current = readDispatch(p.file);
|
|
935
|
+
const requested = (Object.values(current.spawnRequests) ).filter((r) => r.state === 'requested');
|
|
936
|
+
if (!requested.length) return;
|
|
937
|
+
if (control(readLedger(p), current.run.runKey) !== 'running' || !['running', 'blocked'].includes(current.run.state)) return;
|
|
938
|
+
const refuse = (requestId , reason ) => mutateDispatch(p.file, (s ) => {
|
|
939
|
+
const r = s.spawnRequests[requestId];
|
|
940
|
+
if (!r || r.state !== 'requested') return { write: false, result: false };
|
|
941
|
+
r.state = 'refused';
|
|
942
|
+
r.reason = reason;
|
|
943
|
+
// A refused request spent nothing: its reserved tokens go back to the granter's grant (RT-16).
|
|
944
|
+
const g = s.dispatches[r.granterDispatchId];
|
|
945
|
+
if (g && g.phase !== 'ended' && g.grant && g.grant.used && Number.isInteger(r.reserveTokens)) {
|
|
946
|
+
g.grant = { ...g.grant, used: { ...g.grant.used, tokens: Math.max(0, g.grant.used.tokens - r.reserveTokens) } };
|
|
947
|
+
}
|
|
948
|
+
return { write: true, result: true };
|
|
949
|
+
});
|
|
950
|
+
for (const req of requested) {
|
|
951
|
+
const started = Date.now();
|
|
952
|
+
const granter = current.dispatches[req.granterDispatchId];
|
|
953
|
+
const who = { dispatchId: req.granterDispatchId, task: granter ? granter.task : null, token: granter ? granter.token : null };
|
|
954
|
+
if (!granter || granter.phase !== 'launched' || !granter.workerRunId) {
|
|
955
|
+
refuse(req.requestId, 'granter-not-running');
|
|
956
|
+
p.act('launch-child', who, 'refused', `${req.requestId} granter-not-running`, Date.now() - started);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (current.run?.billingCap !== undefined && current.run?.billingCap !== null && current.run?.billingCap !== 'free') {
|
|
960
|
+
refuse(req.requestId, 'billing-cap-invalid');
|
|
961
|
+
p.act('launch-child', who, 'refused', `${req.requestId} billing-cap-invalid`, Date.now() - started);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (current.run?.billingCap === 'free') {
|
|
965
|
+
const reg = p.reg();
|
|
966
|
+
const capped = require('../settings/worker-billing.js');
|
|
967
|
+
const record = capped.readWorkerBilling(p.env, { workerIds: reg.harnesses.map((h ) => h.id) });
|
|
968
|
+
const desc = readiness.workerDescriptors({ descriptors: p.descriptors, registry: reg }).find((e ) => e.harness === granter.harness);
|
|
969
|
+
const effective = capped.effectiveBilling(desc?.worker?.billing, granter.harness, record);
|
|
970
|
+
if (effective.value !== 'free') {
|
|
971
|
+
refuse(req.requestId, `billing:${effective.value}`);
|
|
972
|
+
p.act('launch-child', who, 'refused', `${req.requestId} billing:${effective.value}`, Date.now() - started);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const n = Number(REQUEST_ID.exec(req.requestId)?.[2]);
|
|
977
|
+
let wt ;
|
|
978
|
+
try {
|
|
979
|
+
wt = integrate.createChildWorktree({ root: p.root, slug: p.slug, dispatchId: granter.dispatchId, n, base: granter.attempt.base, env: p.env });
|
|
980
|
+
} catch (error ) {
|
|
981
|
+
refuse(req.requestId, `worktree-failed:${error && (error.status ?? error.code) ? String(error.status ?? error.code) : 'error'}`);
|
|
982
|
+
p.act('launch-child', who, 'refused', `${req.requestId} worktree-failed`, Date.now() - started);
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const remaining = granter.deadline ? Math.max(1, Math.floor((Date.parse(granter.deadline) - p.clock().getTime()) / 1000)) : 1;
|
|
986
|
+
let outcome ;
|
|
987
|
+
try {
|
|
988
|
+
outcome = await executeWorkerCommand({
|
|
989
|
+
action: 'run', slug: p.slug, harness: granter.harness, task: granter.task, token: granter.token,
|
|
990
|
+
promptFile: path.join(p.stateRoot, req.prompt), timeoutSeconds: remaining,
|
|
991
|
+
childOf: granter.workerRunId, request: req.requestId, execWorktree: wt.worktree,
|
|
992
|
+
env: p.env, cwd: p.cwd, platform: p.platform, now: p.clock, descriptors: p.descriptors, registry: p.reg(), coverage: p.coverage,
|
|
993
|
+
});
|
|
994
|
+
} catch (error ) {
|
|
995
|
+
outcome = { exit: 2, body: { error: error && error.code ? error.code : 'launch-threw' } };
|
|
996
|
+
}
|
|
997
|
+
const body = outcome.body || {};
|
|
998
|
+
if (outcome.exit === 0 && body.disposition === 'started') {
|
|
999
|
+
const record = readRuns(p.runStore)[body.runId];
|
|
1000
|
+
const recorded = mutateDispatch(p.file, (s ) => {
|
|
1001
|
+
const r = s.spawnRequests[req.requestId];
|
|
1002
|
+
if (!r || r.state !== 'requested') return { write: false, result: false };
|
|
1003
|
+
// Read inside the lock: a cancel or a lost driver since the pass's snapshot, or a granter that
|
|
1004
|
+
// ended, leaves the request and the run just started is cancelled below (IC-030, IC-033).
|
|
1005
|
+
const g = s.dispatches[req.granterDispatchId];
|
|
1006
|
+
if (!s.run || HELD_RUN_STATES.includes(s.run.state) || !g || g.phase !== 'launched') return { write: false, result: false };
|
|
1007
|
+
Object.assign(r, { state: 'launched', childRunId: body.runId, identity: record ? record.identity ?? null : null, launchedAt: iso(p.clock()), worktree: wt.worktree });
|
|
1008
|
+
bumpPeak(s);
|
|
1009
|
+
return { write: true, result: true };
|
|
1010
|
+
});
|
|
1011
|
+
if (!recorded) {
|
|
1012
|
+
const res = await cancelWorker(p, body.runId, 'request-moved');
|
|
1013
|
+
if (res && res.cleanup && res.cleanup.status === 'verified') {
|
|
1014
|
+
// The worktree is recorded nowhere, so no removal loop would reach it (IC-021).
|
|
1015
|
+
integrate.managerFor(p.root, p.slug, 'attempt', p.env).removeVerified(req.requestId);
|
|
1016
|
+
} else {
|
|
1017
|
+
// RR-5 / DEC-047: a child tree not verified ended may still write there. Keep the worktree
|
|
1018
|
+
// and name it and its run on the request (add-only), so IC-030 step 8 and the deferred
|
|
1019
|
+
// removals read its cleanup from the run and keep and list it.
|
|
1020
|
+
mutateDispatch(p.file, (s ) => {
|
|
1021
|
+
const r = s.spawnRequests[req.requestId];
|
|
1022
|
+
if (!r || (r.worktree != null && r.childRunId != null)) return { write: false, result: null };
|
|
1023
|
+
if (r.worktree == null) r.worktree = wt.worktree;
|
|
1024
|
+
if (r.childRunId == null) r.childRunId = body.runId;
|
|
1025
|
+
return { write: true, result: null };
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
p.act('launch-child', who, recorded ? 'launched' : 'request-moved', `${req.requestId} ${body.runId}`, Date.now() - started);
|
|
1030
|
+
} else {
|
|
1031
|
+
const reason = body.reason || body.error || 'refused';
|
|
1032
|
+
refuse(req.requestId, reason);
|
|
1033
|
+
integrate.managerFor(p.root, p.slug, 'attempt', p.env).removeVerified(req.requestId);
|
|
1034
|
+
p.act('launch-child', who, 'refused', `${req.requestId} ${reason}`, Date.now() - started);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ── phase 6: admit and launch (IC-007 launch, IC-010, IC-014, IC-022) ───────────────────────────
|
|
1040
|
+
|
|
1041
|
+
function hashFile(file ) {
|
|
1042
|
+
try { return sha256(fs.readFileSync(file)); } catch { return null; }
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** IC-010's deltas: what earlier integrations changed between the run base and the attempt base,
|
|
1046
|
+
* each change attributed to the dispatch whose integration introduced it. */
|
|
1047
|
+
function deltasFor(p , s , d ) {
|
|
1048
|
+
const runBase = s.run.integration.base;
|
|
1049
|
+
const attemptBase = d.attempt.base;
|
|
1050
|
+
let changes = [];
|
|
1051
|
+
if (runBase && attemptBase && runBase !== attemptBase && s.run.integration.worktree) {
|
|
1052
|
+
try { changes = integrate.diffNameStatus(runBase, attemptBase, { cwd: s.run.integration.worktree, env: p.env }); } catch { changes = []; }
|
|
1053
|
+
}
|
|
1054
|
+
const by = new Map ();
|
|
1055
|
+
const done = (Object.values(s.dispatches) ).filter((x) => x.disposition === 'completed' && Array.isArray(x.changes))
|
|
1056
|
+
.sort((a, b) => String(a.endedAt).localeCompare(String(b.endedAt)));
|
|
1057
|
+
for (const x of done) for (const c of x.changes) by.set(c.path, x.dispatchId);
|
|
1058
|
+
return { runBase, attemptBase, changes: changes.map((c) => ({ path: c.path, change: c.change, dispatchId: by.get(c.path) ?? null })) };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function workerOf(p , harness ) {
|
|
1062
|
+
if (!harness) return null;
|
|
1063
|
+
if (p.descriptors && Object.hasOwn(p.descriptors, harness)) return p.descriptors[harness];
|
|
1064
|
+
const entry = workerCatalogue({ registry: p.reg() }).find((item ) => item.id === harness);
|
|
1065
|
+
return entry ? entry.worker : null;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** IC-010 handoff for a claimed dispatch. */
|
|
1069
|
+
function handoffFor(p , s , d , planTask , record ) {
|
|
1070
|
+
const files = (planTask.files || []).map(String);
|
|
1071
|
+
const sources = [
|
|
1072
|
+
...files.map((f) => ({ path: normalizeFile(f), sha256: hashFile(path.join(d.attempt.worktree, normalizeFile(f))) })),
|
|
1073
|
+
...CHAIN_ARTIFACTS.map((rel) => {
|
|
1074
|
+
const repoRel = `agent-docs/doflow/${p.slug}/${rel}`;
|
|
1075
|
+
const found = p.roots.map((r ) => hashFile(path.join(r, repoRel))).find((h ) => h !== null) ?? null;
|
|
1076
|
+
return { path: repoRel, sha256: found };
|
|
1077
|
+
}),
|
|
1078
|
+
];
|
|
1079
|
+
const subagent = d.mode === 'subagent';
|
|
1080
|
+
const worker = subagent ? workerOf(p, d.harness) : null;
|
|
1081
|
+
const grant = d.grant && d.grant.depth === 1 && d.grant.children > 0 ? d.grant : null;
|
|
1082
|
+
return buildHandoff({
|
|
1083
|
+
dispatchId: d.dispatchId, dispatchRunId: s.run.dispatchRunId, slug: p.slug, mode: d.mode,
|
|
1084
|
+
task: { id: planTask.id, phase: planTask.phase, owner: planTask.owner ?? null, text: planTask.description ?? '', files, dependsOn: planTask.taskDeps || [] },
|
|
1085
|
+
acceptance: planTask.acceptance || [],
|
|
1086
|
+
sources,
|
|
1087
|
+
deltas: deltasFor(p, s, d),
|
|
1088
|
+
budget: {
|
|
1089
|
+
reserveTokens: Number.isInteger(s.run.reserveTokens) ? s.run.reserveTokens : null,
|
|
1090
|
+
attemptSeconds: subagent ? (Number.isInteger(s.run.attemptSeconds) ? s.run.attemptSeconds : ATTEMPT_SECONDS_DEFAULT) : null,
|
|
1091
|
+
attempts: { used: record ? record.budget.used : 0, limit: record ? record.budget.limit : 0 },
|
|
1092
|
+
},
|
|
1093
|
+
grant: d.grant,
|
|
1094
|
+
worktree: { path: d.attempt.worktree, branch: d.attempt.branch, base: d.attempt.base },
|
|
1095
|
+
report: {
|
|
1096
|
+
parser: worker && worker.structuredOutput && worker.structuredOutput.parser ? worker.structuredOutput.parser : null,
|
|
1097
|
+
spawn: grant ? ['dispatch', '--action', 'spawn', '--slug', p.slug, '--prompt-file', '<path>', '--reserve-tokens', '<n>'] : null,
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** IC-005's perform yield for a launched main-session dispatch. */
|
|
1103
|
+
function performYield(p , d ) {
|
|
1104
|
+
const handoff = path.join(p.stateRoot, d.envelopes.handoff);
|
|
1105
|
+
let bytes = Buffer.alloc(0);
|
|
1106
|
+
try { bytes = fs.readFileSync(handoff); } catch { /* reported by size 0 and a null digest */ }
|
|
1107
|
+
return {
|
|
1108
|
+
dispatchId: d.dispatchId, task: d.task, token: d.token, mode: d.mode,
|
|
1109
|
+
worktree: d.attempt.worktree, branch: d.attempt.branch, base: d.attempt.base,
|
|
1110
|
+
handoff: { path: handoff, bytes: bytes.length, sha256: bytes.length ? sha256(bytes) : null },
|
|
1111
|
+
submit: ['dispatch', '--action', 'submit', '--slug', p.slug, '--dispatch', d.dispatchId, '--status', '<success|failure>', '--summary', '<text>'],
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/** Launches one claimed dispatch outside every lock: the handoff, then a worker run or a perform. */
|
|
1116
|
+
async function launch(p , admitted , plan , view ) {
|
|
1117
|
+
const started = Date.now();
|
|
1118
|
+
const s = readDispatch(p.file);
|
|
1119
|
+
const d = s.dispatches[admitted.dispatchId];
|
|
1120
|
+
if (!d || d.phase !== 'claimed') return;
|
|
1121
|
+
if (s.run?.billingCap !== undefined && s.run?.billingCap !== null && s.run?.billingCap !== 'free') {
|
|
1122
|
+
p.act('launch', d, 'refused', 'billing-cap-invalid', Date.now() - started);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
let binding = null;
|
|
1126
|
+
if (!MAIN_SESSION_MODES.includes(d.mode)) {
|
|
1127
|
+
const closed = gateOf(p.activeNow(), d.harness);
|
|
1128
|
+
if (closed) {
|
|
1129
|
+
p.act('launch', d, 'refused', closed.reason, 0);
|
|
1130
|
+
// IC-009 / IC-010: a gate denial narrows selection; it is not a launch failure.
|
|
1131
|
+
await failDispatch(p, d, ['claimed'], 'report-denied', closed.reason, null, null);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
// IC-012: settings may have changed since selection; refuse before writing the handoff.
|
|
1135
|
+
const reg = p.reg();
|
|
1136
|
+
const tier = resolveTier({
|
|
1137
|
+
dispatchTiers: reg.dispatchTiers,
|
|
1138
|
+
owner: p.ownerOf ? p.ownerOf(d.task) : null,
|
|
1139
|
+
tier: p.tierOf ? p.tierOf(d.task) : null,
|
|
1140
|
+
missing: [],
|
|
1141
|
+
}).tier;
|
|
1142
|
+
const descriptor = readiness.workerDescriptors({ descriptors: p.descriptors, registry: reg })
|
|
1143
|
+
.find((entry ) => entry.harness === d.harness)?.worker;
|
|
1144
|
+
const resolved = resolveBinding({ worker: d.harness, tier, bindings: p.bindingsNow(), descriptor });
|
|
1145
|
+
const gap = bindingGap(resolved);
|
|
1146
|
+
if (gap) {
|
|
1147
|
+
p.act('launch', d, 'refused', gap.reason, 0);
|
|
1148
|
+
await failDispatch(p, d, ['claimed'], 'report-denied', gap.reason, null, null);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (resolved.flag === 'supported') binding = { model: resolved.model, flag: 'supported' };
|
|
1152
|
+
}
|
|
1153
|
+
const planTask = plan.graph.tasks.find((t ) => t.id === d.task);
|
|
1154
|
+
const record = view.tasks.find((r ) => r.id === d.task);
|
|
1155
|
+
const value = handoffFor(p, s, d, planTask, record);
|
|
1156
|
+
const handoff = writeEnvelope({ stateRoot: p.stateRoot, dispatchId: d.dispatchId, kind: 'handoff', value });
|
|
1157
|
+
const seconds = Number.isInteger(s.run.attemptSeconds) ? s.run.attemptSeconds : ATTEMPT_SECONDS_DEFAULT;
|
|
1158
|
+
|
|
1159
|
+
if (MAIN_SESSION_MODES.includes(d.mode)) {
|
|
1160
|
+
const moved = moveDispatch(p, d.dispatchId, ['claimed'], (x ) => {
|
|
1161
|
+
Object.assign(x, { phase: 'launched', launchedAt: iso(p.clock()), deadline: null, workerIdentity: null, envelopes: { handoff: handoff.pointer, result: null } });
|
|
1162
|
+
}, { open: true });
|
|
1163
|
+
p.act('launch', d, moved.applied ? `perform:${d.mode}` : `moved:${moved.phase}`, null, Date.now() - started);
|
|
1164
|
+
if (moved.applied && !p.perform) p.perform = performYield(p, moved.dispatch);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
let outcome ;
|
|
1169
|
+
try {
|
|
1170
|
+
outcome = await executeWorkerCommand({
|
|
1171
|
+
action: 'run', edit: true, slug: p.slug, harness: d.harness, task: d.task, token: d.token,
|
|
1172
|
+
binding,
|
|
1173
|
+
promptFile: handoff.path, timeoutSeconds: seconds + BACKSTOP_SECONDS,
|
|
1174
|
+
env: p.env, cwd: p.cwd, platform: p.platform, now: p.clock, descriptors: p.descriptors, registry: p.reg(), coverage: p.coverage,
|
|
1175
|
+
});
|
|
1176
|
+
} catch (error ) {
|
|
1177
|
+
outcome = { exit: 2, body: { error: error && error.code ? error.code : 'launch-threw' } };
|
|
1178
|
+
}
|
|
1179
|
+
const body = outcome.body || {};
|
|
1180
|
+
if (outcome.exit === 0 && body.disposition === 'started') {
|
|
1181
|
+
const run = readRuns(p.runStore)[body.runId];
|
|
1182
|
+
const launchedAt = p.clock();
|
|
1183
|
+
const moved = moveDispatch(p, d.dispatchId, ['claimed'], (x ) => {
|
|
1184
|
+
Object.assign(x, {
|
|
1185
|
+
phase: 'launched', workerRunId: body.runId, launchedAt: iso(launchedAt),
|
|
1186
|
+
deadline: iso(new Date(launchedAt.getTime() + seconds * 1000)),
|
|
1187
|
+
workerIdentity: run && run.identity ? { ...run.identity } : null,
|
|
1188
|
+
envelopes: { handoff: handoff.pointer, result: null },
|
|
1189
|
+
});
|
|
1190
|
+
}, { open: true });
|
|
1191
|
+
if (!moved.applied) {
|
|
1192
|
+
// A cancel, take over or resume moved the dispatch or marked the run first: end the run just
|
|
1193
|
+
// started (IC-007, IC-030).
|
|
1194
|
+
const res = await cancelWorker(p, body.runId, 'dispatch-moved');
|
|
1195
|
+
recordCleanup(p, d.dispatchId, cancelSummary(res));
|
|
1196
|
+
}
|
|
1197
|
+
p.act('launch', d, moved.applied ? 'subagent' : `moved:${moved.phase}`, `${d.harness} ${body.runId}`, Date.now() - started);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const reason = body.reason || body.error || 'refused';
|
|
1201
|
+
mutateDispatch(p.file, (st ) => {
|
|
1202
|
+
const cur = st.dispatches[d.dispatchId];
|
|
1203
|
+
if (cur && cur.phase === 'claimed') cur.envelopes = { handoff: handoff.pointer, result: null };
|
|
1204
|
+
return { write: true, result: null };
|
|
1205
|
+
});
|
|
1206
|
+
p.act('launch', d, 'refused', reason, Date.now() - started);
|
|
1207
|
+
await failDispatch(p, d, ['claimed'], 'run-refused', reason, null, null);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* 106 IC-004 (D8): the signal basis a hold or step from harness `from` prints: its descriptor's
|
|
1212
|
+
* `structuredOutput.quotaSignals.basis`, else the basis of the run's last quota event for it, else
|
|
1213
|
+
* `unverified`. Read outside every lock.
|
|
1214
|
+
*/
|
|
1215
|
+
function basisFor(p , f , from ) {
|
|
1216
|
+
const worker = workerOf(p, from);
|
|
1217
|
+
const declared = worker && worker.structuredOutput && worker.structuredOutput.quotaSignals ? worker.structuredOutput.quotaSignals.basis : null;
|
|
1218
|
+
if (typeof declared === 'string' && declared) return declared;
|
|
1219
|
+
const last = (Array.isArray(f.events) ? f.events : []).filter((e ) => e && e.kind === 'quota' && e.from === from).at(-1);
|
|
1220
|
+
return last && typeof last.basis === 'string' && last.basis ? last.basis : 'unverified';
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* 106 IC-004 (D6, D7): after admission, one locked write adds the pass's new holds (add-only: a task
|
|
1225
|
+
* already held keeps its hold) with one `hold` event each, and one `step` event per admitted dispatch
|
|
1226
|
+
* whose selection stepped, copied onto it (reason `accepted` for an accepted proposal, which the same
|
|
1227
|
+
* write removes from `fallback.accepted`); then one StepAction per event.
|
|
1228
|
+
*/
|
|
1229
|
+
function recordFallback(p , holds , admitted , bases ) {
|
|
1230
|
+
const stepped = admitted.map((a ) => a.dispatchId);
|
|
1231
|
+
if (!holds.size && !stepped.length) return;
|
|
1232
|
+
const events = mutateDispatch(p.file, (s ) => {
|
|
1233
|
+
if (!s.run) return { write: false, result: [] };
|
|
1234
|
+
const at = iso(p.clock());
|
|
1235
|
+
const out = [];
|
|
1236
|
+
const f = fallbackOf(s.run, p.fallbackDefault);
|
|
1237
|
+
if (!f.holds || typeof f.holds !== 'object') f.holds = {};
|
|
1238
|
+
for (const [task, h] of holds) {
|
|
1239
|
+
if (Object.hasOwn(f.holds, task)) continue;
|
|
1240
|
+
f.holds[task] = { reason: h.reason, from: h.from, proposed: h.proposed, at };
|
|
1241
|
+
// Review R-2: a new hold supersedes the task's acceptance (it named another rung).
|
|
1242
|
+
if (f.accepted && typeof f.accepted === 'object') delete f.accepted[task];
|
|
1243
|
+
out.push({ event: appendFallbackEvent(s.run, { at, task, dispatchId: null, kind: 'hold', from: h.from, to: h.proposed, reason: h.reason, mode: f.mode, basis: bases.get(h.from) ?? 'unverified' }, p.fallbackDefault), token: null });
|
|
1244
|
+
}
|
|
1245
|
+
for (const id of stepped) {
|
|
1246
|
+
const d = s.dispatches[id];
|
|
1247
|
+
const step = d && d.selection ? d.selection.fallback : null;
|
|
1248
|
+
if (!d || d.phase === 'ended' || d.fallback || !step || step.kind !== 'step') continue;
|
|
1249
|
+
const event = appendFallbackEvent(s.run, { at, task: d.task, dispatchId: d.dispatchId, kind: 'step', from: step.from, to: step.to, reason: step.accepted ? 'accepted' : 'quota', mode: f.mode, basis: bases.get(step.from) ?? 'unverified' }, p.fallbackDefault);
|
|
1250
|
+
// Review C-14: an accepted proposal is one step; it is spent in the write that records it.
|
|
1251
|
+
if (step.accepted && f.accepted && typeof f.accepted === 'object') delete f.accepted[d.task];
|
|
1252
|
+
d.fallback = [event];
|
|
1253
|
+
out.push({ event, token: Number.isInteger(d.token) ? d.token : null });
|
|
1254
|
+
}
|
|
1255
|
+
return { write: out.length > 0, result: out };
|
|
1256
|
+
});
|
|
1257
|
+
for (const { event, token } of events) actFallback(p, event, token);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
async function admitAndLaunch(p ) {
|
|
1261
|
+
const current = readDispatch(p.file);
|
|
1262
|
+
const run = current.run;
|
|
1263
|
+
// 106 IC-004: the run's fallback state as read now (D3 for a run started before 106); new holds
|
|
1264
|
+
// are collected here and written after admission, so `modeFor` stays outside every lock.
|
|
1265
|
+
const f = run.fallback && typeof run.fallback === 'object' ? run.fallback : fallbackOf({}, p.fallbackDefault);
|
|
1266
|
+
const heldBefore = f.holds && typeof f.holds === 'object' ? f.holds : {};
|
|
1267
|
+
const newHolds = new Map ();
|
|
1268
|
+
const active = p.activeNow();
|
|
1269
|
+
const bindings = p.bindingsNow();
|
|
1270
|
+
let primary = null;
|
|
1271
|
+
let primaryResolved = false;
|
|
1272
|
+
// IC-013 / IC-010: reuse this pass's bindings and resolve primary only for sequential choices.
|
|
1273
|
+
const modelFor = (task , { mode, harness } ) => {
|
|
1274
|
+
const tier = resolveTier({
|
|
1275
|
+
dispatchTiers: p.reg().dispatchTiers,
|
|
1276
|
+
owner: p.ownerOf ? p.ownerOf(task) : null,
|
|
1277
|
+
tier: p.tierOf ? p.tierOf(task) : null,
|
|
1278
|
+
missing: [],
|
|
1279
|
+
}).tier;
|
|
1280
|
+
if (mode === 'sequential' && !primaryResolved) {
|
|
1281
|
+
primaryResolved = true;
|
|
1282
|
+
try {
|
|
1283
|
+
const { resolvePrimary } = require('../context/primary.js');
|
|
1284
|
+
primary = resolvePrimary({ env: p.env, projectRoot: p.cwd, registry: p.reg() });
|
|
1285
|
+
} catch {
|
|
1286
|
+
primary = { harness: null, model: null, state: 'unknown' };
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return dispatchModelFor({ mode, harness, tier, bindings, primary });
|
|
1290
|
+
};
|
|
1291
|
+
const modeFor = (task , { store: s } ) => {
|
|
1292
|
+
if (Object.hasOwn(heldBefore, task)) return { hold: heldBefore[task].reason };
|
|
1293
|
+
const choice = selectMode({
|
|
1294
|
+
policy: run.mode || 'auto', held: Boolean(run.held && Object.hasOwn(run.held, task)), lastFailureClass: lastFailure(s, task),
|
|
1295
|
+
billingCap: run.billingCap,
|
|
1296
|
+
harness: run.harness || null, needs: Array.isArray(run.needs) ? run.needs : [], descriptors: p.descriptors, registry: p.reg(),
|
|
1297
|
+
coverage: p.coverage, env: p.env, cwd: p.cwd, root: p.root, now: () => p.clock().getTime(), platform: p.platform,
|
|
1298
|
+
evaluateImpl: p.evaluateImpl, cache: p.cache, fallback: f, owner: p.ownerOf ? p.ownerOf(task) : null, tier: p.tierOf ? p.tierOf(task) : null,
|
|
1299
|
+
task, last: lastWorkerDispatch(s, task), active, bindings,
|
|
1300
|
+
});
|
|
1301
|
+
if (!choice.hold) return choice;
|
|
1302
|
+
if (choice.hold.transient) {
|
|
1303
|
+
const reason = choice.hold.reason;
|
|
1304
|
+
p.gaps.push({
|
|
1305
|
+
task, kind: reason, cause: 'input',
|
|
1306
|
+
reason: reason === 'workers-enabled-invalid'
|
|
1307
|
+
? 'workers-enabled-invalid: workers.enabled is invalid, do not run in-session'
|
|
1308
|
+
: 'no-enabled-worker: primary not enabled, do not run in-session',
|
|
1309
|
+
});
|
|
1310
|
+
return { hold: reason };
|
|
1311
|
+
}
|
|
1312
|
+
newHolds.set(task, choice.hold);
|
|
1313
|
+
return { hold: choice.hold.reason };
|
|
1314
|
+
};
|
|
1315
|
+
const result = admit({
|
|
1316
|
+
root: p.root, slug: p.slug, ctx: p.ctx(), driver: p.driver, by: p.by, modeFor, modelFor, env: p.env, now: p.clock, storeFile: p.file,
|
|
1317
|
+
});
|
|
1318
|
+
p.actions.push(...result.actions);
|
|
1319
|
+
const bases = new Map ((Array.isArray(f.exhausted) ? f.exhausted : []).map((h ) => [h, basisFor(p, f, h)]));
|
|
1320
|
+
// A step needs an exhausted harness, so with none no admitted dispatch is read for one.
|
|
1321
|
+
recordFallback(p, newHolds, bases.size ? result.admitted || [] : [], bases);
|
|
1322
|
+
for (const gap of result.gaps || []) p.gaps.push({ task: gap.task, kind: gap.reason, cause: 'budget', reason: gap.reason });
|
|
1323
|
+
for (const r of result.refused || []) {
|
|
1324
|
+
if (!BUDGET_REFUSALS.includes(r.reason)) p.gaps.push({ task: r.task, kind: 'claim-refused', cause: 'no-progress', reason: r.reason });
|
|
1325
|
+
}
|
|
1326
|
+
if (!result.admitted.length) return result;
|
|
1327
|
+
const plan = attempts.loadPlan({ roots: p.roots, slug: p.slug, fsImpl: fs });
|
|
1328
|
+
const view = attempts.inspect({ file: p.ledgerFile, fsImpl: fs, roots: p.roots, slug: p.slug, env: p.env, cwd: p.cwd });
|
|
1329
|
+
for (const a of result.admitted) {
|
|
1330
|
+
if (p.shouldStop()) break;
|
|
1331
|
+
await launch(p, a, plan, view);
|
|
1332
|
+
}
|
|
1333
|
+
return result;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// ── phase 7: decide (IC-026, IC-027) ─────────────────────────────────────────────────────────────
|
|
1337
|
+
|
|
1338
|
+
function heldIds(record ) {
|
|
1339
|
+
return [...(record.held || []).map((h ) => (h && typeof h === 'object' ? h.id : h)), ...(record.unmet || [])].filter(Boolean);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** IC-027's gaps from 080's task records, the store and this pass's admission. */
|
|
1343
|
+
function gapsFor(p , s , view ) {
|
|
1344
|
+
const gaps = [];
|
|
1345
|
+
const blocksOf = (id ) => view.tasks.filter((r ) => heldIds(r).includes(id)).map((r ) => r.id);
|
|
1346
|
+
const have = new Set ();
|
|
1347
|
+
const push = (g ) => {
|
|
1348
|
+
const key = `${g.task}\u0000${g.kind}`;
|
|
1349
|
+
if (have.has(key)) return;
|
|
1350
|
+
have.add(key);
|
|
1351
|
+
gaps.push({ task: g.task ?? null, kind: g.kind, cause: g.cause, reason: g.reason ?? g.kind, blocks: g.blocks ?? (g.task ? blocksOf(g.task) : []) });
|
|
1352
|
+
};
|
|
1353
|
+
if (!s.run.verify || !Array.isArray(s.run.verify.argv)) push({ task: null, kind: 'verification-undeclared', cause: 'input', blocks: view.tasks.map((r ) => r.id) });
|
|
1354
|
+
if (s.run.integrationGap) push({ ...s.run.integrationGap, cause: s.run.integrationGap.cause || 'no-progress' });
|
|
1355
|
+
for (const id of p.errors) {
|
|
1356
|
+
const d = s.dispatches[id];
|
|
1357
|
+
if (d && d.phase !== 'ended' && d.lastError) push({ task: d.task, kind: 'ledger-error', cause: 'input', reason: d.lastError.reason });
|
|
1358
|
+
}
|
|
1359
|
+
for (const g of p.gaps) push(g);
|
|
1360
|
+
// 106 IC-004 / IC-027: each fallback hold is a gap with cause `quota`, unless its task SUCCEEDED
|
|
1361
|
+
// since (finished outside dispatch, the way out of a hold: review C-4).
|
|
1362
|
+
const holds = s.run.fallback && s.run.fallback.holds && typeof s.run.fallback.holds === 'object' ? s.run.fallback.holds : {};
|
|
1363
|
+
const succeeded = new Set(view.tasks.filter((r ) => r.state === 'SUCCEEDED').map((r ) => r.id));
|
|
1364
|
+
for (const [task, h] of Object.entries(holds) ) {
|
|
1365
|
+
if (succeeded.has(task)) continue;
|
|
1366
|
+
push({ task, kind: h.reason, cause: 'quota', reason: `${h.from} quota; proposed ${h.proposed ?? 'none'}` });
|
|
1367
|
+
}
|
|
1368
|
+
for (const r of view.tasks) {
|
|
1369
|
+
if (r.doneStale) { push({ task: r.id, kind: 'plan-changed', cause: 'input', reason: r.reason }); continue; }
|
|
1370
|
+
if (r.state === 'BLOCKED') {
|
|
1371
|
+
if (r.reason === 'budget') push({ task: r.id, kind: 'budget', cause: 'budget', reason: 'budget' });
|
|
1372
|
+
else {
|
|
1373
|
+
const cls = lastFailure(s, r.id);
|
|
1374
|
+
push({ task: r.id, kind: cls || r.reason, cause: cls === 'worker-denied' ? 'authorization' : 'no-progress', reason: cls ? `${cls}: repeated` : r.reason });
|
|
1375
|
+
}
|
|
1376
|
+
} else if (r.state === 'CANCELLED') push({ task: r.id, kind: 'cancelled', cause: 'no-progress', reason: 'cancelled until resume' });
|
|
1377
|
+
else if (r.state === 'PLANNED' && r.reason === 'dependency-held') push({ task: r.id, kind: 'dependency-held', cause: 'no-progress', reason: heldIds(r).join(', '), blocks: [] });
|
|
1378
|
+
}
|
|
1379
|
+
return gaps;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function completedTasks(s , view ) {
|
|
1383
|
+
return view.tasks.filter((r ) => r.state === 'SUCCEEDED').map((r ) => {
|
|
1384
|
+
const d = (Object.values(s.dispatches) ).filter((x) => x.task === r.id && x.disposition === 'completed').sort((a, b) => b.token - a.token)[0];
|
|
1385
|
+
return { task: r.id, token: d ? d.token : null, integratedCommit: d ? d.integratedCommit ?? null : null };
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function keptWorktrees(s ) {
|
|
1390
|
+
const kept = (Object.values(s.dispatches) ).filter((d) => d.phase === 'ended' && d.attempt && d.attempt.worktree && !(d.removal && d.removal.verified) && fs.existsSync(d.attempt.worktree)).map((d) => d.attempt.worktree);
|
|
1391
|
+
const children = (Object.values(s.spawnRequests) ).filter((r) => r.worktree && !(r.removal && r.removal.verified) && fs.existsSync(r.worktree)).map((r) => r.worktree);
|
|
1392
|
+
return [...kept, ...children];
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function blockedReport(s , view , gaps ) {
|
|
1396
|
+
const cause = CAUSE_ORDER.find((c) => gaps.some((g) => g.cause === c)) || 'no-progress';
|
|
1397
|
+
return {
|
|
1398
|
+
cause,
|
|
1399
|
+
completed: completedTasks(s, view),
|
|
1400
|
+
integrated: (Object.values(s.dispatches) ).filter((d) => d.disposition === 'completed' && d.integratedCommit)
|
|
1401
|
+
.map((d) => ({ dispatchId: d.dispatchId, task: d.task, commit: d.integratedCommit, changes: Array.isArray(d.changes) ? d.changes.length : 0 })),
|
|
1402
|
+
gaps,
|
|
1403
|
+
keptWorktrees: keptWorktrees(s),
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Inside a locked write: runs `settle(st)`, which moves the run's state, unless the run holds a
|
|
1409
|
+
* state another writer owns (`cancelling` or `cancelled` from a cancel, IC-030 step 3;
|
|
1410
|
+
* `resume-required` from a lost driver, IC-009; `completed`), in which case nothing is moved and the
|
|
1411
|
+
* held state is returned. `decide` read the store outside the lock, so a cancel or a lost driver can
|
|
1412
|
+
* land between that read and this write; without this check the pass would overwrite it with
|
|
1413
|
+
* `completed` or `blocked`.
|
|
1414
|
+
*/
|
|
1415
|
+
function unlessCancelled(st , settle ) {
|
|
1416
|
+
if (HELD_RUN_STATES.includes(st.run.state)) return st.run.state;
|
|
1417
|
+
settle(st);
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/** The pass outcome for a run in a held state, as `decide` reports one (IC-003). */
|
|
1422
|
+
const cancelledOutcome = (state ) => (state === 'cancelling' ? 'continue' : state);
|
|
1423
|
+
/** The decide action's reason token for a held state: `cancelled` for a cancel's, else the state. */
|
|
1424
|
+
const heldAction = (state ) => (CANCEL_MARKED.includes(state) ? 'cancelled' : state);
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* The pass's last locked write: run state and records, durations, peaks and the step record. An
|
|
1428
|
+
* `apply` that returns a cancel-marked state (from `unlessCancelled`) changed no run state; the pass
|
|
1429
|
+
* then records and returns the cancelled outcome instead of `outcome`, without `extra`.
|
|
1430
|
+
*/
|
|
1431
|
+
function finishPass(p , outcome , apply = null, extra = {}) {
|
|
1432
|
+
const at = p.clock();
|
|
1433
|
+
let landed = outcome;
|
|
1434
|
+
const written = mutateDispatch(p.file, (s ) => {
|
|
1435
|
+
if (!s.run) return { write: false, result: null };
|
|
1436
|
+
const marked = apply ? apply(s) : null;
|
|
1437
|
+
if (typeof marked === 'string') {
|
|
1438
|
+
landed = cancelledOutcome(marked);
|
|
1439
|
+
p.act('decide', null, heldAction(marked), marked);
|
|
1440
|
+
}
|
|
1441
|
+
if (p.integrationMs.length) {
|
|
1442
|
+
const d = s.run.integrationDurations && Number.isInteger(s.run.integrationDurations.count) ? s.run.integrationDurations : emptyDuration();
|
|
1443
|
+
for (const ms of p.integrationMs) addDuration(d, ms);
|
|
1444
|
+
s.run.integrationDurations = d;
|
|
1445
|
+
}
|
|
1446
|
+
bumpPeak(s);
|
|
1447
|
+
s.run.measurement = measurement(s);
|
|
1448
|
+
const last = s.steps.length ? s.steps[s.steps.length - 1].step : 0;
|
|
1449
|
+
const step = (Number.isInteger(last) ? last : 0) + 1;
|
|
1450
|
+
s.steps.push({ step, at: iso(at), driver: { pid: p.driver.pid, start: p.driver.start ?? null }, outcome: landed, actions: p.actions });
|
|
1451
|
+
return { write: true, result: { step, dispatchRunId: s.run.dispatchRunId, measurement: structuredClone(s.run.measurement) } };
|
|
1452
|
+
});
|
|
1453
|
+
return {
|
|
1454
|
+
disposition: 'stepped', dispatchRunId: written ? written.dispatchRunId : null, step: written ? written.step : null, outcome: landed,
|
|
1455
|
+
actions: p.actions, ...(landed === outcome ? extra : {}), measurement: written ? written.measurement : null,
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/** IC-026 step 2 over `s`: why the run cannot complete yet (a non-ended dispatch, a lease, a pending
|
|
1460
|
+
* spawn request), or null. */
|
|
1461
|
+
function busyReason(s ) {
|
|
1462
|
+
const nonEnded = (Object.values(s.dispatches) ).filter((d) => d.phase !== 'ended').length;
|
|
1463
|
+
const pending = (Object.values(s.spawnRequests) ).filter((r) => r.state === 'requested' || (r.state === 'launched' && r.endState == null)).length;
|
|
1464
|
+
if (nonEnded) return `${nonEnded} active`;
|
|
1465
|
+
if (s.lease) return 'lease-held';
|
|
1466
|
+
if (pending) return `${pending} spawn pending`;
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/** IC-026 step 5 and the completion write; IC-021 then removes kept failed-attempt worktrees. */
|
|
1471
|
+
async function complete(p , s , view , decideStarted ) {
|
|
1472
|
+
const run = s.run;
|
|
1473
|
+
const started = Date.now();
|
|
1474
|
+
const verification = await p.verify({
|
|
1475
|
+
root: p.root, slug: p.slug, taskId: p.slug, worktree: run.integration.worktree, argv: run.verify.argv,
|
|
1476
|
+
argvSha256: run.verify.argvSha256 ?? null, seconds: run.verify.timeoutSeconds, env: p.env,
|
|
1477
|
+
dispatchRunId: run.dispatchRunId ?? null,
|
|
1478
|
+
onSpawn: (proc ) => mutateDispatch(p.file, (st ) => { st.run.verifying = proc; return { write: true, result: null }; }),
|
|
1479
|
+
});
|
|
1480
|
+
const passed = verification.exitCode === 0 && verification.timedOut === false;
|
|
1481
|
+
p.act('verify', null, passed ? 'final-passed' : 'final-failed', `evidence ${verification.evidenceId}`, Date.now() - started);
|
|
1482
|
+
const record = (st ) => {
|
|
1483
|
+
st.run.verifying = null;
|
|
1484
|
+
st.run.verifications = [...(Array.isArray(st.run.verifications) ? st.run.verifications : []), verification];
|
|
1485
|
+
};
|
|
1486
|
+
if (!passed) {
|
|
1487
|
+
const gaps = [{ task: null, kind: 'final-verification-failed', cause: 'no-progress', reason: `evidence ${verification.evidenceId}`, blocks: [], evidenceId: verification.evidenceId }];
|
|
1488
|
+
const report = blockedReport(s, view, gaps);
|
|
1489
|
+
p.act('decide', null, 'blocked', 'final-verification-failed', Date.now() - decideStarted);
|
|
1490
|
+
return finishPass(p, 'blocked', (st ) => {
|
|
1491
|
+
record(st);
|
|
1492
|
+
return unlessCancelled(st, (x ) => { setRunState(x, 'blocked'); x.run.blockedReport = report; });
|
|
1493
|
+
}, { blocked: report });
|
|
1494
|
+
}
|
|
1495
|
+
const completedAt = iso(p.clock());
|
|
1496
|
+
const completion = {
|
|
1497
|
+
completedAt,
|
|
1498
|
+
integration: { worktree: run.integration.worktree, branch: run.integration.branch, base: run.integration.base, head: run.integration.head },
|
|
1499
|
+
finalVerification: verification,
|
|
1500
|
+
tasks: completedTasks(s, view).map((t ) => ({ id: t.task, token: t.token, integratedCommit: t.integratedCommit })),
|
|
1501
|
+
removals: { verified: 0, kept: [] },
|
|
1502
|
+
};
|
|
1503
|
+
// A cancel that marked the run during the final verification wins: the run is not completed.
|
|
1504
|
+
// IC-026 step 2 is read again in the same write (RT-13): a dispatch admitted, a lease taken or a
|
|
1505
|
+
// spawn requested by a concurrent driver since decide's snapshot leaves the run running.
|
|
1506
|
+
const marked = mutateDispatch(p.file, (st ) => {
|
|
1507
|
+
record(st);
|
|
1508
|
+
if (HELD_RUN_STATES.includes(st.run.state)) return { write: true, result: st.run.state };
|
|
1509
|
+
const busy = busyReason(st);
|
|
1510
|
+
if (busy) return { write: true, result: { busy } };
|
|
1511
|
+
const halted = unlessCancelled(st, (x ) => { setRunState(x, 'completed'); Object.assign(x.run, { completedAt, completion, blockedReport: null }); });
|
|
1512
|
+
return { write: true, result: halted };
|
|
1513
|
+
});
|
|
1514
|
+
if (marked && typeof marked === 'object') {
|
|
1515
|
+
p.act('decide', null, 'continue', marked.busy, Date.now() - decideStarted);
|
|
1516
|
+
return finishPass(p, 'continue');
|
|
1517
|
+
}
|
|
1518
|
+
if (marked) {
|
|
1519
|
+
p.act('decide', null, heldAction(marked), marked, Date.now() - decideStarted);
|
|
1520
|
+
return finishPass(p, cancelledOutcome(marked));
|
|
1521
|
+
}
|
|
1522
|
+
// The worker cleanup of a dispatch integrated this pass is read before its removal is due (IC-021).
|
|
1523
|
+
recordWorkerCleanups(p);
|
|
1524
|
+
const after = readDispatch(p.file);
|
|
1525
|
+
let verified = 0;
|
|
1526
|
+
for (const d of Object.values(after.dispatches) ) {
|
|
1527
|
+
if (d.phase !== 'ended' || !d.attempt || !d.attempt.worktree) continue;
|
|
1528
|
+
if (d.removal && d.removal.verified) { verified += 1; continue; }
|
|
1529
|
+
if (!integrate.removalDue(d, after.run).due) continue;
|
|
1530
|
+
const removed = await integrate.removeDispatchWorktree({ root: p.root, slug: p.slug, dispatchId: d.dispatchId, env: p.env, storeFile: p.file });
|
|
1531
|
+
if (removed.removed) verified += 1;
|
|
1532
|
+
p.act('cleanup', d, removed.removed ? 'removed' : 'kept', removed.reason);
|
|
1533
|
+
}
|
|
1534
|
+
const kept = keptWorktrees(readDispatch(p.file));
|
|
1535
|
+
completion.removals = { verified, kept };
|
|
1536
|
+
p.act('decide', null, 'completed', 'every task SUCCEEDED', Date.now() - decideStarted);
|
|
1537
|
+
return finishPass(p, 'completed', (st ) => { st.run.completion.removals = completion.removals; }, { completion });
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
async function decide(p ) {
|
|
1541
|
+
const started = Date.now();
|
|
1542
|
+
const s = readDispatch(p.file);
|
|
1543
|
+
const run = s.run;
|
|
1544
|
+
const view = attempts.inspect({ file: p.ledgerFile, fsImpl: fs, roots: p.roots, slug: p.slug, env: p.env, cwd: p.cwd });
|
|
1545
|
+
const nonEnded = (Object.values(s.dispatches) ).filter((d) => d.phase !== 'ended');
|
|
1546
|
+
const pending = (Object.values(s.spawnRequests) ).filter((r) => r.state === 'requested' || (r.state === 'launched' && r.endState == null));
|
|
1547
|
+
const extra = (outcome , reason ) => p.act('decide', null, outcome, reason, Date.now() - started);
|
|
1548
|
+
|
|
1549
|
+
if (run.state === 'resume-required') { extra('resume-required', run.resumeReason); return finishPass(p, 'resume-required'); }
|
|
1550
|
+
if (CANCEL_MARKED.includes(run.state)) { extra('cancelled', run.state); return finishPass(p, cancelledOutcome(run.state)); }
|
|
1551
|
+
|
|
1552
|
+
// An integration gap blocks the run until a pass integrates again (IC-018).
|
|
1553
|
+
if (run.state === 'blocked' && run.integrationGap) {
|
|
1554
|
+
const report = blockedReport(s, view, gapsFor(p, s, view));
|
|
1555
|
+
extra('blocked', run.integrationGap.kind);
|
|
1556
|
+
return finishPass(p, 'blocked', (st ) => unlessCancelled(st, (x ) => { x.run.blockedReport = report; }), { blocked: report });
|
|
1557
|
+
}
|
|
1558
|
+
const verificationDeclared = Boolean(run.verify && Array.isArray(run.verify.argv));
|
|
1559
|
+
const ledgerErrors = [...p.errors].some((id) => s.dispatches[id] && s.dispatches[id].phase !== 'ended' && s.dispatches[id].lastError);
|
|
1560
|
+
|
|
1561
|
+
if (verificationDeclared && !ledgerErrors) {
|
|
1562
|
+
// A main-session dispatch awaiting its submit is yielded again (IC-005).
|
|
1563
|
+
if (!p.perform) {
|
|
1564
|
+
const waiting = nonEnded.find((d) => MAIN_SESSION_MODES.includes(d.mode) && d.phase === 'launched');
|
|
1565
|
+
if (waiting) p.perform = performYield(p, waiting);
|
|
1566
|
+
}
|
|
1567
|
+
if (p.perform) { extra('perform', p.perform.dispatchId); return finishPass(p, 'perform', null, { perform: p.perform }); }
|
|
1568
|
+
if (nonEnded.length || s.lease || pending.length) { extra('continue', `${nonEnded.length} active`); return finishPass(p, 'continue'); }
|
|
1569
|
+
if (view.run.control === 'paused') { extra('paused', '080 run paused'); return finishPass(p, 'paused'); }
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const gaps = gapsFor(p, s, view);
|
|
1573
|
+
const allDone = view.tasks.every((r ) => r.state === 'SUCCEEDED' && !r.doneStale);
|
|
1574
|
+
if (verificationDeclared && !ledgerErrors && allDone && gaps.length === 0) {
|
|
1575
|
+
return complete(p, s, view, started);
|
|
1576
|
+
}
|
|
1577
|
+
// A task that can still be admitted keeps the run going (IC-027).
|
|
1578
|
+
const gapTasks = new Set(gaps.map((g) => g.task).filter(Boolean));
|
|
1579
|
+
const admissible = verificationDeclared && !ledgerErrors && view.tasks.some((r ) => (r.state === 'READY' || (r.state === 'FAILED' && r.claimable === true)) && !gapTasks.has(r.id));
|
|
1580
|
+
if (admissible && !nonEnded.length) { extra('continue', 'admissible'); return finishPass(p, 'continue'); }
|
|
1581
|
+
if (!gaps.length) gaps.push({ task: null, kind: 'no-progress', cause: 'no-progress', reason: 'no task can progress', blocks: [] });
|
|
1582
|
+
const report = blockedReport(s, view, gaps);
|
|
1583
|
+
extra('blocked', report.cause);
|
|
1584
|
+
return finishPass(p, 'blocked', (st ) => unlessCancelled(st, (x ) => { setRunState(x, 'blocked'); x.run.blockedReport = report; }), { blocked: report });
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// ── step and run (IC-003, IC-004) ────────────────────────────────────────────────────────────────
|
|
1588
|
+
|
|
1589
|
+
/** A `blocked` run is not terminal: it runs again unless an integration gap holds it (IC-027). */
|
|
1590
|
+
function reopen(p ) {
|
|
1591
|
+
mutateDispatch(p.file, (s ) => {
|
|
1592
|
+
if (!s.run || s.run.state !== 'blocked' || s.run.integrationGap) return { write: false, result: null };
|
|
1593
|
+
setRunState(s, 'running');
|
|
1594
|
+
return { write: true, result: null };
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
async function runPass(p ) {
|
|
1599
|
+
const stopped = () => {
|
|
1600
|
+
if (!p.shouldStop()) return null;
|
|
1601
|
+
p.act('decide', null, 'interrupted', null);
|
|
1602
|
+
return finishPass(p, 'continue', null, { interrupted: true });
|
|
1603
|
+
};
|
|
1604
|
+
await reconcile(p);
|
|
1605
|
+
let current = readDispatch(p.file);
|
|
1606
|
+
if (current.run.state === 'resume-required') {
|
|
1607
|
+
p.act('decide', null, 'resume-required', current.run.resumeReason);
|
|
1608
|
+
return finishPass(p, 'resume-required');
|
|
1609
|
+
}
|
|
1610
|
+
if (['completed', 'cancelled'].includes(current.run.state)) return finishPass(p, current.run.state);
|
|
1611
|
+
{
|
|
1612
|
+
// The plan's own graph tasks (loadPlan, as inspect reads it): files, owner and the task-line tier.
|
|
1613
|
+
const { graph } = attempts.loadPlan({ roots: p.roots, slug: p.slug, fsImpl: fs });
|
|
1614
|
+
const files = new Map (graph.tasks.map((t ) => [t.id, t.files || []]));
|
|
1615
|
+
const owners = new Map (graph.tasks.map((t ) => [t.id, t.owner ?? null]));
|
|
1616
|
+
const tiers = new Map (graph.tasks.map((t ) => [t.id, t.tier ?? null]));
|
|
1617
|
+
p.filesOf = (task ) => files.get(task) || [];
|
|
1618
|
+
p.ownerOf = (task ) => owners.get(task) ?? null; // 106 IC-004: the owner and
|
|
1619
|
+
p.tierOf = (task ) => tiers.get(task) ?? null; // the task-line tier (review R-1)
|
|
1620
|
+
}
|
|
1621
|
+
const phases = [
|
|
1622
|
+
collect,
|
|
1623
|
+
integrateOne,
|
|
1624
|
+
timeouts,
|
|
1625
|
+
launchChildren,
|
|
1626
|
+
async (q ) => { reopen(q); return admitAndLaunch(q); },
|
|
1627
|
+
];
|
|
1628
|
+
for (const phase of phases) {
|
|
1629
|
+
const halt = stopped();
|
|
1630
|
+
if (halt) return halt;
|
|
1631
|
+
current = readDispatch(p.file);
|
|
1632
|
+
if (current.run.state === 'resume-required') break;
|
|
1633
|
+
await phase(p);
|
|
1634
|
+
}
|
|
1635
|
+
const halt = stopped();
|
|
1636
|
+
if (halt) return halt;
|
|
1637
|
+
return decide(p);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* IC-003: one bounded pass. Options: `slug`, `env`, `cwd` (the run's own worktree), `by`, `now`,
|
|
1642
|
+
* `platform`; `driver` when `run` registered one; `descriptors`, `registry`, `coverage` and
|
|
1643
|
+
* `evaluateImpl` for tests (081's in-process seams; the real registry otherwise); `shouldStop`, a
|
|
1644
|
+
* callback read between phases. Returns IC-003's StepResult, or IC-001's identity refusal.
|
|
1645
|
+
*/
|
|
1646
|
+
async function step(options = {}) {
|
|
1647
|
+
const p = passFor(options);
|
|
1648
|
+
const caller = checkCaller({ env: p.env, cwd: p.cwd, file: p.file });
|
|
1649
|
+
if (caller.refusal) return caller.refusal;
|
|
1650
|
+
const run = caller.store.run;
|
|
1651
|
+
if (['completed', 'cancelled'].includes(run.state)) {
|
|
1652
|
+
return { disposition: 'stepped', dispatchRunId: run.dispatchRunId, step: null, outcome: run.state, actions: [], measurement: measurement(caller.store) };
|
|
1653
|
+
}
|
|
1654
|
+
let own = null;
|
|
1655
|
+
if (options.driver) p.driver = options.driver;
|
|
1656
|
+
else {
|
|
1657
|
+
own = registerDriver({ file: p.file, now: p.clock() });
|
|
1658
|
+
p.driver = own.driver;
|
|
1659
|
+
}
|
|
1660
|
+
let threw = false;
|
|
1661
|
+
try {
|
|
1662
|
+
return await runPass(p);
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
// A pass that threw left claimed or leased state no later pass settles (RT-5): its driver end
|
|
1665
|
+
// records `threw` and makes the run `resume-required` in one write (IC-009, IC-032).
|
|
1666
|
+
threw = true;
|
|
1667
|
+
throw error;
|
|
1668
|
+
} finally {
|
|
1669
|
+
if (own) endDriver({ file: p.file, driver: own.driver, now: p.clock(), endReason: threw ? 'threw' : null });
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/** A pass that did nothing but reconcile (and admission skips), so `run` waits before the next. */
|
|
1674
|
+
function idle(result ) {
|
|
1675
|
+
const waiting = (a ) => ['reconcile', 'decide'].includes(a.action)
|
|
1676
|
+
|| (a.action === 'admit' && String(a.reason).startsWith('skipped:'))
|
|
1677
|
+
|| (a.action === 'integrate' && a.reason === 'lease-held')
|
|
1678
|
+
|| (a.action === 'cleanup' && /^(kept|child-kept)/.test(String(a.reason)));
|
|
1679
|
+
return result.actions.every(waiting);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* IC-004: registers one driver and repeats `step` until its outcome is `completed`, `blocked`,
|
|
1684
|
+
* `cancelled`, `resume-required`, `perform` or `paused`, or `maxSeconds` (default 600) passes, which
|
|
1685
|
+
* returns `continue` with `timeUp: true`. On SIGINT or SIGTERM the phase in progress finishes, the
|
|
1686
|
+
* driver end is written and the result carries `signal` and `exit` (130 or 143) for the command to
|
|
1687
|
+
* exit with; no further phase starts. `sleep` is a seam for the 500 ms idle wait.
|
|
1688
|
+
*/
|
|
1689
|
+
async function run(options = {}) {
|
|
1690
|
+
const maxSeconds = options.maxSeconds === undefined || options.maxSeconds === null ? RUN_MAX_SECONDS_DEFAULT : Number(options.maxSeconds);
|
|
1691
|
+
if (!Number.isInteger(maxSeconds) || maxSeconds < 1 || maxSeconds > RUN_MAX_SECONDS_LIMIT) throw usage(`--max-seconds expects 1 to ${RUN_MAX_SECONDS_LIMIT}, got '${options.maxSeconds}'`);
|
|
1692
|
+
const sleep = options.sleep || defaultSleep;
|
|
1693
|
+
const p = passFor(options);
|
|
1694
|
+
const caller = checkCaller({ env: p.env, cwd: p.cwd, file: p.file });
|
|
1695
|
+
if (caller.refusal) return caller.refusal;
|
|
1696
|
+
const startedMs = Date.now();
|
|
1697
|
+
let signal = null;
|
|
1698
|
+
const handlers = {};
|
|
1699
|
+
for (const name of Object.keys(SIGNAL_EXITS)) {
|
|
1700
|
+
handlers[name] = () => { if (!signal) signal = name; };
|
|
1701
|
+
process.on(name , handlers[name]);
|
|
1702
|
+
}
|
|
1703
|
+
const reg = registerDriver({ file: p.file, now: p.clock() });
|
|
1704
|
+
let steps = 0;
|
|
1705
|
+
let last = null;
|
|
1706
|
+
const fallbackActions = [];
|
|
1707
|
+
let timeUp = false;
|
|
1708
|
+
let threw = false;
|
|
1709
|
+
try {
|
|
1710
|
+
for (;;) {
|
|
1711
|
+
last = await step({ ...options, driver: reg.driver, shouldStop: () => signal !== null });
|
|
1712
|
+
steps += 1;
|
|
1713
|
+
// 106 IC-004 / FR-005: every fallback event of every pass reaches the run's output.
|
|
1714
|
+
if (last && Array.isArray(last.actions)) fallbackActions.push(...last.actions.filter((a ) => a && a.action === 'fallback'));
|
|
1715
|
+
if (last.disposition !== 'stepped' || signal || RUN_STOPS.includes(last.outcome)) break;
|
|
1716
|
+
if (Date.now() - startedMs >= maxSeconds * 1000) { timeUp = true; break; }
|
|
1717
|
+
if (idle(last)) await sleep(IDLE_WAIT_MS);
|
|
1718
|
+
if (signal) break;
|
|
1719
|
+
if (Date.now() - startedMs >= maxSeconds * 1000) { timeUp = true; break; }
|
|
1720
|
+
}
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
threw = true;
|
|
1723
|
+
throw error;
|
|
1724
|
+
} finally {
|
|
1725
|
+
for (const [name, handler] of Object.entries(handlers)) process.removeListener(name , handler);
|
|
1726
|
+
endDriver({ file: p.file, driver: reg.driver, now: p.clock(), endReason: threw ? 'threw' : null });
|
|
1727
|
+
}
|
|
1728
|
+
if (last && last.disposition !== 'stepped') return last;
|
|
1729
|
+
const result = { disposition: 'ran', dispatchRunId: last.dispatchRunId, steps, outcome: last.outcome, timeUp, elapsedMs: Date.now() - startedMs, last, fallback: fallbackActions };
|
|
1730
|
+
if (signal) Object.assign(result, { signal, exit: SIGNAL_EXITS[signal] });
|
|
1731
|
+
return result;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// ── submit (IC-005) ──────────────────────────────────────────────────────────────────────────────
|
|
1735
|
+
|
|
1736
|
+
const WHOLE = /^[0-9]+$/;
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* IC-005: a main-session attempt's result. Options: `slug`, `dispatchId`, `status` (`success` or
|
|
1740
|
+
* `failure`), `summary`, `blockers`, `tokens` with `tokensBasis`, `env`, `cwd`, `now`. Flag errors
|
|
1741
|
+
* throw `usage`; refusals return `{ disposition: 'refused' | 'stale', reason, ... }`.
|
|
1742
|
+
*/
|
|
1743
|
+
async function submit(options = {}) {
|
|
1744
|
+
const { dispatchId, status, summary } = options;
|
|
1745
|
+
if (typeof dispatchId !== 'string' || !DISPATCH_ID.test(dispatchId)) throw usage(`--dispatch expects 'd-' plus 12 hex characters, got '${dispatchId}'`);
|
|
1746
|
+
if (status !== 'success' && status !== 'failure') throw usage('--status expects success or failure');
|
|
1747
|
+
if (typeof summary !== 'string' || !summary.length || Buffer.byteLength(summary, 'utf8') > SUMMARY_BYTES) throw usage(`--summary expects 1 to ${SUMMARY_BYTES} bytes`);
|
|
1748
|
+
const blockers = options.blockers === undefined || options.blockers === null ? [] : [].concat(options.blockers).map(String);
|
|
1749
|
+
if (blockers.length > BLOCKERS_MAX || blockers.some((b) => !b.length || Array.from(b).length > BLOCKER_CHARS)) throw usage(`--blocker is repeatable up to ${BLOCKERS_MAX}, each 1 to ${BLOCKER_CHARS} characters`);
|
|
1750
|
+
const hasTokens = options.tokens !== undefined && options.tokens !== null;
|
|
1751
|
+
const hasBasis = options.tokensBasis !== undefined && options.tokensBasis !== null;
|
|
1752
|
+
if (hasTokens !== hasBasis) throw usage('--tokens and --tokens-basis go together or not at all');
|
|
1753
|
+
if (hasTokens && (!WHOLE.test(String(options.tokens)) || !BASES.includes(options.tokensBasis))) throw usage('--tokens expects a whole number and --tokens-basis measured or estimated');
|
|
1754
|
+
|
|
1755
|
+
const p = passFor(options);
|
|
1756
|
+
const caller = checkCaller({ env: p.env, cwd: p.cwd, file: p.file });
|
|
1757
|
+
if (caller.refusal) return caller.refusal;
|
|
1758
|
+
const d = caller.store.dispatches[dispatchId];
|
|
1759
|
+
if (!d) return refused('unknown-dispatch', { dispatchId });
|
|
1760
|
+
if (!MAIN_SESSION_MODES.includes(d.mode)) return refused('not-main-session', { dispatchId, mode: d.mode });
|
|
1761
|
+
if (d.phase !== 'launched') return refused('dispatch-not-awaiting', { dispatchId, phase: d.phase });
|
|
1762
|
+
|
|
1763
|
+
const reg = registerDriver({ file: p.file, now: p.clock() });
|
|
1764
|
+
p.driver = reg.driver;
|
|
1765
|
+
try {
|
|
1766
|
+
const ledger = readLedger(p);
|
|
1767
|
+
const stale = integrate.staleReason(d, ledger, d.parentRunKey ?? caller.store.run.runKey ?? null);
|
|
1768
|
+
if (stale) {
|
|
1769
|
+
await endStale(p, d, stale, null);
|
|
1770
|
+
return { disposition: 'stale', reason: stale, dispatchId, task: d.task, token: d.token };
|
|
1771
|
+
}
|
|
1772
|
+
let commit ;
|
|
1773
|
+
try {
|
|
1774
|
+
commit = integrate.attemptCommit({ worktree: d.attempt.worktree, base: d.attempt.base, dispatchId, task: d.task, token: d.token, env: p.env });
|
|
1775
|
+
} catch (error ) {
|
|
1776
|
+
if (!(error instanceof integrate.IntegrationError)) throw error;
|
|
1777
|
+
return refused('attempt-commit-failed', { dispatchId, error: error.reason, exitCode: error.exitCode });
|
|
1778
|
+
}
|
|
1779
|
+
const spent = hasTokens ? { tokens: Number(options.tokens), basis: options.tokensBasis } : usageFor({ ...d, usage: null }, null, []);
|
|
1780
|
+
const masked = cutUtf8(maskBody(summary).text, SUMMARY_KEPT_BYTES);
|
|
1781
|
+
const maskedBlockers = blockers.map((b ) => maskLine(b).text);
|
|
1782
|
+
const files = (() => {
|
|
1783
|
+
const view = attempts.inspect({ file: p.ledgerFile, fsImpl: fs, roots: p.roots, slug: p.slug, env: p.env, cwd: p.cwd });
|
|
1784
|
+
const task = view.graph.tasks.find((t ) => t.id === d.task);
|
|
1785
|
+
return task ? task.files || [] : [];
|
|
1786
|
+
})();
|
|
1787
|
+
const value = buildResult({
|
|
1788
|
+
dispatchId, task: d.task, token: d.token, epoch: d.epoch, mode: d.mode,
|
|
1789
|
+
outcome: status === 'success' ? 'offered' : 'failed', failureClass: status === 'success' ? null : 'worker-failure',
|
|
1790
|
+
summary: masked, status, base: d.attempt.base, commit: commit.commit, changes: commit.changes, files, evidence: [],
|
|
1791
|
+
blockers: maskedBlockers, usage: { ...spent, source: hasTokens ? 'main' : 'reservation' }, workerRun: null, isolation: null,
|
|
1792
|
+
});
|
|
1793
|
+
const written = writeEnvelope({ stateRoot: p.stateRoot, dispatchId, kind: 'result', value });
|
|
1794
|
+
const moved = moveDispatch(p, dispatchId, ['launched'], (x ) => {
|
|
1795
|
+
Object.assign(x, {
|
|
1796
|
+
phase: 'collected', outcome: status === 'success' ? 'offered' : 'failed', collectedAt: iso(p.clock()),
|
|
1797
|
+
report: { status, performer: 'main' }, usage: spent, usageParts: { main: spent },
|
|
1798
|
+
commit: commit.commit, changes: commit.changes, undeclared: value.undeclared,
|
|
1799
|
+
envelopes: { ...(x.envelopes || {}), result: written.pointer },
|
|
1800
|
+
});
|
|
1801
|
+
});
|
|
1802
|
+
if (!moved.applied) return refused('dispatch-not-awaiting', { dispatchId, phase: moved.phase });
|
|
1803
|
+
return {
|
|
1804
|
+
disposition: 'submitted', dispatchId, task: d.task, token: d.token, phase: 'collected',
|
|
1805
|
+
changes: commit.changes, undeclared: value.undeclared, result: { path: written.path },
|
|
1806
|
+
};
|
|
1807
|
+
} finally {
|
|
1808
|
+
endDriver({ file: p.file, driver: reg.driver, now: p.clock() });
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
module.exports = {
|
|
1813
|
+
step,
|
|
1814
|
+
run,
|
|
1815
|
+
submit,
|
|
1816
|
+
selectMode,
|
|
1817
|
+
staticGate,
|
|
1818
|
+
evaluateHarnesses,
|
|
1819
|
+
checkCaller,
|
|
1820
|
+
measurement,
|
|
1821
|
+
exitedCleanup,
|
|
1822
|
+
OUTCOMES,
|
|
1823
|
+
RUN_STOPS,
|
|
1824
|
+
IDLE_WAIT_MS,
|
|
1825
|
+
BACKSTOP_SECONDS,
|
|
1826
|
+
ATTEMPT_SECONDS_DEFAULT,
|
|
1827
|
+
RUN_MAX_SECONDS_DEFAULT,
|
|
1828
|
+
SIGNAL_EXITS,
|
|
1829
|
+
LOOP_BY,
|
|
1830
|
+
FALLBACK_MODES,
|
|
1831
|
+
REVIEW_OWNERS,
|
|
1832
|
+
lastFailure,
|
|
1833
|
+
lastWorkerDispatch,
|
|
1834
|
+
noStepReason,
|
|
1835
|
+
};
|