@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,778 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// Task run records (feature 080, design C4): the run key, who may act on a run, the run record,
|
|
6
|
+
// its totals, the aggregate budget check and the resume epoch.
|
|
7
|
+
//
|
|
8
|
+
// `runKeyFor` is one probe here: it reads the environment and walks up to `.git`, so it runs
|
|
9
|
+
// before the ledger lock. `resumeRun` runs the resume probes (source re-measure, scopeContext, git
|
|
10
|
+
// branch) before its lock. Every other function is pure over the parsed ledger object and is called
|
|
11
|
+
// inside `mutate()`'s `apply`: none reads a file, starts a process or reads the clock (`now` is
|
|
12
|
+
// passed in). A refusal is returned as `{ reason, ...fields }` or `null`; the caller wraps it in
|
|
13
|
+
// its own `{ write: false, result: { disposition: 'refused', ... } }`.
|
|
14
|
+
|
|
15
|
+
const { cwdHash, resolveIdentity, ID_PATTERN } = require('./context/identity.js');
|
|
16
|
+
const { findWorkTreeTop } = require('./source-version.js');
|
|
17
|
+
const crypto = require('node:crypto');
|
|
18
|
+
const { taskAttemptLimit, graphDigest, graphTuples, deriveTasks, isGraphPlan, overlaps } = require('./task-graph.js');
|
|
19
|
+
const { measureSources, compareSources } = require('./task-sources.js');
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/** IC-009: `null` attempts means the limit derived from the graph; `null` tokens means no limit. */
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/** `traceIds` is whether this caller's environment lets a run mint trace ids (088, IC-010): the
|
|
33
|
+
* switch is read where the environment is, because `startRun` is handed only the key. A key built
|
|
34
|
+
* by hand leaves it out and `traceIdsOn` reads the process environment. */
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* IC-011: the caller's run key. `top` is the worktree top above the identity's worktree (or the
|
|
39
|
+
* worktree itself outside git), so every subdirectory of one worktree reaches the same run. No
|
|
40
|
+
* flag sets or overrides it. The raw `identity.worktree` stays on `identity` for `scopeContext`.
|
|
41
|
+
*/
|
|
42
|
+
function runKeyFor(env , cwd ) {
|
|
43
|
+
const identity = resolveIdentity(env, { cwd, sessionId: '' });
|
|
44
|
+
const top = findWorkTreeTop(identity.worktree).top ?? identity.worktree;
|
|
45
|
+
return { runKey: `${cwdHash(top)}/${identity.run}/${identity.lineage}`, top, identity, traceIds: traceSwitchOn(env) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 088 DD-5: the telemetry switch read through its one reader. `telemetry.ts` is required here, not
|
|
49
|
+
* at load, so a module user that never reaches a run key does not load the trace modules. A fault
|
|
50
|
+
* reading the switch means no ids. */
|
|
51
|
+
function traceSwitchOn(env ) {
|
|
52
|
+
try { return !require('./trace/telemetry.js').telemetryOff(env); } catch { return false; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether `key` may mint trace ids: its own flag, else the given environment. */
|
|
56
|
+
function traceIdsOn(key , env = process.env) {
|
|
57
|
+
return typeof key.traceIds === 'boolean' ? key.traceIds : traceSwitchOn(env);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 088 IC-010: a fresh 128-bit id as 32 lowercase hex, or null when telemetry is off or the id
|
|
61
|
+
* source faults. Minting never fails the mutation that calls it. */
|
|
62
|
+
function mintTraceId(on ) {
|
|
63
|
+
if (!on) return null;
|
|
64
|
+
try {
|
|
65
|
+
const id = crypto.randomBytes(16).toString('hex');
|
|
66
|
+
return /^[0-9a-f]{32}$/.test(id) ? id : null;
|
|
67
|
+
} catch { return null; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** IC-011: a `solo` lineage may not start, pause, cancel, resume or reopen a run, because every
|
|
71
|
+
* solo session would share one. */
|
|
72
|
+
function refuseSolo(identity ) {
|
|
73
|
+
return identity && identity.lineage === 'solo' ? { reason: 'identity-solo' } : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** IC-026 / DEC-006: a worker session claims nothing in a graph plan and opens no run; it finishes
|
|
77
|
+
* with the token it was handed. */
|
|
78
|
+
function refuseWorker(identity ) {
|
|
79
|
+
return identity && identity.role === 'worker' ? { reason: 'worker-session' } : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeBudget(budget ) {
|
|
83
|
+
const pick = (v ) => (Number.isInteger(v) && v >= 0 ? v : null);
|
|
84
|
+
return { attempts: pick(budget && budget.attempts), tokens: pick(budget && budget.tokens) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function emptyTotals() {
|
|
88
|
+
return { attempts: 0, tokens: { measured: 0, estimated: 0, reserved: 0 }, unreported: 0 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* IC-010: a new run record for `key` (the `runKeyFor` result), `control: 'running'`, with an empty
|
|
93
|
+
* history. `epoch` is 1 for `start` and for a resume that creates the run (IC-025); a legacy-plan
|
|
94
|
+
* claim passes the epoch it records. The caller refreshes `graphDigest`, `graphTasks` and `totals`
|
|
95
|
+
* and appends history.
|
|
96
|
+
*/
|
|
97
|
+
function createRun(key , budget , now , { epoch = 1 } = {}) {
|
|
98
|
+
if (!Number.isInteger(epoch) || epoch < 0) throw new TypeError(`createRun: epoch must be a non-negative integer, got ${epoch}`);
|
|
99
|
+
const { identity } = key;
|
|
100
|
+
return {
|
|
101
|
+
runKey: key.runKey,
|
|
102
|
+
worktreeKey: key.runKey.split('/')[0],
|
|
103
|
+
run: identity.run,
|
|
104
|
+
lineage: identity.lineage,
|
|
105
|
+
worktree: key.top,
|
|
106
|
+
createdAt: now.toISOString(),
|
|
107
|
+
control: 'running',
|
|
108
|
+
epoch,
|
|
109
|
+
budget: normalizeBudget(budget),
|
|
110
|
+
totals: emptyTotals(),
|
|
111
|
+
graphDigest: null ,
|
|
112
|
+
graphTasks: null ,
|
|
113
|
+
history: [] ,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reported(a ) {
|
|
118
|
+
return Boolean(a.usage) && Number.isInteger(a.usage.tokens) && a.usage.tokens >= 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* IC-009: totals over every attempt carrying `runKey`, recomputed from the attempt list (never
|
|
123
|
+
* from the stored copy). Reported usage adds to its basis, including a reported 0; an active
|
|
124
|
+
* attempt's reservation is `reserved`; a cancelled attempt cannot report, so its reservation is
|
|
125
|
+
* `estimated`; a finished, released or superseded attempt with no usage is `unreported`
|
|
126
|
+
* (DEC-017 superseded-unreported: a reopen does not launder an unreported attempt out of DEC-011).
|
|
127
|
+
*/
|
|
128
|
+
function computeTotals(store , runKey ) {
|
|
129
|
+
const totals = emptyTotals();
|
|
130
|
+
const tasks = store && store.tasks && typeof store.tasks === 'object' ? store.tasks : {};
|
|
131
|
+
for (const entry of Object.values (tasks)) {
|
|
132
|
+
const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
|
|
133
|
+
for (const a of attempts) {
|
|
134
|
+
if (a.runKey !== runKey) continue;
|
|
135
|
+
totals.attempts += 1;
|
|
136
|
+
const reserve = a.reserve && Number.isInteger(a.reserve.tokens) ? a.reserve.tokens : 0;
|
|
137
|
+
if (reported(a)) {
|
|
138
|
+
if (a.usage.basis === 'measured') totals.tokens.measured += a.usage.tokens;
|
|
139
|
+
else totals.tokens.estimated += a.usage.tokens;
|
|
140
|
+
} else if (a.state === 'active') totals.tokens.reserved += reserve;
|
|
141
|
+
else if (a.state === 'cancelled') totals.tokens.estimated += reserve;
|
|
142
|
+
else if (a.state === 'finished' || a.state === 'released' || a.state === 'superseded') totals.unreported += 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return totals;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** IC-009: the run's attempt limit (its own, else the sum of the graph's task limits) and token
|
|
149
|
+
* limit (its own, else none). `run` may be absent: a legacy-plan claim before its record exists. */
|
|
150
|
+
function effectiveLimits(run , graph ) {
|
|
151
|
+
const budget = normalizeBudget(run && run.budget);
|
|
152
|
+
const attempts = budget.attempts ?? (graph && Array.isArray(graph.tasks)
|
|
153
|
+
? graph.tasks.reduce((sum , t ) => sum + taskAttemptLimit(t).limit, 0)
|
|
154
|
+
: 0);
|
|
155
|
+
return { attempts, tokens: budget.tokens };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* IC-006 step 12, the aggregate half, in IC-009's order. With a token limit: no reservation,
|
|
160
|
+
* then unreported usage (DEC-011), then the token limit. With or without one: the attempt limit.
|
|
161
|
+
*/
|
|
162
|
+
function aggregateRefusal(run , totals , graph , reserveTokens ) {
|
|
163
|
+
const limits = effectiveLimits(run, graph);
|
|
164
|
+
const { measured, estimated, reserved } = totals.tokens;
|
|
165
|
+
if (limits.tokens !== null) {
|
|
166
|
+
const reserve = Number.isInteger(reserveTokens) ? (reserveTokens ) : 0;
|
|
167
|
+
if (reserve <= 0) return { reason: 'reservation-required', limit: limits.tokens };
|
|
168
|
+
if (totals.unreported > 0) return { reason: 'aggregate-unaccounted', unreported: totals.unreported };
|
|
169
|
+
if (measured + estimated + reserved + reserve > limits.tokens) {
|
|
170
|
+
return {
|
|
171
|
+
reason: 'aggregate-exhausted', scope: 'run', measure: 'tokens', limit: limits.tokens,
|
|
172
|
+
used: measured + estimated, reserved, basis: { measured, estimated },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (totals.attempts + 1 > limits.attempts) {
|
|
177
|
+
return {
|
|
178
|
+
reason: 'aggregate-exhausted', scope: 'run', measure: 'attempts', limit: limits.attempts,
|
|
179
|
+
used: totals.attempts, reserved: 0, basis: null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** IC-006 step 7: a paused or cancelled run refuses claims. An absent run is running. */
|
|
186
|
+
function controlRefusal(run , runKey ) {
|
|
187
|
+
if (run && run.control === 'paused') return { reason: 'run-paused', runKey };
|
|
188
|
+
if (run && run.control === 'cancelled') return { reason: 'run-cancelled', runKey };
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** IC-025 / DEC-007, IC-006 step 8 (graph plans only): the run must exist and `epoch` must equal
|
|
193
|
+
* its epoch. Absent, old or future epochs are all `resume-required`, naming the current one.
|
|
194
|
+
* DEC-019 epoch-zero-never-matches: an epoch-0 run (created by a legacy claim or a control) has
|
|
195
|
+
* never been started or resumed, so it matches no epoch. */
|
|
196
|
+
function epochRefusal(run , runKey , epoch ) {
|
|
197
|
+
if (run && Number.isInteger(run.epoch) && run.epoch >= 1 && Number.isInteger(epoch) && epoch === run.epoch) return null;
|
|
198
|
+
return { reason: 'resume-required', runKey, epoch: run ? run.epoch : null };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** IC-025: the epoch an acknowledged resume issues: `epoch + 1`, or 1 when it creates the run. */
|
|
202
|
+
function nextEpoch(run ) {
|
|
203
|
+
return run && Number.isInteger(run.epoch) ? run.epoch + 1 : 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── run controls (IC-012, IC-026; DEC-005) ────────────────────────────────────────────────────
|
|
207
|
+
//
|
|
208
|
+
// Each is an `apply` for `mutate()`: it takes the parsed store and the inputs resolved before the
|
|
209
|
+
// lock, and returns `{ write, result }`. A refusal returns `write: false`, so it changes nothing.
|
|
210
|
+
// None kills a process: ending a worker is 081's.
|
|
211
|
+
|
|
212
|
+
/** IC-004: above this many tasks a run stores `graphTasks: null` and `graphTruncated: true`. */
|
|
213
|
+
const GRAPH_TASKS_LIMIT = 500;
|
|
214
|
+
|
|
215
|
+
/** IC-004 and IC-009: every run write stores the recorded graph and totals recomputed from the
|
|
216
|
+
* attempt list. */
|
|
217
|
+
function refreshRun(store , run , graph ) {
|
|
218
|
+
run.graphDigest = graphDigest(graph);
|
|
219
|
+
if (graph.tasks.length > GRAPH_TASKS_LIMIT) {
|
|
220
|
+
run.graphTasks = null;
|
|
221
|
+
run.graphTruncated = true;
|
|
222
|
+
} else {
|
|
223
|
+
run.graphTasks = graphTuples(graph);
|
|
224
|
+
delete run.graphTruncated;
|
|
225
|
+
}
|
|
226
|
+
run.totals = computeTotals(store, run.runKey);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function ownRun(store , runKey ) {
|
|
230
|
+
return store.runs && Object.prototype.hasOwnProperty.call(store.runs, runKey) ? store.runs[runKey] : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The caller's run, created when absent. Only start and an acknowledged resume issue an epoch
|
|
234
|
+
* (DEC-007), so a run another control creates has epoch 0, like a legacy claim's (C.2). */
|
|
235
|
+
function ensureRun(store , key , now ) {
|
|
236
|
+
if (!store.runs) store.runs = {};
|
|
237
|
+
let run = ownRun(store, key.runKey);
|
|
238
|
+
if (!run) {
|
|
239
|
+
run = createRun(key, null, now, { epoch: 0 });
|
|
240
|
+
store.runs[key.runKey] = run;
|
|
241
|
+
}
|
|
242
|
+
return run;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** IC-011, IC-026: a solo lineage or a worker session may not act on a run. */
|
|
246
|
+
function identityRefusal(identity ) {
|
|
247
|
+
return refuseSolo(identity) || refuseWorker(identity);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function refusedControl(reason , runKey , extra = {}) {
|
|
251
|
+
return { write: false, result: { disposition: 'refused', reason, runKey, ...extra } };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** IC-012 `start`: creates the caller's run with `epoch: 1` and history `start`. `budget` is
|
|
255
|
+
* `{ attempts, tokens }`, each a whole number or null. */
|
|
256
|
+
function startRun(store , { key, graph, by, budget, now } ) {
|
|
257
|
+
const who = identityRefusal(key.identity);
|
|
258
|
+
if (who) return refusedControl(who.reason, key.runKey);
|
|
259
|
+
if (ownRun(store, key.runKey)) return refusedControl('run-exists', key.runKey);
|
|
260
|
+
if (!store.runs) store.runs = {};
|
|
261
|
+
const run = createRun(key, budget, now, { epoch: 1 });
|
|
262
|
+
store.runs[key.runKey] = run;
|
|
263
|
+
const entry = { at: now.toISOString(), action: 'start', by, reason: null, epoch: 1 };
|
|
264
|
+
const traceId = mintTraceId(traceIdsOn(key));
|
|
265
|
+
if (traceId) { run.traceId = traceId; entry.traceId = traceId; }
|
|
266
|
+
run.history.push(entry);
|
|
267
|
+
refreshRun(store, run, graph);
|
|
268
|
+
return { write: true, result: { disposition: 'started', runKey: key.runKey, epoch: 1, budget: run.budget, revision: store.revision } };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** IC-012 `pause`: the run, created if absent, becomes `paused`; active attempts are untouched and
|
|
272
|
+
* may still finish (IC-007). */
|
|
273
|
+
function pauseRun(store , { key, graph, by, reason, now } ) {
|
|
274
|
+
const who = identityRefusal(key.identity);
|
|
275
|
+
if (who) return refusedControl(who.reason, key.runKey);
|
|
276
|
+
const existing = ownRun(store, key.runKey);
|
|
277
|
+
if (existing && existing.control === 'paused') return refusedControl('control-unchanged', key.runKey, { control: 'paused' });
|
|
278
|
+
if (existing && existing.control === 'cancelled') return refusedControl('run-cancelled', key.runKey);
|
|
279
|
+
const run = ensureRun(store, key, now);
|
|
280
|
+
run.control = 'paused';
|
|
281
|
+
run.history.push({ at: now.toISOString(), action: 'pause', by, reason });
|
|
282
|
+
refreshRun(store, run, graph);
|
|
283
|
+
return { write: true, result: { disposition: 'paused', runKey: key.runKey, revision: store.revision } };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function activeAttempts(store ) {
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const [taskId, entry] of Object.entries (store.tasks || {})) {
|
|
289
|
+
for (const attempt of entry.attempts) if (attempt.state === 'active') out.push({ taskId, attempt });
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* IC-012 run `cancel`: every active attempt carrying this run key becomes `cancelled`. With
|
|
296
|
+
* `adoptUnowned`, an active attempt with no run key is first given this one and cancelled with
|
|
297
|
+
* `adopted: true` (critic A m7). Active attempts of other runs, and runKey-less ones without
|
|
298
|
+
* `adoptUnowned`, are listed in `notCancelled` and left alone. `cancelled` names every attempt
|
|
299
|
+
* this call cancelled, adopted ones included; `adopted` is that subset.
|
|
300
|
+
*/
|
|
301
|
+
function cancelRun(store , { key, graph, by, reason, adoptUnowned = false, now } ) {
|
|
302
|
+
const who = identityRefusal(key.identity);
|
|
303
|
+
if (who) return refusedControl(who.reason, key.runKey);
|
|
304
|
+
const existing = ownRun(store, key.runKey);
|
|
305
|
+
if (existing && existing.control === 'cancelled') return refusedControl('control-unchanged', key.runKey, { control: 'cancelled' });
|
|
306
|
+
const run = ensureRun(store, key, now);
|
|
307
|
+
const at = now.toISOString();
|
|
308
|
+
const cancelled = [];
|
|
309
|
+
const adopted = [];
|
|
310
|
+
const notCancelled = [];
|
|
311
|
+
for (const { taskId, attempt } of activeAttempts(store)) {
|
|
312
|
+
const owned = attempt.runKey === key.runKey;
|
|
313
|
+
const adopt = !attempt.runKey && adoptUnowned;
|
|
314
|
+
if (!owned && !adopt) {
|
|
315
|
+
notCancelled.push({ taskId, token: attempt.token, runKey: attempt.runKey || null });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (adopt) {
|
|
319
|
+
attempt.runKey = key.runKey;
|
|
320
|
+
adopted.push({ taskId, token: attempt.token });
|
|
321
|
+
}
|
|
322
|
+
attempt.state = 'cancelled';
|
|
323
|
+
attempt.cancel = adopt ? { at, by, reason, adopted: true } : { at, by, reason };
|
|
324
|
+
cancelled.push({ taskId, token: attempt.token });
|
|
325
|
+
}
|
|
326
|
+
run.control = 'cancelled';
|
|
327
|
+
run.history.push({ at, action: 'cancel', by, reason, cancelled });
|
|
328
|
+
refreshRun(store, run, graph);
|
|
329
|
+
return {
|
|
330
|
+
write: true,
|
|
331
|
+
result: { disposition: 'cancelled', runKey: key.runKey, cancelled, adopted, notCancelled, revision: store.revision },
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** IC-012 attempt `cancel`: the task's active attempt holding `token`, which must carry this run
|
|
336
|
+
* key, becomes `cancelled`; the run's control is unchanged. */
|
|
337
|
+
function cancelAttempt(store , { key, graph, task, token, by, reason, now } ) {
|
|
338
|
+
const who = identityRefusal(key.identity);
|
|
339
|
+
if (who) return refusedControl(who.reason, key.runKey, { taskId: task, token });
|
|
340
|
+
const entry = store.tasks && store.tasks[task];
|
|
341
|
+
const active = entry ? entry.attempts.find((a ) => a.state === 'active') || null : null;
|
|
342
|
+
if (!active) return refusedControl('no-active-attempt', key.runKey, { taskId: task, token });
|
|
343
|
+
if (active.token !== token) return refusedControl('token-mismatch', key.runKey, { taskId: task, token, activeToken: active.token });
|
|
344
|
+
if (active.runKey !== key.runKey) {
|
|
345
|
+
return { write: false, result: { disposition: 'refused', reason: 'foreign-run', taskId: task, token, runKey: active.runKey || null } };
|
|
346
|
+
}
|
|
347
|
+
const run = ensureRun(store, key, now);
|
|
348
|
+
const at = now.toISOString();
|
|
349
|
+
active.state = 'cancelled';
|
|
350
|
+
active.cancel = { at, by, reason };
|
|
351
|
+
const cancelled = [{ taskId: task, token }];
|
|
352
|
+
run.history.push({ at, action: 'cancel', by, reason, cancelled });
|
|
353
|
+
refreshRun(store, run, graph);
|
|
354
|
+
return {
|
|
355
|
+
write: true,
|
|
356
|
+
result: { disposition: 'cancelled', runKey: key.runKey, taskId: task, token, control: run.control, cancelled, revision: store.revision },
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── resume (IC-015, IC-025; DEC-004, DEC-007, DEC-010) ────────────────────────────────────────
|
|
361
|
+
//
|
|
362
|
+
// The probes (IC-013 re-measure, IC-014 scopeContext and the git branch) run before any lock in
|
|
363
|
+
// `resumeRun`; everything else here is pure over the parsed store, so the locked `apply` can
|
|
364
|
+
// rebuild the ledger-derived half of the report and recompute the digest against what is on disk.
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* IC-005 row 4: the cancelled attempts a resume has cleared. A cancel is cleared when the run its
|
|
368
|
+
* attempt's `runKey` names has a `resume` history entry that lists it in `cleared`. A `resume`
|
|
369
|
+
* entry that carries a `cleared` list is authoritative: its stamp is never compared, since a stamp
|
|
370
|
+
* from another clock can sort either side of a resume. Only a run none of whose resume entries
|
|
371
|
+
* carries a `cleared` list (a record written before the list existed) clears by a resume being
|
|
372
|
+
* later than `cancel.at`. Pure; an
|
|
373
|
+
* empty list for a store with no cancelled attempt (every `8ce5ad7` store). Each entry also
|
|
374
|
+
* carries the cleared `attempt` object itself, so a caller can
|
|
375
|
+
* pair by identity when tokens are duplicated or missing (a malformed ledger).
|
|
376
|
+
*/
|
|
377
|
+
function clearedCancels(store ) {
|
|
378
|
+
const out = [];
|
|
379
|
+
const runs = store && store.runs && typeof store.runs === 'object' ? store.runs : null;
|
|
380
|
+
if (!runs) return out;
|
|
381
|
+
for (const [taskId, entry] of Object.entries (store.tasks || {})) {
|
|
382
|
+
const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
|
|
383
|
+
for (const a of attempts) {
|
|
384
|
+
if (!a || a.state !== 'cancelled' || !a.runKey) continue;
|
|
385
|
+
const run = ownRun(store, a.runKey);
|
|
386
|
+
const history = run && Array.isArray(run.history) ? run.history : [];
|
|
387
|
+
const at = a.cancel && typeof a.cancel.at === 'string' ? a.cancel.at : null;
|
|
388
|
+
const resumes = history.filter((h ) => h && h.action === 'resume');
|
|
389
|
+
// Legacy fallback: only a run none of whose resumes carries a `cleared` list clears by stamp.
|
|
390
|
+
const listed = resumes.some((h ) => Array.isArray(h.cleared));
|
|
391
|
+
const cleared = resumes.some((h ) => Array.isArray(h.cleared)
|
|
392
|
+
? h.cleared.some((c ) => c && c.taskId === taskId && c.token === a.token)
|
|
393
|
+
: (!listed && at !== null && typeof h.at === 'string' && h.at > at));
|
|
394
|
+
if (cleared) out.push({ taskId, token: a.token, attempt: a });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Ids in plan order, then any ledger-only ids sorted, so a report's order never depends on the
|
|
401
|
+
* order keys were written. */
|
|
402
|
+
function taskOrder(store , graph ) {
|
|
403
|
+
const planned = graph.tasks.map((t ) => t.id);
|
|
404
|
+
const extra = Object.keys(store.tasks || {}).filter((id) => !planned.includes(id)).sort();
|
|
405
|
+
return [...planned, ...extra];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* IC-013 at resume: the attempt each task's source comparison reads. A SUCCEEDED task compares
|
|
410
|
+
* its done attempt (listed under `doneChanged`); any other task its highest-token attempt. A task
|
|
411
|
+
* with no attempt, or whose attempt recorded no sources and whose plan line declares no files,
|
|
412
|
+
* has nothing to compare. `measure` is false when the comparison is unverifiable without a disk
|
|
413
|
+
* access (no `sources`, or `sourcesTruncated`).
|
|
414
|
+
*/
|
|
415
|
+
function resumeTargets(store , graph , derived ) {
|
|
416
|
+
const targets = [];
|
|
417
|
+
for (const record of derived) {
|
|
418
|
+
const entry = store.tasks && store.tasks[record.id];
|
|
419
|
+
const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
|
|
420
|
+
if (!attempts.length) continue;
|
|
421
|
+
const done = record.state === 'SUCCEEDED';
|
|
422
|
+
const attempt = done
|
|
423
|
+
? attempts.find((a ) => a.token === record.doneToken)
|
|
424
|
+
: attempts.reduce((a , b ) => (b.token > a.token ? b : a));
|
|
425
|
+
if (!attempt) continue;
|
|
426
|
+
const task = graph.tasks.find((t ) => t.id === record.id);
|
|
427
|
+
const declared = task && Array.isArray(task.files) ? task.files.length : 0;
|
|
428
|
+
const recorded = Array.isArray(attempt.sources);
|
|
429
|
+
if (!recorded && declared === 0) continue;
|
|
430
|
+
targets.push({ taskId: record.id, token: attempt.token, done, attempt, measure: recorded && !attempt.sourcesTruncated, declared });
|
|
431
|
+
}
|
|
432
|
+
return targets;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const targetKey = (taskId , token ) => `${taskId}#${token}`;
|
|
436
|
+
|
|
437
|
+
/** The resume probes' source half (IC-013): one re-measure per target that can be compared, of
|
|
438
|
+
* the entries its attempt recorded, under its own `execWorktree` and then the plan roots. Reads
|
|
439
|
+
* the filesystem; runs before any lock. */
|
|
440
|
+
function probeSources(targets , roots ) {
|
|
441
|
+
const probes = new Map ();
|
|
442
|
+
for (const t of targets) {
|
|
443
|
+
if (!t.measure) continue;
|
|
444
|
+
const files = t.attempt.sources.map((s ) => s.file);
|
|
445
|
+
probes.set(targetKey(t.taskId, t.token), measureSources({ files }, t.attempt.execWorktree ?? null, roots));
|
|
446
|
+
}
|
|
447
|
+
return probes;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** `{ taskId, token, count }` for an unverifiable task: `count` is the entries that could not be
|
|
451
|
+
* compared, every recorded (or declared) entry when the cause is the whole task. */
|
|
452
|
+
function unverifiableCount(target , after ) {
|
|
453
|
+
const recorded = Array.isArray(target.attempt.sources) ? target.attempt.sources : [];
|
|
454
|
+
const whole = recorded.length || target.declared;
|
|
455
|
+
if (!target.measure || !after || after.execWorktreeMissing || after.sourcesTruncated) return whole;
|
|
456
|
+
const now = new Map (after.sources.map((s ) => [s.file, s]));
|
|
457
|
+
const opaque = (s ) => !s || s.kind === 'unhashable' || s.kind === 'unverifiable';
|
|
458
|
+
const missing = recorded.some((s ) => !now.has(s.file));
|
|
459
|
+
if (missing) return whole;
|
|
460
|
+
return recorded.filter((s ) => opaque(s) || opaque(now.get(s.file))).length;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** IC-004 at resume: the recorded digest against the current one and, when the run stored its
|
|
464
|
+
* tuple list, the ids added, removed and changed. */
|
|
465
|
+
function graphComparison(run , graph ) {
|
|
466
|
+
const current = graphDigest(graph);
|
|
467
|
+
const recorded = run && typeof run.graphDigest === 'string' ? run.graphDigest : null;
|
|
468
|
+
const truncated = Boolean(run && run.graphTruncated);
|
|
469
|
+
const out = { recorded, current, changed: recorded !== null && recorded !== current, added: [] , removed: [] , changedIds: [] , truncated };
|
|
470
|
+
if (!run || !Array.isArray(run.graphTasks)) return out;
|
|
471
|
+
const before = new Map (run.graphTasks.map((tuple ) => [String(tuple[0]), JSON.stringify(tuple)]));
|
|
472
|
+
const after = new Map (graphTuples(graph).map((tuple ) => [String(tuple[0]), JSON.stringify(tuple)]));
|
|
473
|
+
for (const [id, tuple] of after) {
|
|
474
|
+
if (!before.has(id)) out.added.push(id);
|
|
475
|
+
else if (before.get(id) !== tuple) out.changedIds.push(id);
|
|
476
|
+
}
|
|
477
|
+
for (const id of before.keys()) if (!after.has(id)) out.removed.push(id);
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* IC-015: the resume report from the store (the ledger-derived half) and the probe results
|
|
483
|
+
* (`probes` from `probeSources`, `staleContext` from `compareScopeContext`). Pure: called once
|
|
484
|
+
* outside the lock for the report, and again inside it to recompute the digest. A target with no
|
|
485
|
+
* probe (an attempt claimed after the probes ran) is unverifiable, so the digest moves.
|
|
486
|
+
*/
|
|
487
|
+
function buildResumeReport(store , { key, graph, hashes = null, probes, staleContext, now } ) {
|
|
488
|
+
const run = ownRun(store, key.runKey);
|
|
489
|
+
const derived = deriveTasks(graph, store, { hashes, clearedCancels: clearedCancels(store) });
|
|
490
|
+
const activeAttempts = [];
|
|
491
|
+
for (const taskId of taskOrder(store, graph)) {
|
|
492
|
+
const entry = store.tasks[taskId];
|
|
493
|
+
const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
|
|
494
|
+
for (const a of attempts.filter((x ) => x.state === 'active').sort((x , y ) => x.token - y.token)) {
|
|
495
|
+
const started = Date.parse(a.startedAt);
|
|
496
|
+
activeAttempts.push({
|
|
497
|
+
taskId, token: a.token, owner: a.owner, mode: a.mode, startedAt: a.startedAt,
|
|
498
|
+
ageSeconds: Number.isFinite(started) ? Math.max(0, Math.floor((now.getTime() - started) / 1000)) : 0,
|
|
499
|
+
runKey: a.runKey || null, execWorktree: a.execWorktree || null,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const changedSources = [];
|
|
504
|
+
const doneChanged = [];
|
|
505
|
+
const unverifiable = [];
|
|
506
|
+
for (const target of resumeTargets(store, graph, derived)) {
|
|
507
|
+
const after = probes.get(targetKey(target.taskId, target.token)) || null;
|
|
508
|
+
const compared = target.measure && after ? compareSources(target.attempt, after) : { unverifiable: true, files: [] };
|
|
509
|
+
if (compared.unverifiable) {
|
|
510
|
+
unverifiable.push({ taskId: target.taskId, token: target.token, count: unverifiableCount(target, after) });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const into = target.done ? doneChanged : changedSources;
|
|
514
|
+
for (const f of compared.files) {
|
|
515
|
+
if (f.change !== 'unchanged') into.push({ taskId: target.taskId, token: target.token, file: f.file, change: f.change });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
runKey: key.runKey,
|
|
520
|
+
control: run ? run.control : 'none',
|
|
521
|
+
epoch: run ? run.epoch : null,
|
|
522
|
+
activeAttempts,
|
|
523
|
+
changedSources,
|
|
524
|
+
doneChanged,
|
|
525
|
+
unverifiable,
|
|
526
|
+
staleContext: staleContext.map((s ) => ({ kind: s.kind, path: s.path, reasons: [...s.reasons] })),
|
|
527
|
+
graph: graphComparison(run, graph),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** JSON with object keys sorted at every level. */
|
|
532
|
+
function canonicalJson(value ) {
|
|
533
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
534
|
+
if (value && typeof value === 'object') {
|
|
535
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
|
|
536
|
+
}
|
|
537
|
+
return JSON.stringify(value === undefined ? null : value);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** IC-015: `sha256:` over the report's canonical JSON with every `ageSeconds` removed, so an
|
|
541
|
+
* acknowledgement a few seconds later still matches. */
|
|
542
|
+
function resumeDigest(report ) {
|
|
543
|
+
const stable = { ...report, activeAttempts: report.activeAttempts.map(({ ageSeconds, ...rest } ) => rest) };
|
|
544
|
+
return `sha256:${crypto.createHash('sha256').update(canonicalJson(stable)).digest('hex')}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** The current branch of `worktree` (`git symbolic-ref --short -q HEAD`), and, when HEAD is
|
|
548
|
+
* detached or unreadable, its commit (`git rev-parse HEAD`). 2 s per call; a failure is null. */
|
|
549
|
+
function probeGit(worktree , gitRunner ) {
|
|
550
|
+
const run = gitRunner || ((args , cwd ) => {
|
|
551
|
+
const { execFileSync } = require('node:child_process');
|
|
552
|
+
try {
|
|
553
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
const clean = (out ) => (typeof out === 'string' && out.trim() ? out.trim() : null);
|
|
559
|
+
const branch = clean(run(['symbolic-ref', '--short', '-q', 'HEAD'], worktree));
|
|
560
|
+
const commit = branch ? null : clean(run(['rev-parse', 'HEAD'], worktree));
|
|
561
|
+
return { branch, commit };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* IC-015 / IC-025: the resume. `io.read()` returns the store without a lock (`readTaskState`);
|
|
566
|
+
* `io.mutate(apply)` runs `apply(store)` inside the ledger lock and returns its result. The
|
|
567
|
+
* caller has loaded the plan (`graph`, `hashes`) and resolved `key` (`runKeyFor`) and checked the
|
|
568
|
+
* flags.
|
|
569
|
+
*
|
|
570
|
+
* Order: identity refusals (`identity-solo`; `worker-session` in a graph plan); then every probe,
|
|
571
|
+
* before any lock: the store is read, each target's sources re-measured, `scopeContext` called
|
|
572
|
+
* with the raw identity worktree and compared with the branch and commit probed in the run's
|
|
573
|
+
* worktree. Without `ack`: `ack-required` with the report and digest, nothing written. With
|
|
574
|
+
* `ack`: one `mutate()` that rebuilds the ledger-derived half against the disk, recomputes the
|
|
575
|
+
* digest and either refuses `report-changed` (nothing written) or sets `control: 'running'`,
|
|
576
|
+
* issues `nextEpoch`, lists the run's cancelled attempts in the history entry's `cleared` and
|
|
577
|
+
* appends history `resume`. `summary-store.ts` and the git process are loaded here only (IC-022).
|
|
578
|
+
*/
|
|
579
|
+
function resumeRun(
|
|
580
|
+
io ,
|
|
581
|
+
{ key, graph, hashes = null, roots = [], ack = null, by = null, reason = null, now, env = process.env, gitRunner = null }
|
|
582
|
+
,
|
|
583
|
+
) {
|
|
584
|
+
const refusal = refuseSolo(key.identity) || (isGraphPlan(graph) ? refuseWorker(key.identity) : null);
|
|
585
|
+
if (refusal) return { disposition: 'refused', reason: refusal.reason, runKey: key.runKey };
|
|
586
|
+
|
|
587
|
+
// Probes, before any lock.
|
|
588
|
+
const seen = io.read();
|
|
589
|
+
const targets = resumeTargets(seen, graph, deriveTasks(graph, seen, { hashes, clearedCancels: clearedCancels(seen) }));
|
|
590
|
+
const probes = probeSources(targets, roots);
|
|
591
|
+
const { scopeContext, compareScopeContext } = require('./context/summary-store.js');
|
|
592
|
+
const context = scopeContext(key.identity.worktree, key.identity.run, key.identity.lineage, env);
|
|
593
|
+
const seenRun = ownRun(seen, key.runKey);
|
|
594
|
+
const { branch, commit } = probeGit(seenRun && seenRun.worktree ? seenRun.worktree : key.top, gitRunner);
|
|
595
|
+
const staleContext = compareScopeContext(context, { worktree: key.identity.worktree, branch, commit });
|
|
596
|
+
const inputs = { key, graph, hashes, probes, staleContext, now };
|
|
597
|
+
|
|
598
|
+
if (!ack) {
|
|
599
|
+
const report = buildResumeReport(seen, inputs);
|
|
600
|
+
return { disposition: 'ack-required', report, digest: resumeDigest(report) };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return io.mutate((store ) => {
|
|
604
|
+
const report = buildResumeReport(store, inputs);
|
|
605
|
+
const digest = resumeDigest(report);
|
|
606
|
+
if (digest !== ack) return { write: false, result: { disposition: 'refused', reason: 'report-changed', report, digest } };
|
|
607
|
+
if (!store.runs) store.runs = {};
|
|
608
|
+
let run = ownRun(store, key.runKey);
|
|
609
|
+
const epoch = nextEpoch(run);
|
|
610
|
+
if (!run) {
|
|
611
|
+
run = createRun(key, null, now, { epoch });
|
|
612
|
+
store.runs[key.runKey] = run;
|
|
613
|
+
}
|
|
614
|
+
run.control = 'running';
|
|
615
|
+
run.epoch = epoch;
|
|
616
|
+
const cleared = [];
|
|
617
|
+
for (const [taskId, entry] of Object.entries (store.tasks || {})) {
|
|
618
|
+
for (const a of entry.attempts) if (a.state === 'cancelled' && a.runKey === key.runKey) cleared.push({ taskId, token: a.token });
|
|
619
|
+
}
|
|
620
|
+
const entry = { at: now.toISOString(), action: 'resume', by, reason: reason ?? null, digest, epoch, cleared };
|
|
621
|
+
const traceId = mintTraceId(traceIdsOn(key, env));
|
|
622
|
+
if (traceId) {
|
|
623
|
+
entry.traceId = traceId;
|
|
624
|
+
entry.traceLink = typeof run.traceId === 'string' ? run.traceId : null;
|
|
625
|
+
run.traceId = traceId;
|
|
626
|
+
}
|
|
627
|
+
run.history.push(entry);
|
|
628
|
+
refreshRun(store, run, graph);
|
|
629
|
+
return { write: true, result: { disposition: 'resumed', report, digest, epoch, revision: store.revision } };
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ── inspect (IC-016, IC-009 `totalsMismatch`; critic B m2, m4, m6) ────────────────────────────
|
|
634
|
+
//
|
|
635
|
+
// Pure over a store the caller read with `readTaskState` (no lock, no write, no git process).
|
|
636
|
+
|
|
637
|
+
/** IC-009: the stored totals equal the recomputed ones. Key order is not compared. */
|
|
638
|
+
function sameTotals(stored , computed ) {
|
|
639
|
+
return canonicalJson(stored === undefined ? null : stored) === canonicalJson(computed);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Critic B m6: tasks per phase and `[P]` fan-out (the `[P]` tasks of a phase, which may run
|
|
644
|
+
* together). Advisory only: no threshold, never a refusal.
|
|
645
|
+
*/
|
|
646
|
+
function decompositionAdvisory(graph ) {
|
|
647
|
+
const phases = graph.phases.map((phase ) => {
|
|
648
|
+
const tasks = graph.tasks.filter((t ) => t.phase === phase);
|
|
649
|
+
return { phase, tasks: tasks.length, parallel: tasks.filter((t ) => t.parallel).length };
|
|
650
|
+
});
|
|
651
|
+
return {
|
|
652
|
+
advisoryOnly: true ,
|
|
653
|
+
phases,
|
|
654
|
+
maxTasksPerPhase: phases.reduce((m , p ) => Math.max(m, p.tasks), 0),
|
|
655
|
+
maxParallelFanOut: phases.reduce((m , p ) => Math.max(m, p.parallel), 0),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** 088 OI-1: a history entry as it was before trace ids, so `inspect` output does not depend on the
|
|
660
|
+
* telemetry switch (FR-002). */
|
|
661
|
+
function withoutTrace(entry ) {
|
|
662
|
+
if (!entry || typeof entry !== 'object' || (!('traceId' in entry) && !('traceLink' in entry))) return entry;
|
|
663
|
+
const { traceId, traceLink, ...rest } = entry;
|
|
664
|
+
return rest;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** 093 IC-018 c: `worktreeKey`, `run` and `lineage` split from a stored `runKey`
|
|
668
|
+
* (`<cwdHash>/<run>/<lineage>`, `runKeyFor`); all three null when the record has none or another shape. */
|
|
669
|
+
function runKeyParts(runKey ) {
|
|
670
|
+
const parts = typeof runKey === 'string' ? runKey.split('/') : [];
|
|
671
|
+
if (parts.length === 3 && /^[0-9a-f]{16}$/.test(parts[0]) && ID_PATTERN.test(parts[1]) && ID_PATTERN.test(parts[2])) {
|
|
672
|
+
return { worktreeKey: parts[0], run: parts[1], lineage: parts[2] };
|
|
673
|
+
}
|
|
674
|
+
return { worktreeKey: null, run: null, lineage: null };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** 093 IC-013: a task's attempts in ledger order, metadata only (no reason text, evidence id,
|
|
678
|
+
* worktree, usage or trace). `endedAt` is `finishedAt` (finished; superseded, which is always a
|
|
679
|
+
* finished attempt), else `endedAt` (released), else `cancel.at` (cancelled); `result` is kept
|
|
680
|
+
* only when it is one of RESULTS. IC-018 c appends `worktreeKey`, `run` and `lineage` (`runKeyParts`). */
|
|
681
|
+
function attemptsView(store , taskId ) {
|
|
682
|
+
// Required here, not at the top: task-attempts.ts requires this module while it loads.
|
|
683
|
+
const { RESULTS } = require('./task-attempts.js');
|
|
684
|
+
const entry = store && store.tasks ? store.tasks[taskId] : null;
|
|
685
|
+
const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
|
|
686
|
+
return attempts.map((a ) => ({
|
|
687
|
+
token: a.token, owner: a.owner ?? null, mode: a.mode ?? null, state: a.state,
|
|
688
|
+
startedAt: a.startedAt ?? null, endedAt: a.finishedAt ?? a.endedAt ?? (a.cancel && a.cancel.at) ?? null,
|
|
689
|
+
result: RESULTS.includes(a.result) ? a.result : null,
|
|
690
|
+
...runKeyParts(a.runKey),
|
|
691
|
+
}));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* IC-016: the inspect view of the caller's run. `store` is a plain read (`readTaskState`);
|
|
696
|
+
* `graph`, `hashes` and `warnings` come from `loadPlan`; `key` from `runKeyFor`. Pure: nothing is
|
|
697
|
+
* written, no lock is taken and no process runs. `totals` is recomputed from the attempt list and
|
|
698
|
+
* `totalsMismatch` says whether the stored copy differs (IC-009); the stored copy is returned as
|
|
699
|
+
* `storedTotals` only then. `defaults` lists every budget limit that is a default rather than
|
|
700
|
+
* declared (critic B m2), so a caller sees nominal limits without reading each row.
|
|
701
|
+
*/
|
|
702
|
+
function inspectRun(store , { slug, key, graph, hashes = null, warnings = [] } ) {
|
|
703
|
+
const run = ownRun(store, key.runKey);
|
|
704
|
+
const tasks = deriveTasks(graph, store, { hashes, clearedCancels: clearedCancels(store) });
|
|
705
|
+
const limits = effectiveLimits(run, graph);
|
|
706
|
+
const totals = computeTotals(store, key.runKey);
|
|
707
|
+
const totalsMismatch = Boolean(run) && !sameTotals(run.totals, totals);
|
|
708
|
+
const runView = {
|
|
709
|
+
runKey: key.runKey,
|
|
710
|
+
exists: Boolean(run),
|
|
711
|
+
control: run ? run.control : 'none',
|
|
712
|
+
epoch: run ? run.epoch : null,
|
|
713
|
+
budget: { attempts: limits.attempts, tokens: limits.tokens, source: normalizeBudget(run && run.budget).attempts === null ? 'default' : 'run' },
|
|
714
|
+
totals,
|
|
715
|
+
totalsMismatch,
|
|
716
|
+
history: run && Array.isArray(run.history) ? run.history.map(withoutTrace) : [],
|
|
717
|
+
};
|
|
718
|
+
if (totalsMismatch) runView.storedTotals = run.totals === undefined ? null : run.totals;
|
|
719
|
+
return {
|
|
720
|
+
slug,
|
|
721
|
+
revision: Number.isInteger(store.revision) ? store.revision : 0,
|
|
722
|
+
graph: {
|
|
723
|
+
digest: graphDigest(graph),
|
|
724
|
+
graphPlan: isGraphPlan(graph),
|
|
725
|
+
warnings,
|
|
726
|
+
tasks: graph.tasks.map((t ) => ({
|
|
727
|
+
id: t.id, phase: t.phase, owner: t.owner ?? null, taskDeps: t.taskDeps, parallel: t.parallel,
|
|
728
|
+
acceptance: t.acceptance, files: t.files || [],
|
|
729
|
+
})),
|
|
730
|
+
overlaps: overlaps(graph),
|
|
731
|
+
},
|
|
732
|
+
tasks: tasks.map((r ) => ({ ...r, attempts: attemptsView(store, r.id) })),
|
|
733
|
+
run: runView,
|
|
734
|
+
defaults: {
|
|
735
|
+
runAttempts: runView.budget.source === 'default',
|
|
736
|
+
tasks: tasks.filter((r ) => r.budget.source === 'default').map((r ) => r.id),
|
|
737
|
+
},
|
|
738
|
+
advisory: { decomposition: decompositionAdvisory(graph) },
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* IC-016 text: one line per task (`id state reason budget used/limit`, `(default)` on a default
|
|
744
|
+
* limit), then control, epoch, limits with their source and totals with each token figure
|
|
745
|
+
* labelled by basis. Measured and estimated are never summed.
|
|
746
|
+
*/
|
|
747
|
+
function formatInspect(view ) {
|
|
748
|
+
const lines = [];
|
|
749
|
+
const r = view.run;
|
|
750
|
+
if (view.defaults.runAttempts || view.defaults.tasks.length) {
|
|
751
|
+
const what = [
|
|
752
|
+
...(view.defaults.runAttempts ? ['run attempt limit'] : []),
|
|
753
|
+
...(view.defaults.tasks.length ? [`task limits of ${view.defaults.tasks.join(', ')}`] : []),
|
|
754
|
+
];
|
|
755
|
+
lines.push(`DEFAULT BUDGETS: ${what.join('; ')} are defaults, not declared limits`);
|
|
756
|
+
}
|
|
757
|
+
lines.push(`Task attempts for ${view.slug} (revision ${view.revision}, ${view.graph.graphPlan ? 'graph plan' : 'legacy plan'})`);
|
|
758
|
+
for (const t of view.tasks) {
|
|
759
|
+
lines.push(` ${t.id} ${t.state} ${t.reason} budget ${t.budget.used}/${t.budget.limit}${t.budget.source === 'default' ? ' (default)' : ''}`);
|
|
760
|
+
}
|
|
761
|
+
lines.push(`Run ${r.runKey}: control ${r.control}, epoch ${r.epoch === null ? 'none' : r.epoch}`);
|
|
762
|
+
lines.push(` limits: attempts ${r.budget.attempts} (${r.budget.source}), tokens ${r.budget.tokens === null ? 'none' : `${r.budget.tokens} (run)`}`);
|
|
763
|
+
const tk = r.totals.tokens;
|
|
764
|
+
lines.push(` totals: attempts ${r.totals.attempts}; tokens measured ${tk.measured}, estimated ${tk.estimated}, reserved ${tk.reserved}; unreported ${r.totals.unreported}`);
|
|
765
|
+
if (r.totalsMismatch) lines.push(' totals mismatch: the stored totals differ from the attempt list; the figures above are recomputed');
|
|
766
|
+
for (const w of view.graph.warnings) lines.push(`Warning: ${w.kind} ${w.ids.join(' ')}${w.value !== undefined ? ` '${w.value}'` : ''}`);
|
|
767
|
+
for (const o of view.graph.overlaps) lines.push(`Overlap: ${o.a} and ${o.b} both declare ${o.files.join(', ')}`);
|
|
768
|
+
const d = view.advisory.decomposition;
|
|
769
|
+
lines.push(`Advisory (decomposition): ${d.phases.map((p ) => `${p.phase} ${p.tasks} task${p.tasks === 1 ? '' : 's'}, ${p.parallel} [P]`).join('; ')}`);
|
|
770
|
+
return lines;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
module.exports = {
|
|
774
|
+
runKeyFor, refuseSolo, refuseWorker, createRun, computeTotals, effectiveLimits,
|
|
775
|
+
aggregateRefusal, controlRefusal, epochRefusal, nextEpoch,
|
|
776
|
+
GRAPH_TASKS_LIMIT, refreshRun, ensureRun, identityRefusal, startRun, pauseRun, cancelRun, cancelAttempt,
|
|
777
|
+
clearedCancels, buildResumeReport, resumeDigest, resumeRun, inspectRun, formatInspect,
|
|
778
|
+
};
|