@khoavu882/doflow 2.0.0-beta.5 → 2.0.1
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 +16 -16
- package/bin/doflow.js +22 -12
- package/core/.antigravity-plugin/plugin.json +1 -2
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/settings/settings.json +26 -9
- 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/hooks.json +0 -10
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
- package/core/harnesses/copilot/hooks/doflow-lib.sh +94 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-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 +51 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -11
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
- package/core/harnesses/kiro/hooks/session-start.sh +1 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
- package/core/harnesses/pi/extensions/doflow-guard.js +11 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
- package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +16 -2
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +20 -7
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
- package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
- package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
- package/core/harnesses/shared/hooks/stream-hook-runner.js +58 -20
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +55 -82
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +58 -31
- package/core/registry/contribution-points.json +35 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1445 -366
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +599 -996
- 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/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/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 +272 -0
- package/core/registry/storage.json +687 -31
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -6
- 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 +3 -2
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- 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 +49 -5
- package/core/shared/scripts/doflow/bin/doflow-run +152 -34
- package/core/shared/skills/do/SKILL.md +14 -12
- 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 +45 -97
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -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 +48 -148
- 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 +23 -20
- package/core/shared/skills/do-diagnose/SKILL.md +18 -6
- package/core/shared/skills/do-document/SKILL.md +12 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +19 -7
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +16 -2
- 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 +10 -11
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +13 -14
- package/core/shared/skills/do-plan/SKILL.md +34 -20
- 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/shared/workflows/do-flow-chain.md +2 -0
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +143 -66
- package/src/adapters/claude/index.js +46 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +61 -13
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +291 -46
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +23 -12
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +2 -1
- package/src/adapters/pi/index.js +14 -7
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +22 -3
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +2 -1
- package/src/cli/index.js +337 -32
- package/src/cli/runtime-commands.js +220 -60
- 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 +4 -16
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +412 -78
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +551 -26
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +472 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +20 -11
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +13 -7
- package/src/runtime/cli.js +196 -19
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/delivery-store.js +4 -6
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/pointer.js +67 -12
- 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 +158 -79
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-ledger.js +52 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +33 -3
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +624 -18
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +281 -26
- package/src/runtime/intake/engine.js +677 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- 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 +88 -10
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +38 -7
- package/src/runtime/lifecycle/overview.js +26 -2
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +2 -2
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +487 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +46 -5
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +10 -30
- package/src/runtime/retrieval-record.js +75 -5
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +273 -0
- package/src/runtime/roadmap/loop.js +1719 -0
- package/src/runtime/roadmap/merge.js +390 -0
- package/src/runtime/roadmap/rows.js +1122 -0
- package/src/runtime/roadmap/seed.js +1063 -0
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +60 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +1 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +85 -14
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +176 -55
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +373 -64
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verb-view.js +267 -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 -54
- 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 +256 -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 +610 -67
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -7
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -251
- package/src/adapters/gemini/index.js +0 -429
|
@@ -24,20 +24,31 @@ const path = require('node:path');
|
|
|
24
24
|
const crypto = require('node:crypto');
|
|
25
25
|
const { updateTaskState, readTaskState } = require('./task-state.js');
|
|
26
26
|
const { parsePlan } = require('./scaffold/artifacts.js');
|
|
27
|
+
const { buildGraph, planVerdict, deriveTasks, isGraphPlan, doneOf, normalizeOwner } = require('./task-graph.js');
|
|
28
|
+
const {
|
|
29
|
+
runKeyFor, refuseWorker, computeTotals, aggregateRefusal, controlRefusal, epochRefusal,
|
|
30
|
+
refreshRun, ensureRun, identityRefusal, startRun, pauseRun, cancelRun, cancelAttempt,
|
|
31
|
+
clearedCancels, resumeRun, inspectRun,
|
|
32
|
+
} = require('./task-run.js');
|
|
33
|
+
const { measureSources } = require('./task-sources.js');
|
|
34
|
+
const { findWorkTreeTop } = require('./source-version.js');
|
|
27
35
|
const { EvidenceLedger } = require('./evidence-ledger.js');
|
|
28
36
|
const { projectRoot: lifecycleProjectRoot } = require('./lifecycle/root.js');
|
|
29
37
|
const { isSafeSlug } = require('./task-scope.js');
|
|
30
38
|
|
|
31
39
|
const SCHEMA_VERSION = 1;
|
|
32
40
|
const MODES = Object.freeze(['manual', 'sequential', 'subagent']);
|
|
33
|
-
const RESULTS = Object.freeze(['COMPLETED', 'BLOCKED', 'ABANDONED', 'INCONCLUSIVE']);
|
|
34
|
-
const
|
|
41
|
+
const RESULTS = Object.freeze(['COMPLETED', 'BLOCKED', 'ABANDONED', 'INCONCLUSIVE', 'FAILED']);
|
|
42
|
+
const USAGE_BASES = Object.freeze(['measured', 'estimated']);
|
|
43
|
+
const RUN_CONTROLS = Object.freeze(['running', 'paused', 'cancelled']);
|
|
44
|
+
const ACTIONS = Object.freeze(['claim', 'finish', 'release', 'status', 'start', 'pause', 'cancel', 'resume', 'inspect', 'reopen', 'report-usage']);
|
|
35
45
|
const TASK_ID = /^[A-Za-z]+\.\d+$/;
|
|
36
46
|
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._ -]*$/;
|
|
37
47
|
|
|
38
48
|
/** A failure that is the caller's or the environment's, not a finding: exit 2. */
|
|
39
49
|
class TaskAttemptError extends Error {
|
|
40
50
|
|
|
51
|
+
|
|
41
52
|
constructor(code , message ) {
|
|
42
53
|
super(message);
|
|
43
54
|
this.name = 'TaskAttemptError';
|
|
@@ -62,11 +73,12 @@ function assertSlug(slug ) {
|
|
|
62
73
|
// ── plan.md ────────────────────────────────────────────────────────────────────────────────────
|
|
63
74
|
|
|
64
75
|
/** The hash a claim records and `finish` compares: everything about the task's plan line except
|
|
65
|
-
* its checkbox, so ticking the box after a finish is not an edit of the task.
|
|
76
|
+
* its checkbox, so ticking the box after a finish is not an edit of the task. A line with neither
|
|
77
|
+
* `acceptance:` nor `budget:` hashes exactly as before IC-002; `parallel` is never hashed. */
|
|
66
78
|
function taskLineHash(task ) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
const legacy = [task.id, task.description, task.stories, task.owner, task.files, task.dependsOn, task.externalContract];
|
|
80
|
+
const extended = (task.acceptance && task.acceptance.length) || task.budget ? [...legacy, task.acceptance, task.budget] : legacy;
|
|
81
|
+
const material = JSON.stringify(extended);
|
|
70
82
|
return `sha256:${crypto.createHash('sha256').update(material).digest('hex')}`;
|
|
71
83
|
}
|
|
72
84
|
|
|
@@ -82,24 +94,42 @@ function loadPlan({ roots, slug, fsImpl = fs } ) {
|
|
|
82
94
|
try { text = fsImpl.readFileSync(found, 'utf8'); } catch (error ) {
|
|
83
95
|
throw new TaskAttemptError('plan-unreadable', `cannot read ${found}: ${error.message}`);
|
|
84
96
|
}
|
|
97
|
+
const verdict = planVerdict(text);
|
|
85
98
|
const parsed = parsePlan(text);
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (const task of parsed.tasks) {
|
|
91
|
-
if (seen.has(task.id)) throw new TaskAttemptError('plan-malformed', `plan task id '${task.id}' appears more than once in ${found}`);
|
|
92
|
-
seen.add(task.id);
|
|
99
|
+
if (verdict.tasks === 0) {
|
|
100
|
+
const error = new TaskAttemptError('plan-malformed', `${found} holds no \`- [ ] <phase>.<n>\` tasks: ${parsed.gaps.map((g ) => g.why).join('; ') || 'nothing to track'}`);
|
|
101
|
+
error.problems = verdict.problems;
|
|
102
|
+
throw error;
|
|
93
103
|
}
|
|
104
|
+
const built = buildGraph(parsed);
|
|
105
|
+
if (!verdict.ok) throw malformedGraph(found, verdict.problems);
|
|
94
106
|
const phases = uniq(parsed.tasks.map((t ) => t.phase));
|
|
95
107
|
return {
|
|
96
108
|
planPath: relative.split(path.sep).join('/'),
|
|
97
109
|
tasks: parsed.tasks.map((t ) => ({
|
|
98
110
|
id: t.id, phase: t.phase, phaseIndex: phases.indexOf(t.phase), checked: t.done, hash: taskLineHash(t),
|
|
99
111
|
})),
|
|
112
|
+
graph: built.graph,
|
|
113
|
+
warnings: built.warnings,
|
|
100
114
|
};
|
|
101
115
|
}
|
|
102
116
|
|
|
117
|
+
/** IC-003: `plan-malformed` carrying every problem. A lone `duplicate-id` keeps the pre-080 text. */
|
|
118
|
+
function malformedGraph(found , problems ) {
|
|
119
|
+
const only = problems.length === 1 ? problems[0] : null;
|
|
120
|
+
const message = only && only.kind === 'duplicate-id'
|
|
121
|
+
? `plan task id '${only.ids[0]}' appears more than once in ${found}`
|
|
122
|
+
: `${found} has ${problems.length} task-graph problem${problems.length === 1 ? '' : 's'}: ${problems.map(describeProblem).join('; ')}`;
|
|
123
|
+
const error = new TaskAttemptError('plan-malformed', message);
|
|
124
|
+
error.problems = problems;
|
|
125
|
+
return error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function describeProblem(problem ) {
|
|
129
|
+
const detail = problem.via ? ` via ${problem.via.join(', ')}` : problem.value !== undefined ? ` '${problem.value}'` : '';
|
|
130
|
+
return `${problem.kind}${problem.ids.length ? ` ${problem.ids.join(' ')}` : ''}${detail}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
103
133
|
// ── store ──────────────────────────────────────────────────────────────────────────────────────
|
|
104
134
|
|
|
105
135
|
function unreadable(file , why ) {
|
|
@@ -119,6 +149,17 @@ function normalizeStore(data , file ) {
|
|
|
119
149
|
throw unreadable(file, `Task '${id}' has no readable attempt list.`);
|
|
120
150
|
}
|
|
121
151
|
}
|
|
152
|
+
// IC-010: `runs` is optional and additive; the store stays version 1.
|
|
153
|
+
if (data.runs !== undefined) {
|
|
154
|
+
if (typeof data.runs !== 'object' || data.runs === null || Array.isArray(data.runs)) {
|
|
155
|
+
throw unreadable(file, "Its 'runs' is not an object.");
|
|
156
|
+
}
|
|
157
|
+
for (const [key, run] of (Object.entries(data.runs) )) {
|
|
158
|
+
if (!run || typeof run !== 'object' || Array.isArray(run)) throw unreadable(file, `Run '${key}' is not an object.`);
|
|
159
|
+
if (!RUN_CONTROLS.includes(run.control)) throw unreadable(file, `Run '${key}' has control '${run.control}', not one of ${RUN_CONTROLS.join(', ')}.`);
|
|
160
|
+
if (!Number.isInteger(run.epoch) || run.epoch < 0) throw unreadable(file, `Run '${key}' has epoch '${run.epoch}', not a non-negative integer.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
122
163
|
return data;
|
|
123
164
|
}
|
|
124
165
|
|
|
@@ -138,8 +179,12 @@ function mutate({ file, fsImpl, apply } ) {
|
|
|
138
179
|
try {
|
|
139
180
|
updateTaskState({
|
|
140
181
|
fsImpl, file,
|
|
182
|
+
// `apply` decides from the store alone, so a reclaimed writer re-runs it on the fresh store.
|
|
183
|
+
lostLockRetries: 2,
|
|
141
184
|
build: (disk ) => {
|
|
142
|
-
|
|
185
|
+
// A copy of the top level: `updateTaskState` stamps `disk.revision + 1` on what this returns, so
|
|
186
|
+
// bumping the disk object itself would make every write advance the revision by two.
|
|
187
|
+
const store = { ...normalizeStore(disk, file) };
|
|
143
188
|
store.revision = (Number.isInteger(store.revision) ? store.revision : 0) + 1;
|
|
144
189
|
outcome = apply(store);
|
|
145
190
|
if (!outcome.write) throw new NoWrite();
|
|
@@ -149,6 +194,8 @@ function mutate({ file, fsImpl, apply } ) {
|
|
|
149
194
|
} catch (error ) {
|
|
150
195
|
if (error instanceof NoWrite) return outcome.result;
|
|
151
196
|
if (error instanceof TaskAttemptError) throw error;
|
|
197
|
+
// Contention, not damage: the store is intact and a later run decides afresh.
|
|
198
|
+
if (error?.code === 'lock-lost') throw new TaskAttemptError('lock-lost', error.message);
|
|
152
199
|
throw unreadable(file, error.message);
|
|
153
200
|
}
|
|
154
201
|
return outcome.result;
|
|
@@ -160,11 +207,18 @@ function entryFor(store , taskId ) {
|
|
|
160
207
|
}
|
|
161
208
|
|
|
162
209
|
const activeAttempt = (entry ) => (entry ? entry.attempts.find((a ) => a.state === 'active') || null : null);
|
|
163
|
-
|
|
210
|
+
// DEC-016 done-attempt-selection: the highest-token finished COMPLETED attempt, as deriveTasks reads it.
|
|
211
|
+
const doneAttempt = (entry ) => (entry ? doneOf(entry.attempts) : null);
|
|
164
212
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
213
|
+
function planHashes(plan ) {
|
|
214
|
+
return Object.fromEntries(plan.tasks.map((t ) => [t.id, t.hash]));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** IC-005 over the store, with each task's current line hash from `loadPlan` (done-stale, IC-024)
|
|
218
|
+
* and the cancels an acknowledged resume cleared (row 4, IC-015). Pure, so it may run inside the
|
|
219
|
+
* lock. A store with no cancelled attempt (every `8ce5ad7` store) clears nothing. */
|
|
220
|
+
function derivePlan(plan , store ) {
|
|
221
|
+
return deriveTasks(plan.graph, store, { hashes: planHashes(plan), clearedCancels: clearedCancels(store) });
|
|
168
222
|
}
|
|
169
223
|
|
|
170
224
|
// ── evidence ───────────────────────────────────────────────────────────────────────────────────
|
|
@@ -220,31 +274,164 @@ function rootsFor({ cwd, lifecycleRoot } ) {
|
|
|
220
274
|
return uniq([path.resolve(cwd), lifecycleRoot]);
|
|
221
275
|
}
|
|
222
276
|
|
|
223
|
-
|
|
277
|
+
/** The caller's run record, or null. Own keys only, so a run key never reaches the prototype. */
|
|
278
|
+
function runOf(store , runKey ) {
|
|
279
|
+
return store.runs && Object.prototype.hasOwnProperty.call(store.runs, runKey) ? store.runs[runKey] : null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** IC-026: `--exec-worktree` made absolute against `cwd`, then lifted to its worktree top. A path
|
|
283
|
+
* that is not an existing directory is exit 2 `usage` (DEC-023 exec-worktree-directory). Runs
|
|
284
|
+
* before the lock. */
|
|
285
|
+
function resolveExecWorktree(execWorktree , cwd , fsImpl ) {
|
|
286
|
+
if (!given(execWorktree)) return null;
|
|
287
|
+
const absolute = path.resolve(cwd, String(execWorktree));
|
|
288
|
+
if (String(execWorktree).trim() === '' || !fsImpl.existsSync(absolute)) {
|
|
289
|
+
throw new TaskAttemptError('usage', `--exec-worktree '${execWorktree}' does not exist`);
|
|
290
|
+
}
|
|
291
|
+
if (!fsImpl.statSync(absolute).isDirectory()) {
|
|
292
|
+
throw new TaskAttemptError('usage', `--exec-worktree '${execWorktree}' is not a directory`);
|
|
293
|
+
}
|
|
294
|
+
return findWorkTreeTop(absolute).top ?? absolute;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function wholeNumber(value , flag , min = 0) {
|
|
298
|
+
if (!given(value)) return null;
|
|
299
|
+
if (!Number.isInteger(value) || value < min) throw new TaskAttemptError('usage', `${flag} expects a whole number of at least ${min}, got '${value}'`);
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Synchronous wait for the IC-021 test-only seam; never reached from the CLI. */
|
|
304
|
+
function sleepSync(ms ) {
|
|
305
|
+
if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* IC-006 steps 3 to 12 against `store`, in the contract's order. Reads only: returns a refusal
|
|
310
|
+
* result, or `{ ok }` carrying what the write needs. For a plan without the header (DEC-008) steps
|
|
311
|
+
* 6, 8 and 9 do not apply and steps 3, 4, 5 and 12 answer exactly as before 080.
|
|
312
|
+
*/
|
|
313
|
+
function claimChecks(store , c ) {
|
|
314
|
+
const { plan, graphPlan, task, owner, override, key, epoch, reserveTokens } = c;
|
|
315
|
+
const refuse = (reason , extra = {}) => ({ refused: { disposition: 'refused', taskId: task, reason, ...extra } });
|
|
316
|
+
const target = plan.tasks.find((t ) => t.id === task);
|
|
317
|
+
if (!target) return refuse('unknown-task');
|
|
318
|
+
const record = derivePlan(plan, store).find((r ) => r.id === task);
|
|
319
|
+
if (record.state === 'SUCCEEDED') return refuse('already-done');
|
|
320
|
+
const active = activeAttempt(store.tasks[task]);
|
|
321
|
+
if (active) return refuse('active-attempt', { activeToken: active.token, owner: active.owner, startedAt: active.startedAt });
|
|
322
|
+
if (graphPlan) {
|
|
323
|
+
const worker = refuseWorker(key.identity);
|
|
324
|
+
if (worker) return refuse(worker.reason);
|
|
325
|
+
}
|
|
326
|
+
const run = runOf(store, key.runKey);
|
|
327
|
+
const control = controlRefusal(run, key.runKey);
|
|
328
|
+
if (control) { const { reason, ...extra } = control; return refuse(reason, extra); }
|
|
329
|
+
if (graphPlan) {
|
|
330
|
+
const stale = epochRefusal(run, key.runKey, epoch);
|
|
331
|
+
if (stale) { const { reason, ...extra } = stale; return refuse(reason, extra); }
|
|
332
|
+
}
|
|
333
|
+
const declared = plan.graph.tasks.find((t ) => t.id === task).owner;
|
|
334
|
+
let ownerOverrideRecord = null;
|
|
335
|
+
if (graphPlan && declared && normalizeOwner(owner) !== normalizeOwner(declared)) {
|
|
336
|
+
if (!override) return refuse('owner-mismatch', { declared });
|
|
337
|
+
ownerOverrideRecord = { declared, reason: override };
|
|
338
|
+
}
|
|
339
|
+
const { limit, used } = record.budget;
|
|
340
|
+
if (used >= limit) return refuse('budget-exhausted', { scope: 'task', limit, used });
|
|
341
|
+
if (record.state === 'CANCELLED') {
|
|
342
|
+
const cancelled = store.tasks[task].attempts.filter((a ) => a.state === 'cancelled')
|
|
343
|
+
.reduce((a , b ) => (b.token > a.token ? b : a));
|
|
344
|
+
return refuse('cancelled', { cancelledToken: cancelled.token, clear: 'resume' });
|
|
345
|
+
}
|
|
346
|
+
const readiness = record.readiness;
|
|
347
|
+
if (readiness.reason === 'dependency-held') return refuse('dependency-held', { held: readiness.held });
|
|
348
|
+
if (readiness.reason === 'unmet-dependency') return refuse('unmet-dependency', { unmet: readiness.unmet });
|
|
349
|
+
if (readiness.reason === 'unmet-phase') return refuse('unmet-phase', graphPlan ? { unmet: readiness.unmet, held: readiness.held } : { unmet: readiness.unmet });
|
|
350
|
+
const aggregate = aggregateRefusal(run, computeTotals(store, key.runKey), plan.graph, reserveTokens);
|
|
351
|
+
if (aggregate) { const { reason, ...extra } = aggregate; return refuse(reason, extra); }
|
|
352
|
+
return { ok: { target, ownerOverrideRecord } };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The claim's write (IC-006 "on success"): the attempt with IC-010's run fields, a legacy-plan run
|
|
357
|
+
* record created if absent (a graph plan's run exists, step 8), and the run's recorded graph
|
|
358
|
+
* (IC-004) and totals (IC-009) refreshed from the attempt list. A legacy-created run gets epoch 0:
|
|
359
|
+
* only start and an acknowledged resume issue an epoch (DEC-007).
|
|
360
|
+
*/
|
|
361
|
+
function claimWrite(store , c , ok ) {
|
|
362
|
+
const { plan, graphPlan, task, owner, mode, key, execWorktree, reserveTokens, now } = c;
|
|
363
|
+
const run = ensureRun(store, key, now);
|
|
364
|
+
const entry = entryFor(store, task);
|
|
365
|
+
const token = entry.nextToken;
|
|
366
|
+
entry.nextToken += 1;
|
|
367
|
+
const attempt = {
|
|
368
|
+
token, owner, mode, startedAt: now.toISOString(), taskLineHash: ok.target.hash,
|
|
369
|
+
state: 'active', result: null, evidenceId: null, reason: null,
|
|
370
|
+
...(ok.ownerOverrideRecord ? { ownerOverride: ok.ownerOverrideRecord } : {}),
|
|
371
|
+
runKey: key.runKey, epoch: run.epoch, execWorktree: execWorktree ?? run.worktree,
|
|
372
|
+
};
|
|
373
|
+
if (reserveTokens !== null) attempt.reserve = { tokens: reserveTokens };
|
|
374
|
+
attempt.sources = c.measured.sources;
|
|
375
|
+
attempt.sourcesTruncated = c.measured.sourcesTruncated;
|
|
376
|
+
entry.attempts.push(attempt);
|
|
377
|
+
store.planPath = plan.planPath;
|
|
378
|
+
refreshRun(store, run, plan.graph);
|
|
379
|
+
const claimed = { disposition: 'claimed', taskId: task, token, revision: store.revision };
|
|
380
|
+
// A legacy plan's result keeps the 8ce5ad7 shape (FR-015); the run fields are on the attempt.
|
|
381
|
+
// IC-013: the unhashable or unverifiable entries are listed once, only when there are any, so a
|
|
382
|
+
// claim whose files all hash keeps C.2's result shape.
|
|
383
|
+
if (graphPlan) {
|
|
384
|
+
Object.assign(claimed, { runKey: key.runKey, epoch: attempt.epoch, execWorktree: attempt.execWorktree });
|
|
385
|
+
if (c.measured.sourcesUnverifiable.length) claimed.sourcesUnverifiable = c.measured.sourcesUnverifiable;
|
|
386
|
+
}
|
|
387
|
+
return claimed;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* IC-006. Steps 0 to 2 before the lock (flags, graph, run key, execution worktree); steps 3 to 12
|
|
392
|
+
* and the write inside one `mutate()` against the store on disk, so every refusal writes nothing.
|
|
393
|
+
*
|
|
394
|
+
* `checkOutsideLock` and `delayAfterCheckMs` are the IC-021 test-only seam: the checks run against
|
|
395
|
+
* a plain read before the lock, then the claim waits, then writes without re-checking. It exists
|
|
396
|
+
* only so the race suite can prove it detects a missing lock; `handleTaskAttemptCommand` never
|
|
397
|
+
* passes it.
|
|
398
|
+
*/
|
|
399
|
+
function claim({
|
|
400
|
+
file, fsImpl, roots, slug, task, owner, mode, ownerOverride, epoch, reserveTokens, execWorktree,
|
|
401
|
+
env = process.env, cwd = process.cwd(), now = new Date(), checkOutsideLock = false, delayAfterCheckMs = 0,
|
|
402
|
+
} ) {
|
|
224
403
|
if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
|
|
225
404
|
if (!owner || !SAFE_NAME.test(owner)) throw new TaskAttemptError('usage', '--owner is required: the name of the agent or person taking the task');
|
|
226
405
|
if (!MODES.includes(mode)) throw new TaskAttemptError('usage', `--mode is required: one of ${MODES.join(', ')}`);
|
|
406
|
+
// DEC-019: --epoch is at least 1 in-process, as on the CLI; no run ever holds epoch 0 to match.
|
|
407
|
+
const epochGiven = wholeNumber(epoch, '--epoch', 1);
|
|
408
|
+
const reserve = wholeNumber(reserveTokens, '--reserve-tokens');
|
|
227
409
|
const plan = loadPlan({ roots, slug, fsImpl });
|
|
410
|
+
const graphPlan = isGraphPlan(plan.graph);
|
|
411
|
+
const override = graphPlan && given(ownerOverride) ? String(ownerOverride).trim() : null;
|
|
412
|
+
if (override === '') throw new TaskAttemptError('usage', '--owner-override needs a reason: why the declared owner is not taking the task');
|
|
413
|
+
const key = runKeyFor(env, cwd);
|
|
414
|
+
const exec = resolveExecWorktree(execWorktree, cwd, fsImpl);
|
|
415
|
+
// IC-013 / IC-006 step 2: hash the declared files before the lock, under the execution worktree
|
|
416
|
+
// the attempt will record (the flag, else the run's worktree, which is the key's top).
|
|
417
|
+
const declared = plan.graph.tasks.find((t ) => t.id === task);
|
|
418
|
+
const measured = measureSources({ files: declared ? declared.files : [] }, exec ?? key.top, roots);
|
|
419
|
+
const c = {
|
|
420
|
+
plan, graphPlan, task, owner, mode, override, key, epoch: epochGiven, reserveTokens: reserve,
|
|
421
|
+
execWorktree: exec, measured, now,
|
|
422
|
+
};
|
|
423
|
+
if (checkOutsideLock) {
|
|
424
|
+
const checked = claimChecks(readStore(file, fsImpl), c);
|
|
425
|
+
if (checked.refused) return checked.refused;
|
|
426
|
+
sleepSync(delayAfterCheckMs);
|
|
427
|
+
return mutate({ file, fsImpl, apply: (store ) => ({ write: true, result: claimWrite(store, c, checked.ok) }) });
|
|
428
|
+
}
|
|
228
429
|
return mutate({
|
|
229
430
|
file, fsImpl,
|
|
230
431
|
apply: (store ) => {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (doneAttempt(store.tasks[task])) return refuse('already-done');
|
|
235
|
-
const active = activeAttempt(store.tasks[task]);
|
|
236
|
-
if (active) return refuse('active-attempt', { activeToken: active.token, owner: active.owner, startedAt: active.startedAt });
|
|
237
|
-
const unmet = plan.tasks.filter((t ) => t.phaseIndex < target.phaseIndex && !doneAttempt(store.tasks[t.id])).map((t ) => t.id);
|
|
238
|
-
if (unmet.length) return refuse('unmet-phase', { unmet });
|
|
239
|
-
const entry = entryFor(store, task);
|
|
240
|
-
const token = entry.nextToken;
|
|
241
|
-
entry.nextToken += 1;
|
|
242
|
-
entry.attempts.push({
|
|
243
|
-
token, owner, mode, startedAt: now.toISOString(), taskLineHash: target.hash,
|
|
244
|
-
state: 'active', result: null, evidenceId: null, reason: null,
|
|
245
|
-
});
|
|
246
|
-
store.planPath = plan.planPath;
|
|
247
|
-
return { write: true, result: { disposition: 'claimed', taskId: task, token, revision: store.revision } };
|
|
432
|
+
const checked = claimChecks(store, c);
|
|
433
|
+
if (checked.refused) return { write: false, result: checked.refused };
|
|
434
|
+
return { write: true, result: claimWrite(store, c, checked.ok) };
|
|
248
435
|
},
|
|
249
436
|
});
|
|
250
437
|
}
|
|
@@ -253,13 +440,40 @@ function stale(taskId , token , reason ) {
|
|
|
253
440
|
return { write: false, result: { disposition: 'stale', taskId, token, reason } };
|
|
254
441
|
}
|
|
255
442
|
|
|
256
|
-
|
|
443
|
+
const given = (value ) => value !== undefined && value !== null;
|
|
444
|
+
|
|
445
|
+
/** IC-007: `tokens` and `basis` together or not at all (DEC-003: caller-reported, never metered). */
|
|
446
|
+
function readUsage(tokens , basis ) {
|
|
447
|
+
if (!given(tokens) && !given(basis)) return null;
|
|
448
|
+
if (given(tokens) !== given(basis)) throw new TaskAttemptError('usage', '--tokens and --tokens-basis are given together or not at all');
|
|
449
|
+
if (!Number.isInteger(tokens) || tokens < 0) throw new TaskAttemptError('usage', `--tokens expects a whole number of at least 0, got '${tokens}'`);
|
|
450
|
+
if (!USAGE_BASES.includes(basis)) throw new TaskAttemptError('usage', `--tokens-basis expects one of ${USAGE_BASES.join(', ')}, got '${basis}'`);
|
|
451
|
+
return { tokens, basis };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** The finisher's identity, read from the environment before the lock (IC-026). */
|
|
455
|
+
function callerIdentity(env , cwd ) {
|
|
456
|
+
const { resolveIdentity } = require('./context/identity.js');
|
|
457
|
+
return resolveIdentity(env, { cwd, sessionId: '' });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** IC-007 step 1: the attempt holding `token` was cancelled. Runs before every other check. */
|
|
461
|
+
function cancelledAttempt(entry , token ) {
|
|
462
|
+
return entry ? entry.attempts.find((a ) => a.token === token && a.state === 'cancelled') || null : null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function finish({ file, fsImpl, roots, slug, task, token, result, evidenceId, reason, tokens, basis, env = process.env, cwd = process.cwd(), now = new Date() } ) {
|
|
257
466
|
if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
|
|
258
467
|
if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the token claim returned');
|
|
259
468
|
if (!RESULTS.includes(result)) throw new TaskAttemptError('usage', `--result is required: one of ${RESULTS.join(', ')}`);
|
|
469
|
+
const why = given(reason) ? String(reason).trim() : '';
|
|
470
|
+
if (result === 'FAILED' && !why) throw new TaskAttemptError('usage', '--reason is required with --result FAILED: what failed');
|
|
471
|
+
const reported = readUsage(tokens, basis);
|
|
472
|
+
const identity = callerIdentity(env, cwd);
|
|
260
473
|
return mutate({
|
|
261
474
|
file, fsImpl,
|
|
262
475
|
apply: (store ) => {
|
|
476
|
+
if (cancelledAttempt(store.tasks[task], token)) return stale(task, token, 'attempt-cancelled');
|
|
263
477
|
const active = activeAttempt(store.tasks[task]);
|
|
264
478
|
if (!active) return stale(task, token, 'no-active-attempt');
|
|
265
479
|
if (active.token !== token) return stale(task, token, 'token-mismatch');
|
|
@@ -269,55 +483,270 @@ function finish({ file, fsImpl, roots, slug, task, token, result, evidenceId }
|
|
|
269
483
|
if (!line) return refuse('unknown-task');
|
|
270
484
|
if (line.hash !== active.taskLineHash) return refuse('plan-changed');
|
|
271
485
|
if (result === 'COMPLETED') {
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
486
|
+
const refusal = evidenceRefusal({ evidenceId, slug, taskId: task, startedAt: active.startedAt, roots, fsImpl });
|
|
487
|
+
if (refusal) return refuse(refusal);
|
|
274
488
|
}
|
|
275
489
|
active.state = 'finished';
|
|
276
490
|
active.result = result;
|
|
277
491
|
active.evidenceId = evidenceId || null;
|
|
492
|
+
if (why) active.reason = why;
|
|
493
|
+
active.finishedAt = now.toISOString();
|
|
494
|
+
active.finishedBy = { role: identity.role, lineage: identity.lineage, worktree: identity.worktree };
|
|
495
|
+
active.usage = reported ? { ...reported, reporter: `${identity.role}:${identity.lineage}` } : null;
|
|
496
|
+
// IC-009: every locked write stores totals recomputed from the attempt list.
|
|
497
|
+
const run = active.runKey ? runOf(store, active.runKey) : null;
|
|
498
|
+
if (run) refreshRun(store, run, plan.graph);
|
|
278
499
|
return { write: true, result: { disposition: 'applied', taskId: task, token, result, revision: store.revision } };
|
|
279
500
|
},
|
|
280
501
|
});
|
|
281
502
|
}
|
|
282
503
|
|
|
283
|
-
function release({ file, fsImpl, task, token, reason } ) {
|
|
504
|
+
function release({ file, fsImpl, roots, slug, task, token, reason, tokens, basis, env = process.env, cwd = process.cwd(), now = new Date() } ) {
|
|
284
505
|
if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
|
|
285
506
|
if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the token claim returned');
|
|
286
507
|
if (!reason || !String(reason).trim()) throw new TaskAttemptError('usage', '--reason is required: why the attempt is being released');
|
|
508
|
+
const reported = readUsage(tokens, basis);
|
|
509
|
+
const identity = reported ? callerIdentity(env, cwd) : null;
|
|
510
|
+
// DEC-020 release-without-plan: never a precondition; read before the lock, null when unloadable.
|
|
511
|
+
const plan = planOrNull({ roots, slug, fsImpl });
|
|
287
512
|
return mutate({
|
|
288
513
|
file, fsImpl,
|
|
289
514
|
apply: (store ) => {
|
|
515
|
+
if (cancelledAttempt(store.tasks[task], token)) return stale(task, token, 'attempt-cancelled');
|
|
290
516
|
const active = activeAttempt(store.tasks[task]);
|
|
291
517
|
if (!active) return stale(task, token, 'no-active-attempt');
|
|
292
518
|
if (active.token !== token) return stale(task, token, 'token-mismatch');
|
|
293
519
|
active.state = 'released';
|
|
294
520
|
active.reason = String(reason).trim();
|
|
521
|
+
active.endedAt = now.toISOString();
|
|
522
|
+
active.usage = reported ? { ...reported, reporter: `${identity.role}:${identity.lineage}` } : null;
|
|
523
|
+
// IC-009: every locked write stores totals recomputed from the attempt list. DEC-020: with an
|
|
524
|
+
// unloadable plan only the totals are refreshed; the recorded graph is kept.
|
|
525
|
+
const run = active.runKey ? runOf(store, active.runKey) : null;
|
|
526
|
+
if (run) refreshRunOrTotals(store, run, plan);
|
|
295
527
|
return { write: true, result: { disposition: 'released', taskId: task, token, revision: store.revision } };
|
|
296
528
|
},
|
|
297
529
|
});
|
|
298
530
|
}
|
|
299
531
|
|
|
300
|
-
/**
|
|
532
|
+
/** DEC-020 release-without-plan: the plan when it loads, else null for a malformed or unreadable
|
|
533
|
+
* one. Other failures (a store problem, a bug) still throw. */
|
|
534
|
+
function planOrNull(args ) {
|
|
535
|
+
try { return loadPlan(args); } catch (error ) {
|
|
536
|
+
if (error instanceof TaskAttemptError && (error.code === 'plan-malformed' || error.code === 'plan-unreadable')) return null;
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** IC-004 / IC-009 refresh with a loaded plan; with none (DEC-020), the totals only. */
|
|
542
|
+
function refreshRunOrTotals(store , run , plan ) {
|
|
543
|
+
if (plan) refreshRun(store, run, plan.graph);
|
|
544
|
+
else run.totals = computeTotals(store, run.runKey);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* DEC-018 unreported-usage-recovery: `report-usage` writes caller-reported usage onto an ended
|
|
549
|
+
* attempt that has none, so a run under a token limit that DEC-011 refuses as
|
|
550
|
+
* `aggregate-unaccounted` can recover. Any plan; fenced by the token alone (like finish, IC-026),
|
|
551
|
+
* so a worker may report. Never overwrites. The owning run's totals are refreshed in the same
|
|
552
|
+
* locked write; like release (DEC-020) an unloadable plan refreshes totals only.
|
|
553
|
+
*/
|
|
554
|
+
function reportUsage({ file, fsImpl, roots, slug, task, token, tokens, basis, env = process.env, cwd = process.cwd() } ) {
|
|
555
|
+
if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
|
|
556
|
+
if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the token of the ended attempt');
|
|
557
|
+
if (!given(tokens) || !given(basis)) throw new TaskAttemptError('usage', '--tokens and --tokens-basis are required: the usage to record');
|
|
558
|
+
const reported = readUsage(tokens, basis);
|
|
559
|
+
const identity = callerIdentity(env, cwd);
|
|
560
|
+
const plan = planOrNull({ roots, slug, fsImpl });
|
|
561
|
+
return mutate({
|
|
562
|
+
file, fsImpl,
|
|
563
|
+
apply: (store ) => {
|
|
564
|
+
const refuse = (reasonCode ) => ({ write: false, result: { disposition: 'refused', taskId: task, token, reason: reasonCode } });
|
|
565
|
+
const entry = Object.prototype.hasOwnProperty.call(store.tasks, task) ? store.tasks[task] : null;
|
|
566
|
+
// A task the plan no longer lists may still hold an unreported attempt, so the ledger counts too.
|
|
567
|
+
const known = plan ? plan.tasks.some((t ) => t.id === task) : false;
|
|
568
|
+
if (!known && !entry) return refuse('unknown-task');
|
|
569
|
+
const attempt = entry ? entry.attempts.find((a ) => a.token === token) || null : null;
|
|
570
|
+
if (!attempt) return stale(task, token, 'no-attempt');
|
|
571
|
+
if (attempt.state === 'active') return refuse('active-attempt');
|
|
572
|
+
if (attempt.state === 'cancelled') return stale(task, token, 'attempt-cancelled');
|
|
573
|
+
if (attempt.usage) return refuse('usage-present');
|
|
574
|
+
attempt.usage = { ...reported, reporter: `${identity.role}:${identity.lineage}` };
|
|
575
|
+
const run = attempt.runKey ? runOf(store, attempt.runKey) : null;
|
|
576
|
+
if (run) refreshRunOrTotals(store, run, plan);
|
|
577
|
+
return { write: true, result: { disposition: 'usage-recorded', taskId: task, token, revision: store.revision } };
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ── run controls and reopen (IC-012, IC-024, IC-026; DEC-005, DEC-009) ──────────────────────────
|
|
583
|
+
//
|
|
584
|
+
// Flags, the plan and the run key are resolved before the lock; the decision and the write are one
|
|
585
|
+
// `mutate()`. Each identity, control and token refusal returns `write: false`.
|
|
586
|
+
|
|
587
|
+
function requireBy(by ) {
|
|
588
|
+
if (!by || !SAFE_NAME.test(String(by))) throw new TaskAttemptError('usage', '--by is required: the name of the agent or person acting on the run');
|
|
589
|
+
return String(by);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function requireReason(reason , what ) {
|
|
593
|
+
const why = given(reason) ? String(reason).trim() : '';
|
|
594
|
+
if (!why) throw new TaskAttemptError('usage', `--reason is required: ${what}`);
|
|
595
|
+
return why;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function requireTaskToken(task , token ) {
|
|
599
|
+
if (!TASK_ID.test(task || '')) throw new TaskAttemptError('usage', '--task is required: a plan task id such as A.1');
|
|
600
|
+
if (!Number.isInteger(token) || token < 1) throw new TaskAttemptError('usage', '--token is required: the attempt token');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function budgetLimit(value , flag ) {
|
|
604
|
+
if (!given(value)) return null;
|
|
605
|
+
if (!Number.isInteger(value) || value < 1) throw new TaskAttemptError('usage', `${flag} expects a whole number of at least 1, got '${value}'`);
|
|
606
|
+
return value;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** IC-012 `start`: the caller's run with `epoch: 1`, or `run-exists`. */
|
|
610
|
+
function start({ file, fsImpl, roots, slug, by, budgetAttempts, budgetTokens, env = process.env, cwd = process.cwd(), now = new Date() } ) {
|
|
611
|
+
const who = requireBy(by);
|
|
612
|
+
const budget = { attempts: budgetLimit(budgetAttempts, '--budget-attempts'), tokens: budgetLimit(budgetTokens, '--budget-tokens') };
|
|
613
|
+
const plan = loadPlan({ roots, slug, fsImpl });
|
|
614
|
+
const key = runKeyFor(env, cwd);
|
|
615
|
+
return mutate({ file, fsImpl, apply: (store ) => startRun(store, { key, graph: plan.graph, by: who, budget, now }) });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** IC-012 `pause`: claims refuse until an acknowledged resume; active attempts still finish. */
|
|
619
|
+
function pause({ file, fsImpl, roots, slug, by, reason, env = process.env, cwd = process.cwd(), now = new Date() } ) {
|
|
620
|
+
const who = requireBy(by);
|
|
621
|
+
const why = requireReason(reason, 'why the run is being paused');
|
|
622
|
+
const plan = loadPlan({ roots, slug, fsImpl });
|
|
623
|
+
const key = runKeyFor(env, cwd);
|
|
624
|
+
return mutate({ file, fsImpl, apply: (store ) => pauseRun(store, { key, graph: plan.graph, by: who, reason: why, now }) });
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** IC-012 `cancel`: one attempt when `task` or `token` is given, else the whole run. Marks
|
|
628
|
+
* attempts only; ending a worker process is 081's. */
|
|
629
|
+
function cancel({ file, fsImpl, roots, slug, task, token, by, reason, adoptUnowned = false, env = process.env, cwd = process.cwd(), now = new Date() } ) {
|
|
630
|
+
const who = requireBy(by);
|
|
631
|
+
const why = requireReason(reason, 'why the run or attempt is being cancelled');
|
|
632
|
+
const single = given(task) || given(token);
|
|
633
|
+
if (single) {
|
|
634
|
+
requireTaskToken(task, token);
|
|
635
|
+
if (adoptUnowned) throw new TaskAttemptError('usage', '--adopt-unowned applies to a run cancel, not to one attempt');
|
|
636
|
+
}
|
|
637
|
+
const plan = loadPlan({ roots, slug, fsImpl });
|
|
638
|
+
const key = runKeyFor(env, cwd);
|
|
639
|
+
return mutate({
|
|
640
|
+
file, fsImpl,
|
|
641
|
+
apply: (store ) => (single
|
|
642
|
+
? cancelAttempt(store, { key, graph: plan.graph, task, token, by: who, reason: why, now })
|
|
643
|
+
: cancelRun(store, { key, graph: plan.graph, by: who, reason: why, adoptUnowned: Boolean(adoptUnowned), now })),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Task ids that depend on `taskId`, directly or through another dependent: an explicit
|
|
648
|
+
* `depends-on:` naming it, or no `depends-on:` and a later phase. */
|
|
649
|
+
function dependentsOf(graph , taskId ) {
|
|
650
|
+
const found = new Set ();
|
|
651
|
+
const queue = [taskId];
|
|
652
|
+
while (queue.length) {
|
|
653
|
+
const id = queue.shift() ;
|
|
654
|
+
const of = graph.tasks.find((t ) => t.id === id);
|
|
655
|
+
for (const t of graph.tasks) {
|
|
656
|
+
if (found.has(t.id) || t.id === taskId) continue;
|
|
657
|
+
const waits = t.taskDeps.length ? t.taskDeps.includes(id) : t.phaseIndex > of.phaseIndex;
|
|
658
|
+
if (waits) { found.add(t.id); queue.push(t.id); }
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return graph.tasks.filter((t ) => found.has(t.id)).map((t ) => t.id);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* IC-024 / DEC-009: the task's done attempt (stale or not) becomes `superseded`, so the task is no
|
|
666
|
+
* longer done and the attempt counts toward the task budget (IC-008). Available in every plan.
|
|
667
|
+
* `dependentsActive` names active attempts of dependent tasks; the caller decides about them.
|
|
668
|
+
*/
|
|
669
|
+
function reopen({ file, fsImpl, roots, slug, task, token, by, reason, env = process.env, cwd = process.cwd(), now = new Date() } ) {
|
|
670
|
+
requireTaskToken(task, token);
|
|
671
|
+
const who = requireBy(by);
|
|
672
|
+
const why = requireReason(reason, 'why the done task is being reopened');
|
|
673
|
+
const plan = loadPlan({ roots, slug, fsImpl });
|
|
674
|
+
const key = runKeyFor(env, cwd);
|
|
675
|
+
return mutate({
|
|
676
|
+
file, fsImpl,
|
|
677
|
+
apply: (store ) => {
|
|
678
|
+
const refuse = (reasonCode , extra = {}) => ({ write: false, result: { disposition: 'refused', taskId: task, token, reason: reasonCode, ...extra } });
|
|
679
|
+
const identity = identityRefusal(key.identity);
|
|
680
|
+
if (identity) return refuse(identity.reason);
|
|
681
|
+
if (!plan.tasks.some((t ) => t.id === task)) return refuse('unknown-task');
|
|
682
|
+
const done = doneAttempt(store.tasks[task]);
|
|
683
|
+
if (!done) return refuse('not-done');
|
|
684
|
+
if (done.token !== token) return refuse('token-mismatch', { doneToken: done.token });
|
|
685
|
+
const existing = runOf(store, key.runKey);
|
|
686
|
+
if (existing && existing.control === 'cancelled') return refuse('run-cancelled', { runKey: key.runKey });
|
|
687
|
+
const run = ensureRun(store, key, now);
|
|
688
|
+
const at = now.toISOString();
|
|
689
|
+
done.state = 'superseded';
|
|
690
|
+
done.reopen = { at, by: who, reason: why, runKey: key.runKey };
|
|
691
|
+
run.history.push({ at, action: 'reopen', by: who, reason: why, taskId: task, token });
|
|
692
|
+
refreshRun(store, run, plan.graph);
|
|
693
|
+
const dependentsActive = dependentsOf(plan.graph, task)
|
|
694
|
+
.map((id ) => ({ id, active: activeAttempt(store.tasks[id]) }))
|
|
695
|
+
.filter((d ) => d.active)
|
|
696
|
+
.map((d ) => ({ taskId: d.id, token: d.active.token }));
|
|
697
|
+
return { write: true, result: { disposition: 'reopened', taskId: task, token, dependentsActive } };
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const ACK = /^sha256:[0-9a-f]{64}$/;
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* IC-015 `resume`: the report and its digest, and with `ack` the acknowledged resume. Flags and
|
|
706
|
+
* the plan are checked first; `resumeRun` refuses on identity, runs the probes before any lock,
|
|
707
|
+
* and with `ack` makes one `mutate()`. Without `ack` the store is read with `readTaskState` and
|
|
708
|
+
* nothing is written (DEC-004). `by` is required with `ack`; `reason` is optional, non-empty when
|
|
709
|
+
* given. `gitRunner(args, cwd) => stdout | null` replaces the git process (tests).
|
|
710
|
+
*/
|
|
711
|
+
function resume({ file, fsImpl, roots, slug, ack, by, reason, env = process.env, cwd = process.cwd(), now = new Date(), gitRunner = null } ) {
|
|
712
|
+
const acknowledged = given(ack) ? String(ack) : null;
|
|
713
|
+
if (acknowledged !== null && !ACK.test(acknowledged)) throw new TaskAttemptError('usage', `--ack expects the digest resume reported (sha256:<64 hex>), got '${ack}'`);
|
|
714
|
+
const who = acknowledged !== null ? requireBy(by) : (given(by) ? requireBy(by) : null);
|
|
715
|
+
const why = given(reason) ? requireReason(reason, 'why the run is being resumed') : null;
|
|
716
|
+
const plan = loadPlan({ roots, slug, fsImpl });
|
|
717
|
+
const key = runKeyFor(env, cwd);
|
|
718
|
+
return resumeRun(
|
|
719
|
+
{ read: () => readStore(file, fsImpl), mutate: (apply ) => mutate({ file, fsImpl, apply }) },
|
|
720
|
+
{ key, graph: plan.graph, hashes: planHashes(plan), roots, ack: acknowledged, by: who, reason: why, now, env, gitRunner },
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* IC-016 `inspect`: the plan's graph, every task's derived state, budgets with their source, and
|
|
726
|
+
* the caller's run with recomputed totals and `totalsMismatch`. Read-only: the store is read with
|
|
727
|
+
* `readStore` (`readTaskState`), no lock is taken, nothing is written and no git process runs.
|
|
728
|
+
*/
|
|
729
|
+
function inspect({ file, fsImpl, roots, slug, env = process.env, cwd = process.cwd() } ) {
|
|
730
|
+
const plan = loadPlan({ roots, slug, fsImpl });
|
|
731
|
+
const key = runKeyFor(env, cwd);
|
|
732
|
+
const store = readStore(file, fsImpl);
|
|
733
|
+
return inspectRun(store, { slug, key, graph: plan.graph, hashes: planHashes(plan), warnings: plan.warnings });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Completion read from the ledger and the plan, never from checkboxes (FR-005). Each task keeps
|
|
737
|
+
* the pre-080 fields and adds IC-005's `state`, `reason`, `claimable`, `budget` and, where
|
|
738
|
+
* present, `held`, `unmet` and `doneStale`. */
|
|
301
739
|
function status({ file, fsImpl, roots, slug } ) {
|
|
302
740
|
const plan = loadPlan({ roots, slug, fsImpl });
|
|
303
741
|
const store = readStore(file, fsImpl);
|
|
304
|
-
const tasks = plan.
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
let derived;
|
|
310
|
-
if (done) derived = 'done';
|
|
311
|
-
else if (active) derived = 'running';
|
|
312
|
-
else if (last && last.result === 'BLOCKED') derived = 'blocked';
|
|
313
|
-
else if (plan.tasks.every((o ) => o.phaseIndex >= t.phaseIndex || doneAttempt(store.tasks[o.id]))) derived = 'ready';
|
|
314
|
-
else derived = 'pending';
|
|
315
|
-
return {
|
|
316
|
-
id: t.id, phase: t.phase, derived,
|
|
317
|
-
activeToken: active ? active.token : null,
|
|
318
|
-
doneToken: done ? done.token : null,
|
|
319
|
-
active: active ? { owner: active.owner, mode: active.mode, startedAt: active.startedAt } : null,
|
|
742
|
+
const tasks = derivePlan(plan, store).map((r ) => {
|
|
743
|
+
const out = {
|
|
744
|
+
id: r.id, phase: r.phase, derived: r.derived,
|
|
745
|
+
activeToken: r.activeToken, doneToken: r.doneToken, active: r.active,
|
|
746
|
+
state: r.state, reason: r.reason, claimable: r.claimable, budget: r.budget,
|
|
320
747
|
};
|
|
748
|
+
for (const key of ['held', 'unmet', 'doneStale']) if (r[key] !== undefined) out[key] = r[key];
|
|
749
|
+
return out;
|
|
321
750
|
});
|
|
322
751
|
return {
|
|
323
752
|
complete: tasks.length > 0 && tasks.every((t ) => t.derived === 'done'),
|
|
@@ -340,14 +769,20 @@ function checkCompletion({ slug, cwd, fsImpl = fs } ) {
|
|
|
340
769
|
if (!fsImpl.existsSync(file)) return { state: 'none', slug, incomplete: [] };
|
|
341
770
|
const answer = status({ file, fsImpl, roots: rootsFor({ cwd, lifecycleRoot }), slug });
|
|
342
771
|
const incomplete = answer.tasks.filter((t ) => t.derived !== 'done').map((t ) => t.id);
|
|
772
|
+
// IC-024: only a graph plan has done-stale tasks (DEC-009); a legacy answer keeps its shape.
|
|
773
|
+
const stale = answer.tasks.filter((t ) => t.doneStale);
|
|
774
|
+
if (stale.length) {
|
|
775
|
+
const reasons = Object.fromEntries(stale.map((t ) => [t.id, 'plan-changed-after-done']));
|
|
776
|
+
return { state: 'incomplete', slug, incomplete, reasons };
|
|
777
|
+
}
|
|
343
778
|
return { state: answer.complete ? 'complete' : 'incomplete', slug, incomplete };
|
|
344
779
|
}
|
|
345
780
|
|
|
346
781
|
// ── CLI ────────────────────────────────────────────────────────────────────────────────────────
|
|
347
782
|
|
|
348
|
-
function failure(code , message , json ) {
|
|
783
|
+
function failure(code , message , json , problems ) {
|
|
349
784
|
const { finishRuntime } = require('./cli-result.js');
|
|
350
|
-
if (json) console.log(JSON.stringify({ ok: false, error: code, message }, null, 2));
|
|
785
|
+
if (json) console.log(JSON.stringify(problems ? { ok: false, error: code, message, problems } : { ok: false, error: code, message }, null, 2));
|
|
351
786
|
else console.error(`doflow task-attempt: ${message}`);
|
|
352
787
|
return finishRuntime(2);
|
|
353
788
|
}
|
|
@@ -357,23 +792,89 @@ function describe(result ) {
|
|
|
357
792
|
if (result.taskId) parts.push(`task ${result.taskId}`);
|
|
358
793
|
if (result.token !== undefined) parts.push(`token ${result.token}`);
|
|
359
794
|
if (result.reason) parts.push(`(${result.reason})`);
|
|
795
|
+
if (result.disposition === 'ack-required' || result.disposition === 'resumed' || result.reason === 'report-changed') {
|
|
796
|
+
if (result.digest) parts.push(`digest ${result.digest}`);
|
|
797
|
+
}
|
|
798
|
+
if (['started', 'resumed'].includes(result.disposition) && given(result.epoch)) parts.push(`epoch ${result.epoch}`);
|
|
360
799
|
return parts.join(' ');
|
|
361
800
|
}
|
|
362
801
|
|
|
802
|
+
/** IC-017: the dispositions that exit 0. `status` and `inspect` are reads and always exit 0. */
|
|
803
|
+
const APPLIED = Object.freeze(['claimed', 'applied', 'released', 'started', 'paused', 'cancelled', 'resumed', 'reopened', 'usage-recorded']);
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* IC-017: the actions each flag may be given to. The 080 flags are refused on every other action;
|
|
807
|
+
* the pre-080 flags are refused only on the 080 actions, so every existing invocation keeps its
|
|
808
|
+
* exit code (NFR-001).
|
|
809
|
+
*/
|
|
810
|
+
const FLAG_ACTIONS = {
|
|
811
|
+
ownerOverride: { flag: '--owner-override', actions: ['claim'] },
|
|
812
|
+
epoch: { flag: '--epoch', actions: ['claim'] },
|
|
813
|
+
execWorktree: { flag: '--exec-worktree', actions: ['claim'] },
|
|
814
|
+
reserveTokens: { flag: '--reserve-tokens', actions: ['claim'] },
|
|
815
|
+
tokens: { flag: '--tokens', actions: ['finish', 'release', 'report-usage'] },
|
|
816
|
+
tokensBasis: { flag: '--tokens-basis', actions: ['finish', 'release', 'report-usage'] },
|
|
817
|
+
by: { flag: '--by', actions: ['start', 'pause', 'cancel', 'reopen', 'resume'] },
|
|
818
|
+
budgetAttempts: { flag: '--budget-attempts', actions: ['start'] },
|
|
819
|
+
budgetTokens: { flag: '--budget-tokens', actions: ['start'] },
|
|
820
|
+
ack: { flag: '--ack', actions: ['resume'] },
|
|
821
|
+
adoptUnowned: { flag: '--adopt-unowned', actions: ['cancel'] },
|
|
822
|
+
};
|
|
823
|
+
const PRE_080_ACTIONS = Object.freeze(['claim', 'finish', 'release', 'status']);
|
|
824
|
+
const PRE_080_FLAG_ACTIONS = {
|
|
825
|
+
task: { flag: '--task', actions: ['cancel', 'reopen', 'report-usage'] },
|
|
826
|
+
token: { flag: '--token', actions: ['cancel', 'reopen', 'report-usage'] },
|
|
827
|
+
reason: { flag: '--reason', actions: ['pause', 'cancel', 'reopen', 'resume'] },
|
|
828
|
+
owner: { flag: '--owner', actions: [] },
|
|
829
|
+
mode: { flag: '--mode', actions: [] },
|
|
830
|
+
result: { flag: '--result', actions: [] },
|
|
831
|
+
evidenceId: { flag: '--evidence-id', actions: [] },
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
function refuseMisplacedFlags(action , flags ) {
|
|
835
|
+
const check = (table ) => {
|
|
836
|
+
for (const [key, { flag, actions }] of Object.entries(table)) {
|
|
837
|
+
if (given(flags[key]) && flags[key] !== false && !actions.includes(action)) {
|
|
838
|
+
throw new TaskAttemptError('usage', `${flag} does not apply to --action ${action}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
check(FLAG_ACTIONS);
|
|
843
|
+
if (!PRE_080_ACTIONS.includes(action)) check(PRE_080_FLAG_ACTIONS);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** IC-017: a CLI integer is digits only, so `5abc`, `1.5` and `-1` are usage rather than whatever
|
|
847
|
+
* `parseInt` would make of them. A number passed in-process is checked as its decimal form. */
|
|
848
|
+
function digits(value , flag , min ) {
|
|
849
|
+
if (!given(value)) return undefined;
|
|
850
|
+
const text = String(value);
|
|
851
|
+
if (!/^[0-9]+$/.test(text) || Number(text) > Number.MAX_SAFE_INTEGER || Number(text) < min) {
|
|
852
|
+
throw new TaskAttemptError('usage', `${flag} expects a whole number of at least ${min}, got '${value}'`);
|
|
853
|
+
}
|
|
854
|
+
return Number(text);
|
|
855
|
+
}
|
|
856
|
+
|
|
363
857
|
/**
|
|
364
|
-
* `doflow task-attempt --action
|
|
365
|
-
* or
|
|
858
|
+
* `doflow task-attempt --action <action> --slug <slug>` (IC-017). Exit 0 for the applied
|
|
859
|
+
* dispositions and every `status` or `inspect` read; 1 refused, stale or ack-required, with the
|
|
860
|
+
* disposition in the JSON; 2 malformed input or an unreadable store or plan.
|
|
366
861
|
* @param {Object} options
|
|
367
862
|
* @param {string} options.cwd the invoking checkout
|
|
368
863
|
* @param {(cwd: string) => string|null} [options.resolveSlug] fallback when `slug` is not given
|
|
369
864
|
*/
|
|
370
865
|
function handleTaskAttemptCommand({
|
|
371
866
|
action = 'status', slug = null, task, owner, mode, token, result, evidenceId, reason, json = false,
|
|
867
|
+
ownerOverride, epoch, reserveTokens, execWorktree, tokens, tokensBasis, budgetAttempts, budgetTokens, by, ack,
|
|
868
|
+
adoptUnowned = false, env = process.env,
|
|
372
869
|
cwd = process.cwd(), resolveSlug = null, fsImpl = fs, now = new Date(),
|
|
373
870
|
} = {}) {
|
|
374
871
|
const { finishRuntime } = require('./cli-result.js');
|
|
375
872
|
try {
|
|
376
873
|
if (!ACTIONS.includes(action)) throw new TaskAttemptError('usage', `unknown action '${action}'; valid: ${ACTIONS.join(' | ')}`);
|
|
874
|
+
refuseMisplacedFlags(action, {
|
|
875
|
+
ownerOverride, epoch, reserveTokens, execWorktree, tokens, tokensBasis, budgetAttempts, budgetTokens, by, ack, adoptUnowned,
|
|
876
|
+
task, token, reason, owner, mode, result, evidenceId,
|
|
877
|
+
});
|
|
377
878
|
const feature = slug || (resolveSlug ? resolveSlug(cwd) : null);
|
|
378
879
|
if (!feature) throw new TaskAttemptError('usage', '--slug is required: no feature could be resolved from the current branch');
|
|
379
880
|
assertSlug(feature);
|
|
@@ -382,34 +883,53 @@ function handleTaskAttemptCommand({
|
|
|
382
883
|
if (!/^[0-9]+$/.test(String(token))) throw new TaskAttemptError('usage', `--token expects the whole number claim returned, got '${token}'`);
|
|
383
884
|
tokenNumber = Number(token);
|
|
384
885
|
}
|
|
886
|
+
const epochNumber = digits(epoch, '--epoch', 1);
|
|
887
|
+
const reserveNumber = digits(reserveTokens, '--reserve-tokens', 0);
|
|
888
|
+
const tokensNumber = digits(tokens, '--tokens', 0);
|
|
889
|
+
const attemptsLimit = digits(budgetAttempts, '--budget-attempts', 1);
|
|
890
|
+
const tokensLimit = digits(budgetTokens, '--budget-tokens', 1);
|
|
891
|
+
if (given(ownerOverride) && !String(ownerOverride).trim()) throw new TaskAttemptError('usage', '--owner-override needs a reason: why the declared owner is not taking the task');
|
|
385
892
|
const lifecycleRoot = lifecycleProjectRoot(cwd);
|
|
386
893
|
const file = storeFile(lifecycleRoot, feature);
|
|
387
894
|
const roots = rootsFor({ cwd, lifecycleRoot });
|
|
388
|
-
const common = { file, fsImpl, roots, slug: feature };
|
|
895
|
+
const common = { file, fsImpl, roots, slug: feature, env, cwd, now };
|
|
896
|
+
const usage = { tokens: tokensNumber, basis: tokensBasis };
|
|
389
897
|
|
|
390
898
|
let answer;
|
|
391
899
|
switch (action) {
|
|
392
|
-
case 'claim': answer = claim({ ...common, task, owner, mode,
|
|
393
|
-
case 'finish': answer = finish({ ...common, task, token: tokenNumber, result, evidenceId }); break;
|
|
394
|
-
case 'release': answer = release({ ...common, task, token: tokenNumber, reason }); break;
|
|
900
|
+
case 'claim': answer = claim({ ...common, task, owner, mode, ownerOverride, epoch: epochNumber, reserveTokens: reserveNumber, execWorktree }); break;
|
|
901
|
+
case 'finish': answer = finish({ ...common, task, token: tokenNumber, result, evidenceId, reason, ...usage }); break;
|
|
902
|
+
case 'release': answer = release({ ...common, task, token: tokenNumber, reason, ...usage }); break;
|
|
903
|
+
case 'start': answer = start({ ...common, by, budgetAttempts: attemptsLimit, budgetTokens: tokensLimit }); break;
|
|
904
|
+
case 'pause': answer = pause({ ...common, by, reason }); break;
|
|
905
|
+
case 'cancel': answer = cancel({ ...common, task, token: tokenNumber, by, reason, adoptUnowned: Boolean(adoptUnowned) }); break;
|
|
906
|
+
case 'reopen': answer = reopen({ ...common, task, token: tokenNumber, by, reason }); break;
|
|
907
|
+
case 'report-usage': answer = reportUsage({ ...common, task, token: tokenNumber, ...usage }); break;
|
|
908
|
+
case 'resume': answer = resume({ ...common, ack, by, reason }); break;
|
|
909
|
+
case 'inspect': answer = inspect(common); break;
|
|
395
910
|
default: answer = status(common);
|
|
396
911
|
}
|
|
397
912
|
|
|
398
|
-
const
|
|
913
|
+
const read = action === 'status' || action === 'inspect';
|
|
914
|
+
const ok = read || APPLIED.includes(answer.disposition);
|
|
399
915
|
if (json) console.log(JSON.stringify(answer, null, 2));
|
|
400
916
|
else if (action === 'status') {
|
|
401
917
|
console.log(`Task attempts for ${feature}: ${answer.complete ? 'complete' : 'incomplete'}`);
|
|
402
918
|
for (const t of answer.tasks) console.log(` ${t.id} ${t.derived}${t.activeToken ? ` (token ${t.activeToken}, ${t.active.owner} since ${t.active.startedAt})` : ''}`);
|
|
403
919
|
if (answer.unclaimedChecked.length) console.log(`Checked in plan.md with no COMPLETED attempt: ${answer.unclaimedChecked.join(', ')}`);
|
|
920
|
+
} else if (action === 'inspect') {
|
|
921
|
+
const { formatInspect } = require('./task-run.js');
|
|
922
|
+
for (const line of formatInspect(answer)) console.log(line);
|
|
404
923
|
} else (ok ? console.log : console.error)(describe(answer));
|
|
405
924
|
return finishRuntime(ok ? 0 : 1);
|
|
406
925
|
} catch (error ) {
|
|
407
|
-
if (error instanceof TaskAttemptError) return failure(error.code, error.message, json);
|
|
926
|
+
if (error instanceof TaskAttemptError) return failure(error.code, error.message, json, error.problems);
|
|
408
927
|
throw error;
|
|
409
928
|
}
|
|
410
929
|
}
|
|
411
930
|
|
|
412
931
|
module.exports = {
|
|
413
|
-
handleTaskAttemptCommand, checkCompletion, claim, finish, release, status,
|
|
414
|
-
|
|
932
|
+
handleTaskAttemptCommand, checkCompletion, claim, finish, release, status, start, pause, cancel, reopen, resume, inspect, reportUsage,
|
|
933
|
+
loadPlan, taskLineHash,
|
|
934
|
+
storeFile, TaskAttemptError, MODES, RESULTS, ACTIONS, USAGE_BASES, RUN_CONTROLS, activeAttempt,
|
|
415
935
|
};
|