@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
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handoff and result envelopes (feature 082, IC-010, IC-011, IC-012).
|
|
7
|
+
*
|
|
8
|
+
* - `buildHandoff` and `buildResult` are pure: every list arrives as input, so no git call, file
|
|
9
|
+
* read or environment read happens here (DEC-005). Neither envelope carries a transcript, an
|
|
10
|
+
* environment value or a verification argv (NFR-006).
|
|
11
|
+
* - `undeclared` is the changed paths, rename and copy sources included, outside the task's
|
|
12
|
+
* `files:` after trimming and dropping a leading `./` (DEC-033). The normalization is this
|
|
13
|
+
* module's own copy of `task-graph.ts`'s private `normalizeFile`, so that 080 file stays
|
|
14
|
+
* byte-identical (DEC-059).
|
|
15
|
+
* - `writeEnvelope` writes canonical JSON (keys sorted, no whitespace), so the file's byte size is
|
|
16
|
+
* the bound it checks: 16384 for a handoff, 32768 for a result (DEC-027). Over the bound, lists
|
|
17
|
+
* are cut in IC-012's fixed order, each to the longest prefix that fits, the tail going to a
|
|
18
|
+
* sidecar named by an `overflow` entry; then `task.text` or `summary` is cut on a UTF-8
|
|
19
|
+
* boundary. Every write is a temporary file in the same directory, then a rename.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const crypto = require('node:crypto');
|
|
25
|
+
const { maskLine } = require('../mask.js');
|
|
26
|
+
|
|
27
|
+
const ENVELOPE_DIR = 'dispatch-envelopes';
|
|
28
|
+
const BOUNDS = { handoff: 16384, result: 32768 };
|
|
29
|
+
/** IC-012: the cut order, each list cut to its longest fitting prefix before the next is touched. */
|
|
30
|
+
const CUT_ORDER = {
|
|
31
|
+
handoff: ['sources', 'deltas.changes', 'acceptance'],
|
|
32
|
+
result: ['changes', 'undeclared', 'evidence', 'blockers'],
|
|
33
|
+
};
|
|
34
|
+
const TEXT_BYTES = 2000;
|
|
35
|
+
const CUT_MARK = '[cut]';
|
|
36
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)*$/;
|
|
37
|
+
|
|
38
|
+
/** IC-010: the fixed loop rules every handoff begins with, in this order. */
|
|
39
|
+
const FIXED_CONSTRAINTS = [
|
|
40
|
+
'edit only inside worktree.path',
|
|
41
|
+
'leave worktree.branch checked out',
|
|
42
|
+
'do not push, fetch or change git config',
|
|
43
|
+
'write nothing outside the worktree',
|
|
44
|
+
'spawn children only through dispatch --action spawn within grant',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** IC-012: JSON with object keys sorted at every level and no whitespace; 080's `canonicalJson`
|
|
48
|
+
* (`task-run.ts`, not exported), copied. */
|
|
49
|
+
function canonical(value ) {
|
|
50
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
51
|
+
if (value && typeof value === 'object') {
|
|
52
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
|
|
53
|
+
}
|
|
54
|
+
return JSON.stringify(value === undefined ? null : value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** DEC-059: a `files:` entry compared after trimming and dropping a leading `./` (copy of
|
|
58
|
+
* `task-graph.ts`'s module-private `normalizeFile`). */
|
|
59
|
+
function normalizeFile(file ) {
|
|
60
|
+
return String(file).trim().replace(/^(\.\/)+/, '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The longest prefix of `text` that is at most `maxBytes` UTF-8 bytes and ends on a character. */
|
|
64
|
+
function cutUtf8(text , maxBytes ) {
|
|
65
|
+
const buf = Buffer.from(String(text), 'utf8');
|
|
66
|
+
if (buf.length <= maxBytes) return String(text);
|
|
67
|
+
let end = Math.max(0, maxBytes);
|
|
68
|
+
while (end > 0 && (buf[end] & 0xc0) === 0x80) end -= 1;
|
|
69
|
+
return buf.subarray(0, end).toString('utf8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function bytesOf(value ) {
|
|
73
|
+
return Buffer.byteLength(canonical(value), 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** IC-010: the repository-relative artifact whose index carries an acceptance id. */
|
|
77
|
+
function acceptanceSource(slug , id ) {
|
|
78
|
+
const base = `agent-docs/doflow/${slug}`;
|
|
79
|
+
return /^IC-/.test(id) ? `${base}/design/specs.md` : `${base}/intention/requirement.md`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** IC-010. `input.acceptance` is the task's acceptance ids; every other list is taken as given. */
|
|
83
|
+
function buildHandoff(input ) {
|
|
84
|
+
const task = input.task || {};
|
|
85
|
+
const files = Array.isArray(task.files) ? task.files.map(String) : [];
|
|
86
|
+
const writeSet = files.length ? `files: ${files.join(', ')}` : 'files: none declared';
|
|
87
|
+
return {
|
|
88
|
+
schema: 'doflow-handoff-v1',
|
|
89
|
+
dispatchId: input.dispatchId,
|
|
90
|
+
dispatchRunId: input.dispatchRunId,
|
|
91
|
+
slug: input.slug,
|
|
92
|
+
mode: input.mode,
|
|
93
|
+
task: {
|
|
94
|
+
id: task.id,
|
|
95
|
+
phase: task.phase,
|
|
96
|
+
owner: task.owner == null ? null : task.owner,
|
|
97
|
+
text: cutUtf8(maskLine(task.text == null ? '' : task.text).text, TEXT_BYTES),
|
|
98
|
+
files,
|
|
99
|
+
dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.map(String) : [],
|
|
100
|
+
},
|
|
101
|
+
constraints: [...FIXED_CONSTRAINTS, writeSet],
|
|
102
|
+
acceptance: (input.acceptance || []).map((id ) => ({ id: String(id), source: acceptanceSource(input.slug, String(id)) })),
|
|
103
|
+
sources: (input.sources || []).map((s ) => ({ path: s.path, sha256: s.sha256 == null ? null : s.sha256 })),
|
|
104
|
+
deltas: {
|
|
105
|
+
runBase: input.deltas && input.deltas.runBase,
|
|
106
|
+
attemptBase: input.deltas && input.deltas.attemptBase,
|
|
107
|
+
changes: ((input.deltas && input.deltas.changes) || []).map((c ) => ({
|
|
108
|
+
path: c.path, change: c.change, dispatchId: c.dispatchId == null ? null : c.dispatchId,
|
|
109
|
+
})),
|
|
110
|
+
},
|
|
111
|
+
budget: {
|
|
112
|
+
reserveTokens: input.budget && input.budget.reserveTokens != null ? input.budget.reserveTokens : null,
|
|
113
|
+
attemptSeconds: input.budget && input.budget.attemptSeconds != null ? input.budget.attemptSeconds : null,
|
|
114
|
+
attempts: { used: input.budget.attempts.used, limit: input.budget.attempts.limit },
|
|
115
|
+
},
|
|
116
|
+
grant: input.grant ? { depth: input.grant.depth, children: input.grant.children, tokens: input.grant.tokens } : null,
|
|
117
|
+
worktree: { path: input.worktree.path, branch: input.worktree.branch, base: input.worktree.base },
|
|
118
|
+
report: {
|
|
119
|
+
parser: input.report && input.report.parser ? input.report.parser : null,
|
|
120
|
+
spawn: input.report && Array.isArray(input.report.spawn) ? input.report.spawn.map(String) : null,
|
|
121
|
+
},
|
|
122
|
+
overflow: [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** IC-011: changed paths, rename and copy sources included, outside the task's normalized files. */
|
|
127
|
+
function undeclaredChanges(changes , files ) {
|
|
128
|
+
const declared = new Set((files || []).map(normalizeFile).filter(Boolean));
|
|
129
|
+
const out = [];
|
|
130
|
+
const seen = new Set ();
|
|
131
|
+
for (const change of changes) {
|
|
132
|
+
for (const p of [change.from, change.path]) {
|
|
133
|
+
if (p == null) continue;
|
|
134
|
+
const file = normalizeFile(p);
|
|
135
|
+
if (!file || declared.has(file) || seen.has(file)) continue;
|
|
136
|
+
seen.add(file);
|
|
137
|
+
out.push(file);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** IC-011. `input.files` is the task's declared `files:`; `changes` come from git, computed by the
|
|
144
|
+
* caller over the attempt commit. `summary` arrives masked and is cut to 2000 bytes here. */
|
|
145
|
+
function buildResult(input ) {
|
|
146
|
+
const changes = (input.changes || []).map((c ) => ({ path: c.path, change: c.change, from: c.from == null ? null : c.from }));
|
|
147
|
+
return {
|
|
148
|
+
schema: 'doflow-result-v1',
|
|
149
|
+
dispatchId: input.dispatchId,
|
|
150
|
+
task: input.task,
|
|
151
|
+
token: input.token,
|
|
152
|
+
epoch: input.epoch,
|
|
153
|
+
mode: input.mode,
|
|
154
|
+
outcome: input.outcome,
|
|
155
|
+
staleReason: input.staleReason == null ? null : input.staleReason,
|
|
156
|
+
failureClass: input.failureClass == null ? null : input.failureClass,
|
|
157
|
+
summary: cutUtf8(input.summary == null ? '' : String(input.summary), TEXT_BYTES),
|
|
158
|
+
status: input.status,
|
|
159
|
+
base: input.base,
|
|
160
|
+
commit: input.commit == null ? null : input.commit,
|
|
161
|
+
changes,
|
|
162
|
+
undeclared: undeclaredChanges(changes, input.files),
|
|
163
|
+
evidence: (input.evidence || []).map((e ) => ({ id: e.id, kind: 'test-result', exitCode: e.exitCode == null ? null : e.exitCode })),
|
|
164
|
+
blockers: (input.blockers || []).map(String),
|
|
165
|
+
usage: { tokens: input.usage.tokens, basis: input.usage.basis, source: input.usage.source },
|
|
166
|
+
workerRun: input.workerRun
|
|
167
|
+
? { id: input.workerRun.id, output: input.workerRun.output ? { stdout: input.workerRun.output.stdout, stderr: input.workerRun.output.stderr } : null }
|
|
168
|
+
: null,
|
|
169
|
+
isolation: input.isolation ? { status: input.isolation.status, reasons: (input.isolation.reasons || []).map(String) } : null,
|
|
170
|
+
overflow: [],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getList(value , list ) {
|
|
175
|
+
return list.split('.').reduce((node , key ) => node[key], value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function setList(value , list , items ) {
|
|
179
|
+
const keys = list.split('.');
|
|
180
|
+
const last = keys.pop() ;
|
|
181
|
+
keys.reduce((node , key ) => node[key], value)[last] = items;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Writes `text` to `file` through a temporary file in the same directory, then a rename. */
|
|
185
|
+
function writeAtomic(file , text ) {
|
|
186
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
|
|
187
|
+
try {
|
|
188
|
+
fs.writeFileSync(tmp, text, { encoding: 'utf8', mode: 0o600 });
|
|
189
|
+
fs.renameSync(tmp, file);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* the original error is the one reported */ }
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* IC-012. Writes `value` (a built envelope) to `<stateRoot>/dispatch-envelopes/<dispatchId>.<kind>.json`
|
|
198
|
+
* in canonical form within its bound, sidecars first. Returns `{ path, pointer, bytes, overflow,
|
|
199
|
+
* sidecars }`; `pointer` and each sidecar pointer are relative to `stateRoot`.
|
|
200
|
+
*/
|
|
201
|
+
function writeEnvelope({ stateRoot, dispatchId, kind, value } ) {
|
|
202
|
+
if (!(kind in BOUNDS)) throw new TypeError(`unknown envelope kind '${kind}'`);
|
|
203
|
+
if (typeof dispatchId !== 'string' || !SAFE_ID.test(dispatchId)) throw new TypeError(`invalid dispatch id '${dispatchId}'`);
|
|
204
|
+
const bound = BOUNDS[kind];
|
|
205
|
+
const envelope = JSON.parse(canonical(value));
|
|
206
|
+
if (!Array.isArray(envelope.overflow)) envelope.overflow = [];
|
|
207
|
+
const sidecars = [];
|
|
208
|
+
|
|
209
|
+
for (const list of CUT_ORDER[kind]) {
|
|
210
|
+
if (bytesOf(envelope) <= bound) break;
|
|
211
|
+
const items = getList(envelope, list);
|
|
212
|
+
if (!items.length) continue;
|
|
213
|
+
const pointer = `${ENVELOPE_DIR}/${dispatchId}.${list.replace(/\./g, '-')}.json`;
|
|
214
|
+
const entry = { list, pointer, total: items.length, kept: 0 };
|
|
215
|
+
envelope.overflow.push(entry);
|
|
216
|
+
const fits = (k ) => {
|
|
217
|
+
setList(envelope, list, items.slice(0, k));
|
|
218
|
+
entry.kept = k;
|
|
219
|
+
return bytesOf(envelope) <= bound;
|
|
220
|
+
};
|
|
221
|
+
// Longest fitting prefix: size grows with the prefix length, so a binary search finds it.
|
|
222
|
+
let lo = 0;
|
|
223
|
+
let hi = items.length;
|
|
224
|
+
while (lo < hi) {
|
|
225
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
226
|
+
if (fits(mid)) lo = mid; else hi = mid - 1;
|
|
227
|
+
}
|
|
228
|
+
fits(lo);
|
|
229
|
+
sidecars.push({ list, pointer, items: items.slice(lo) });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (bytesOf(envelope) > bound) {
|
|
233
|
+
const holder = kind === 'handoff' ? envelope.task : envelope;
|
|
234
|
+
const field = kind === 'handoff' ? 'text' : 'summary';
|
|
235
|
+
const mark = kind === 'result' ? CUT_MARK : '';
|
|
236
|
+
const original = String(holder[field]);
|
|
237
|
+
const fits = (n ) => {
|
|
238
|
+
holder[field] = cutUtf8(original, n) + mark;
|
|
239
|
+
return bytesOf(envelope) <= bound;
|
|
240
|
+
};
|
|
241
|
+
let lo = 0;
|
|
242
|
+
let hi = Buffer.byteLength(original, 'utf8');
|
|
243
|
+
while (lo < hi) {
|
|
244
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
245
|
+
if (fits(mid)) lo = mid; else hi = mid - 1;
|
|
246
|
+
}
|
|
247
|
+
if (!fits(lo)) throw new Error(`${kind} envelope for ${dispatchId} exceeds ${bound} bytes with every list and its text cut`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const dir = path.join(stateRoot, ENVELOPE_DIR);
|
|
251
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
252
|
+
for (const sidecar of sidecars) writeAtomic(path.join(stateRoot, sidecar.pointer), canonical(sidecar.items));
|
|
253
|
+
const text = canonical(envelope);
|
|
254
|
+
const file = path.join(dir, `${dispatchId}.${kind}.json`);
|
|
255
|
+
writeAtomic(file, text);
|
|
256
|
+
return {
|
|
257
|
+
path: file,
|
|
258
|
+
pointer: `${ENVELOPE_DIR}/${dispatchId}.${kind}.json`,
|
|
259
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
260
|
+
overflow: envelope.overflow,
|
|
261
|
+
sidecars: sidecars.map((s) => s.pointer),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
ENVELOPE_DIR,
|
|
267
|
+
BOUNDS,
|
|
268
|
+
CUT_ORDER,
|
|
269
|
+
FIXED_CONSTRAINTS,
|
|
270
|
+
canonical,
|
|
271
|
+
normalizeFile,
|
|
272
|
+
cutUtf8,
|
|
273
|
+
buildHandoff,
|
|
274
|
+
buildResult,
|
|
275
|
+
writeEnvelope,
|
|
276
|
+
};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Feature 082 C.2 (IC-033 declaration, request and status; DEC-008, DEC-028, DEC-041, DEC-051,
|
|
4
|
+
// DEC-062): spawn grants and the `spawn` action. Run directly by Node native type-stripping.
|
|
5
|
+
//
|
|
6
|
+
// - `validateGrant` checks `dispatch start --spawn-grant` against `--reserve-tokens`; `copyGrant`
|
|
7
|
+
// gives the copy each subagent dispatch carries, with its own `used` counters.
|
|
8
|
+
// - `requestSpawn` runs IC-033's refusals in order. Steps 1 to 3 and every read (the prompt, the
|
|
9
|
+
// 080 run's control, real paths) happen before the lock; steps 4 to 10 and the request's record
|
|
10
|
+
// run inside one `mutateDispatch`, so a refusal writes nothing. The prompt copy
|
|
11
|
+
// `dispatch-envelopes/<d-id>.c<n>.prompt.txt` is written after that lock, by temp file then
|
|
12
|
+
// rename: this is one of the three dispatch modules allowed a direct fs write (IC-037).
|
|
13
|
+
// - `spawnStatus` runs steps 1 to 4 over an unlocked read and reports a request of the caller's
|
|
14
|
+
// own dispatch.
|
|
15
|
+
//
|
|
16
|
+
// Launching the child is the loop's (IC-033 launch, C.3): it moves a request from `requested` to
|
|
17
|
+
// `launched` or `refused`, records `childRunId` and the child's `identity`, and sets `endState`
|
|
18
|
+
// (and `report`) when the child run ends. A request counts toward `grant.children` until it is
|
|
19
|
+
// `refused` or has an `endState`. Spawns a harness makes through its own subagent tool are
|
|
20
|
+
// invisible here; only this action is bounded (DEC-008).
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const crypto = require('node:crypto');
|
|
25
|
+
const { readTaskState } = require('../task-state.js');
|
|
26
|
+
const { storeFile: attemptStoreFile } = require('../task-attempts.js');
|
|
27
|
+
const { resolveIdentity } = require('../context/identity.js');
|
|
28
|
+
const { projectRoot } = require('../lifecycle/root.js');
|
|
29
|
+
const { storeFileFor, readDispatch, mutateDispatch, DispatchStoreError } = require('./store.js');
|
|
30
|
+
const { ENVELOPE_DIR } = require('./envelope.js');
|
|
31
|
+
|
|
32
|
+
const MAX_DEPTH = 1;
|
|
33
|
+
const MAX_CHILDREN = 4;
|
|
34
|
+
const GRANT_KEYS = Object.freeze(['depth', 'children', 'tokens']);
|
|
35
|
+
const WORKTREE_DIR = 'dispatch-worktrees';
|
|
36
|
+
const REQUEST_ID = /^(d-[0-9a-f]{12})\.c([1-9][0-9]*)$/;
|
|
37
|
+
const WHOLE = /^[0-9]+$/;
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
function usage(message ) {
|
|
43
|
+
return new DispatchStoreError('usage', message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A whole number given as an integer or a `^[0-9]+$` string, else null. */
|
|
47
|
+
function whole(value ) {
|
|
48
|
+
if (Number.isInteger(value) && value >= 0) return value;
|
|
49
|
+
if (typeof value === 'string' && WHOLE.test(value)) {
|
|
50
|
+
const n = Number(value);
|
|
51
|
+
if (Number.isSafeInteger(n)) return n;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* IC-033 declaration. `grant` is the `--spawn-grant` JSON text or its parsed object; returns
|
|
58
|
+
* `{ depth, children, tokens }`, or throws `usage` (exit 2). `depth` 0 or 1, `children` 0 to 4,
|
|
59
|
+
* `tokens` a whole number no larger than `reserveTokens`; a run started without `--reserve-tokens`
|
|
60
|
+
* can grant no tokens, since the carve has nothing to be carved from.
|
|
61
|
+
*/
|
|
62
|
+
function validateGrant(grant , reserveTokens = null) {
|
|
63
|
+
let value = grant;
|
|
64
|
+
if (typeof grant === 'string') {
|
|
65
|
+
try { value = JSON.parse(grant); } catch { throw usage('--spawn-grant is not valid JSON'); }
|
|
66
|
+
}
|
|
67
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw usage('--spawn-grant must be a JSON object {depth, children, tokens}');
|
|
68
|
+
const extra = Object.keys(value).filter((k) => !GRANT_KEYS.includes(k));
|
|
69
|
+
if (extra.length) throw usage(`--spawn-grant has unknown field '${extra[0]}'`);
|
|
70
|
+
const { depth, children, tokens } = value;
|
|
71
|
+
if (depth !== 0 && depth !== 1) throw usage(`--spawn-grant depth must be 0 or ${MAX_DEPTH}`);
|
|
72
|
+
if (!Number.isInteger(children) || children < 0 || children > MAX_CHILDREN) throw usage(`--spawn-grant children must be 0 to ${MAX_CHILDREN}`);
|
|
73
|
+
if (!Number.isSafeInteger(tokens) || tokens < 0) throw usage('--spawn-grant tokens must be a whole number');
|
|
74
|
+
const reserve = reserveTokens == null ? null : whole(reserveTokens);
|
|
75
|
+
if (reserveTokens != null && reserve === null) throw usage('--reserve-tokens must be a whole number');
|
|
76
|
+
if (tokens > (reserve ?? 0)) throw usage(`--spawn-grant tokens ${tokens} exceeds --reserve-tokens ${reserve ?? 'absent'}`);
|
|
77
|
+
return { depth, children, tokens };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** DEC-051: the grant a dispatch carries, its own counters at zero; null for a main-session
|
|
81
|
+
* dispatch or a run with no grant. */
|
|
82
|
+
function copyGrant(runGrant , mode ) {
|
|
83
|
+
if (!runGrant || mode !== 'subagent') return null;
|
|
84
|
+
return { depth: runGrant.depth, children: runGrant.children, tokens: runGrant.tokens, used: { children: 0, tokens: 0 } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function real(p ) {
|
|
88
|
+
try { return fs.realpathSync.native(p); } catch { return path.resolve(p); }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function refused(reason , extra = {}) {
|
|
92
|
+
return { disposition: 'refused', reason, ...extra };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Steps 1 and 2: the caller's identity, or a refusal. */
|
|
96
|
+
function callerOf(env , cwd ) {
|
|
97
|
+
const id = resolveIdentity(env, { cwd, sessionId: '' });
|
|
98
|
+
if (id.role !== 'worker') return { refusal: refused('not-worker-session') };
|
|
99
|
+
if (id.lineageKind !== 'launched' || !env.DOFLOW_WORKTREE) return { refusal: refused('identity-incomplete') };
|
|
100
|
+
return { caller: { run: id.run, lineage: id.lineage, attempt: id.attempt, worktree: real(env.DOFLOW_WORKTREE) } };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sameIdentity(a , b ) {
|
|
104
|
+
return !!a && a.run === b.run && a.lineage === b.lineage && String(a.attempt) === String(b.attempt);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The child worktree a request names (IC-016), real path when it exists. */
|
|
108
|
+
function childWorktree(root , requestId ) {
|
|
109
|
+
return real(path.join(root, '.doflow', 'state', WORKTREE_DIR, requestId));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Real paths of every dispatch's attempt worktree and every request's child worktree (keyed
|
|
113
|
+
* `child:<id>`), read before the lock so `decide` does no I/O. */
|
|
114
|
+
function realPaths(store , root ) {
|
|
115
|
+
const out = new Map();
|
|
116
|
+
for (const d of Object.values(store.dispatches) ) {
|
|
117
|
+
const p = d.attempt && d.attempt.worktree;
|
|
118
|
+
if (typeof p === 'string' && !out.has(p)) out.set(p, real(p));
|
|
119
|
+
}
|
|
120
|
+
for (const id of Object.keys(store.spawnRequests)) out.set(`child:${id}`, childWorktree(root, id));
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function liveChild(request ) {
|
|
125
|
+
return request.state !== 'refused' && request.endState == null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Step 4. Every non-ended dispatch whose recorded worker identity equals the caller's and whose
|
|
130
|
+
* attempt worktree is the caller's by real path, plus, with `children`, every live request of a
|
|
131
|
+
* non-ended dispatch whose child worktree is the caller's (matched so step 5 can name it). The
|
|
132
|
+
* worktree is part of the match: two workers of one launched main session share run, lineage and
|
|
133
|
+
* attempt (DEC-041).
|
|
134
|
+
*/
|
|
135
|
+
function matchCaller(store , caller , paths , root , children ) {
|
|
136
|
+
const childPath = (id ) => paths.get(`child:${id}`) ?? path.join(root, '.doflow', 'state', WORKTREE_DIR, id);
|
|
137
|
+
const matches = [];
|
|
138
|
+
for (const d of Object.values(store.dispatches) ) {
|
|
139
|
+
if (d.phase === 'ended' || !sameIdentity(d.workerIdentity, caller)) continue;
|
|
140
|
+
const p = d.attempt && d.attempt.worktree;
|
|
141
|
+
if (typeof p !== 'string' || (paths.get(p) ?? path.resolve(p)) !== caller.worktree) continue;
|
|
142
|
+
matches.push({ dispatchId: d.dispatchId, requestId: null });
|
|
143
|
+
}
|
|
144
|
+
if (!children) return matches;
|
|
145
|
+
for (const r of Object.values(store.spawnRequests) ) {
|
|
146
|
+
const granter = store.dispatches[r.granterDispatchId];
|
|
147
|
+
if (!granter || granter.phase === 'ended' || !liveChild(r)) continue;
|
|
148
|
+
if (r.identity && !sameIdentity(r.identity, caller)) continue;
|
|
149
|
+
if (childPath(r.requestId) !== caller.worktree) continue;
|
|
150
|
+
matches.push({ dispatchId: r.granterDispatchId, requestId: r.requestId });
|
|
151
|
+
}
|
|
152
|
+
return matches;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The next `<d-id>.c<n>` for a granter: one past the highest `n` it has used. */
|
|
156
|
+
function nextRequestId(store , dispatchId ) {
|
|
157
|
+
let n = 0;
|
|
158
|
+
for (const id of Object.keys(store.spawnRequests)) {
|
|
159
|
+
const m = REQUEST_ID.exec(id);
|
|
160
|
+
if (m && m[1] === dispatchId) n = Math.max(n, Number(m[2]));
|
|
161
|
+
}
|
|
162
|
+
return `${dispatchId}.c${n + 1}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The 080 run's `control`, read without a lock before the dispatch lock (IC-006 allows no read
|
|
166
|
+
* inside it); null when the ledger or the run is absent. */
|
|
167
|
+
function runControl(root , slug , runKey , fsImpl ) {
|
|
168
|
+
if (typeof runKey !== 'string') return null;
|
|
169
|
+
let ledger ;
|
|
170
|
+
try { ledger = readTaskState(fsImpl, attemptStoreFile(root, slug)); } catch { return null; }
|
|
171
|
+
const run = ledger && ledger.runs && Object.prototype.hasOwnProperty.call(ledger.runs, runKey) ? ledger.runs[runKey] : null;
|
|
172
|
+
return run && typeof run.control === 'string' ? run.control : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* IC-033 request: `dispatch --action spawn --slug --prompt-file --reserve-tokens`. Returns
|
|
177
|
+
* `{ disposition: 'spawn-requested', requestId, state: 'requested' }`, or `{ disposition: 'refused',
|
|
178
|
+
* reason, ... }` (exit 1) with nothing written; throws `usage` (exit 2) for a bad flag or an
|
|
179
|
+
* unreadable prompt file.
|
|
180
|
+
*/
|
|
181
|
+
function requestSpawn({ slug, promptFile, reserveTokens, env = process.env, cwd = process.cwd(), root, fsImpl = fs, now = new Date() } ) {
|
|
182
|
+
storeFileFor(root ?? '.', slug); // step 0: validates the slug
|
|
183
|
+
if (typeof promptFile !== 'string' || promptFile === '') throw usage('--prompt-file is required for --action spawn');
|
|
184
|
+
const reserve = whole(reserveTokens);
|
|
185
|
+
if (reserve === null) throw usage('--reserve-tokens must be a whole number for --action spawn');
|
|
186
|
+
|
|
187
|
+
const who = callerOf(env, cwd);
|
|
188
|
+
if (who.refusal) return who.refusal;
|
|
189
|
+
const caller = who.caller;
|
|
190
|
+
const base = real(root ?? projectRoot(cwd));
|
|
191
|
+
const storeFile = storeFileFor(base, slug);
|
|
192
|
+
|
|
193
|
+
const seen = readDispatch(storeFile, fsImpl);
|
|
194
|
+
if (!seen.run) return refused('no-dispatch-run');
|
|
195
|
+
|
|
196
|
+
let prompt ;
|
|
197
|
+
try { prompt = fsImpl.readFileSync(path.resolve(cwd, promptFile)); } catch (err ) {
|
|
198
|
+
throw usage(`--prompt-file '${promptFile}' cannot be read: ${err && err.code ? err.code : err}`);
|
|
199
|
+
}
|
|
200
|
+
const promptSha256 = crypto.createHash('sha256').update(prompt).digest('hex');
|
|
201
|
+
const control = runControl(base, slug, seen.run.runKey, fsImpl);
|
|
202
|
+
const paths = realPaths(seen, base);
|
|
203
|
+
|
|
204
|
+
// The copy, so the worker may delete its own file (which would otherwise count as a change of its
|
|
205
|
+
// attempt). Written before the lock under a temporary name and renamed to its request's name in the
|
|
206
|
+
// write that records the request, so no `requested` record is ever seen without its prompt (RT-16).
|
|
207
|
+
const dir = path.join(base, '.doflow', 'state', ENVELOPE_DIR);
|
|
208
|
+
const staged = path.join(dir, `.spawn.${process.pid}.${crypto.randomBytes(4).toString('hex')}.prompt.tmp`);
|
|
209
|
+
fsImpl.mkdirSync(dir, { recursive: true });
|
|
210
|
+
try {
|
|
211
|
+
fsImpl.writeFileSync(staged, prompt, { mode: 0o600 });
|
|
212
|
+
} catch (err) {
|
|
213
|
+
try { fsImpl.rmSync(staged, { force: true }); } catch { /* the original error is the one reported */ }
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
let copyError = null;
|
|
217
|
+
|
|
218
|
+
let result = null;
|
|
219
|
+
try {
|
|
220
|
+
result = mutateDispatch(storeFile, (store ) => {
|
|
221
|
+
const no = (reason , extra ) => ({ write: false, result: refused(reason, extra) });
|
|
222
|
+
if (!store.run) return no('no-dispatch-run');
|
|
223
|
+
const matches = matchCaller(store, caller, paths, base, true);
|
|
224
|
+
if (matches.length === 0) return no('no-matching-dispatch');
|
|
225
|
+
if (matches.length > 1) return no('identity-ambiguous', { dispatchIds: matches.map((m) => m.dispatchId).sort() });
|
|
226
|
+
const [{ dispatchId, requestId: childOf }] = matches;
|
|
227
|
+
if (childOf) return no('depth-exceeded', { limit: MAX_DEPTH, requestId: childOf });
|
|
228
|
+
const d = store.dispatches[dispatchId];
|
|
229
|
+
if (d.phase !== 'launched') return no('granter-not-running', { dispatchId, phase: d.phase });
|
|
230
|
+
if (control === 'paused') return no('run-paused', { dispatchId });
|
|
231
|
+
if (control !== 'running' || store.run.state === 'cancelling' || store.run.state === 'cancelled') return no('run-cancelled', { dispatchId });
|
|
232
|
+
const grant = d.grant;
|
|
233
|
+
if (!grant || grant.depth !== MAX_DEPTH) return no('spawning-denied', { dispatchId });
|
|
234
|
+
const active = (Object.values(store.spawnRequests) ).filter((r) => r.granterDispatchId === dispatchId && liveChild(r)).length;
|
|
235
|
+
if (active >= grant.children) return no('concurrency-exceeded', { dispatchId, limit: grant.children });
|
|
236
|
+
const used = grant.used || { children: 0, tokens: 0 };
|
|
237
|
+
const remaining = grant.tokens - used.tokens;
|
|
238
|
+
if (reserve > remaining) return no('budget-exceeded', { dispatchId, remaining });
|
|
239
|
+
|
|
240
|
+
const requestId = nextRequestId(store, dispatchId);
|
|
241
|
+
// One rename inside the lock, so the prompt exists before the record does; a failed rename
|
|
242
|
+
// writes nothing and spends nothing.
|
|
243
|
+
try { fsImpl.renameSync(staged, path.join(dir, `${requestId}.prompt.txt`)); } catch (err) {
|
|
244
|
+
copyError = err;
|
|
245
|
+
return { write: false, result: null };
|
|
246
|
+
}
|
|
247
|
+
store.spawnRequests[requestId] = {
|
|
248
|
+
requestId,
|
|
249
|
+
granterDispatchId: dispatchId,
|
|
250
|
+
prompt: `${ENVELOPE_DIR}/${requestId}.prompt.txt`,
|
|
251
|
+
promptSha256,
|
|
252
|
+
reserveTokens: reserve,
|
|
253
|
+
requestedAt: now.toISOString(),
|
|
254
|
+
state: 'requested',
|
|
255
|
+
reason: null,
|
|
256
|
+
childRunId: null,
|
|
257
|
+
identity: null,
|
|
258
|
+
endState: null,
|
|
259
|
+
report: null,
|
|
260
|
+
};
|
|
261
|
+
d.grant = { ...grant, used: { children: used.children + 1, tokens: used.tokens + reserve } };
|
|
262
|
+
return { write: true, result: { disposition: 'spawn-requested', requestId, state: 'requested' } };
|
|
263
|
+
}, fsImpl);
|
|
264
|
+
} finally {
|
|
265
|
+
if (!result || result.disposition !== 'spawn-requested') {
|
|
266
|
+
try { fsImpl.rmSync(staged, { force: true }); } catch { /* a refusal is still the answer */ }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (copyError) throw copyError;
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* IC-033 status: `dispatch --action spawn --slug --request-id`. Steps 1 to 4 over an unlocked read;
|
|
275
|
+
* `{ disposition: 'spawn-status', requestId, state, childRunId, endState, report }` for a request
|
|
276
|
+
* of the caller's own dispatch, else a refusal (`unknown-request` for any other request id).
|
|
277
|
+
*/
|
|
278
|
+
function spawnStatus({ slug, requestId, env = process.env, cwd = process.cwd(), root, fsImpl = fs } ) {
|
|
279
|
+
storeFileFor(root ?? '.', slug);
|
|
280
|
+
if (typeof requestId !== 'string' || !REQUEST_ID.test(requestId)) throw usage(`--request-id '${requestId}' is not '<d-id>.c<n>'`);
|
|
281
|
+
const who = callerOf(env, cwd);
|
|
282
|
+
if (who.refusal) return who.refusal;
|
|
283
|
+
const base = real(root ?? projectRoot(cwd));
|
|
284
|
+
const store = readDispatch(storeFileFor(base, slug), fsImpl);
|
|
285
|
+
if (!store.run) return refused('no-dispatch-run');
|
|
286
|
+
const matches = matchCaller(store, who.caller, realPaths(store, base), base, false);
|
|
287
|
+
if (matches.length === 0) return refused('no-matching-dispatch');
|
|
288
|
+
if (matches.length > 1) return refused('identity-ambiguous', { dispatchIds: matches.map((m) => m.dispatchId).sort() });
|
|
289
|
+
const request = Object.prototype.hasOwnProperty.call(store.spawnRequests, requestId) ? store.spawnRequests[requestId] : null;
|
|
290
|
+
if (!request || request.granterDispatchId !== matches[0].dispatchId) return refused('unknown-request', { requestId });
|
|
291
|
+
return {
|
|
292
|
+
disposition: 'spawn-status',
|
|
293
|
+
requestId,
|
|
294
|
+
state: request.state,
|
|
295
|
+
childRunId: request.childRunId ?? null,
|
|
296
|
+
endState: request.endState ?? null,
|
|
297
|
+
report: request.report ?? null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
MAX_DEPTH,
|
|
303
|
+
MAX_CHILDREN,
|
|
304
|
+
REQUEST_ID,
|
|
305
|
+
WORKTREE_DIR,
|
|
306
|
+
validateGrant,
|
|
307
|
+
copyGrant,
|
|
308
|
+
requestSpawn,
|
|
309
|
+
spawnStatus,
|
|
310
|
+
};
|