@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
|
@@ -12,22 +12,27 @@
|
|
|
12
12
|
|
|
13
13
|
const fs = require('node:fs');
|
|
14
14
|
const path = require('node:path');
|
|
15
|
+
const { execFileSync } = require('node:child_process');
|
|
15
16
|
const { WorkflowEngine } = require('./workflow-engine.js');
|
|
16
|
-
const { updateTaskState, readTaskState } = require('./task-state.js');
|
|
17
|
+
const { updateTaskState, readTaskState, boundedAcquire } = require('./task-state.js');
|
|
18
|
+
const { recordIn, isSafeSlug } = require('./task-scope.js');
|
|
17
19
|
const { ResearchRequestStore } = require('./research-request.js');
|
|
18
20
|
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
19
21
|
const { resolveActiveFeature } = require('./feature-resolve.js');
|
|
20
|
-
const { compactDecisions } = require('./decision-register.js');
|
|
22
|
+
const { compactDecisions, addDecisions } = require('./decision-register.js');
|
|
21
23
|
const { EvidenceLedger } = require('./evidence-ledger.js');
|
|
22
24
|
const { resolveConsultations } = require('./mcp-consultations.js');
|
|
23
25
|
const { mcpReach } = require('./mcp-reach.js');
|
|
24
26
|
const { loadRegistry } = require('../registry/index.js');
|
|
25
27
|
const { loadStorageRegistry } = require('./storage/registry.js');
|
|
26
28
|
const { resolveRoots, resolveStorePath } = require('./storage/roots.js');
|
|
27
|
-
const { checkCompletion: checkTaskAttemptCompletion } = require('./task-attempts.js');
|
|
29
|
+
const { checkCompletion: checkTaskAttemptCompletion, storeFile: taskAttemptStoreFile } = require('./task-attempts.js');
|
|
30
|
+
const { runKeyFor, refuseSolo } = require('./task-run.js');
|
|
31
|
+
const { projectRoot: lifecycleProjectRoot } = require('./lifecycle/root.js');
|
|
28
32
|
const { appendOutcome, findPick, readWindow } = require('./advice/record.js');
|
|
29
33
|
const {
|
|
30
|
-
checkReadiness, harnessHookNote, featureSlugFor, runSlugOf, findRunFile,
|
|
34
|
+
checkReadiness, harnessHookNote, featureSlugFor, runSlugOf, findRunFile, orchestrationRuns, selectRunEntries,
|
|
35
|
+
RUN_STORE_REL, runStoreDir, readRunRecord,
|
|
31
36
|
} = require('./implementation-gate.js');
|
|
32
37
|
|
|
33
38
|
/** Appended to the note of an implementation stage completed with no task-attempt ledger (FR-006). */
|
|
@@ -56,10 +61,31 @@ function runState(state ) {
|
|
|
56
61
|
return state;
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
/** The stable status shape shared by the status action and the read-only list action. */
|
|
65
|
+
function snapshotRun(run ) {
|
|
66
|
+
const current = run.cursor < run.program.length ? run.program[run.cursor] : null;
|
|
67
|
+
const done = run.program.filter((n ) => n.status === 'completed' || n.status === 'skipped' || n.status === 'approved').length;
|
|
68
|
+
return {
|
|
69
|
+
taskId: run.taskId,
|
|
70
|
+
taskClass: run.taskClass,
|
|
71
|
+
workflowName: run.workflowName,
|
|
72
|
+
state: run.state,
|
|
73
|
+
current: current ? { type: current.type, id: current.id, kind: current.kind ?? null, agent: current.agent ?? null, mutatesSource: current.mutatesSource ?? null, readinessTemplate: current.readinessTemplate ?? null } : null,
|
|
74
|
+
awaitingGate: run.state === 'AWAITING_GATE' && current ? { gateId: current.id, name: current.name, prompt: current.prompt } : null,
|
|
75
|
+
progress: { done, total: run.program.length },
|
|
76
|
+
terminalStage: run.terminalStage,
|
|
77
|
+
revision: Number.isInteger(run.revision) ? run.revision : 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
59
81
|
const GATE_DECISIONS = Object.freeze(['approve', 'reject']);
|
|
82
|
+
/** IC-005: the register stage a policy approval of each gate is recorded under. */
|
|
83
|
+
const POLICY_GATE_STAGE = Object.freeze({ 'gate-0': 'discovery', 'gate-a': 'planning', 'gate-b': 'review' });
|
|
60
84
|
/** What a completed stage established, separate from the fact it was walked past (review A1). */
|
|
61
85
|
const STAGE_OUTCOMES = new Set(['passed', 'failed', 'unverified']);
|
|
62
86
|
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
87
|
+
/** A context-pack delivery digest, as the delivery store keys it (`packDigest`). */
|
|
88
|
+
const PACK_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
63
89
|
/** Why a handoff with no run and no class records nothing, as the result and the CLI line say. */
|
|
64
90
|
const STANDALONE_REASON = 'no run exists for this task and no --task-class was given, so nothing was recorded and no readiness was required';
|
|
65
91
|
|
|
@@ -70,6 +96,77 @@ function assertSafeId(value , label ) {
|
|
|
70
96
|
|
|
71
97
|
function iso(now ) { return (now ?? new Date()).toISOString(); }
|
|
72
98
|
|
|
99
|
+
/** The well-formed `{ taskId, digest }` items of a node's `packs`, in delivery order. */
|
|
100
|
+
function stagePacks(packs ) {
|
|
101
|
+
return (Array.isArray(packs) ? packs : [])
|
|
102
|
+
.filter((p ) => p && typeof p.taskId === 'string' && typeof p.digest === 'string' && ID_PATTERN.test(p.taskId) && PACK_DIGEST.test(p.digest))
|
|
103
|
+
.map((p ) => ({ taskId: p.taskId, digest: p.digest }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Issue #9 stage 3 (FU-7j3k84): record, on the run's own record, that a context pack was handed to
|
|
108
|
+
* the stage active at this moment. Called by the context-pack verb after it records a delivery.
|
|
109
|
+
*
|
|
110
|
+
* The run is the pack task's own run, else its feature's run: the feature named by `slug`, else by
|
|
111
|
+
* this checkout's feature pointer (row 135) while the checkout is still on the branch it recorded. Only runs
|
|
112
|
+
* under `stateRoot` are read; the branch name alone never picks one. The first run that exists
|
|
113
|
+
* decides: when it is RUNNING with a stage under its cursor, that stage's node gets
|
|
114
|
+
* `packs: [{ taskId, digest }]`, one item per pack task (its latest delivery, moved last). Anything
|
|
115
|
+
* else (no run, a gate, a finished run, an unreadable or locked record) records nothing: unknown,
|
|
116
|
+
* never a guess. A delivery the node already ends with writes nothing, so a repeat leaves the
|
|
117
|
+
* run's revision alone. Never throws.
|
|
118
|
+
* `checkout` (`{ repoRoot, branch }`) is the caller's already-resolved checkout, so the pointer lookup
|
|
119
|
+
* spawns no git child of its own.
|
|
120
|
+
* @returns {{ runTaskId: string, stage: string }|null} where the pack was recorded
|
|
121
|
+
*/
|
|
122
|
+
function recordStagePack({ stateRoot, sourceRoot = stateRoot, taskId, slug = null, digest, checkout = null, fsImpl = fs } ) {
|
|
123
|
+
try {
|
|
124
|
+
if (!ID_PATTERN.test(String(taskId ?? '')) || !PACK_DIGEST.test(String(digest ?? ''))) return null;
|
|
125
|
+
const storeDir = runStoreDir(stateRoot);
|
|
126
|
+
const locate = (runTaskId , feature ) => {
|
|
127
|
+
const { file, read } = recordIn({ base: storeDir, namespace: feature && feature !== runTaskId ? feature : null, name: `${runTaskId}.json`, fsImpl });
|
|
128
|
+
// `read` is the namespaced file when it exists, else a legacy flat file that is this feature's.
|
|
129
|
+
return fsImpl.existsSync(read) ? { runTaskId, file: read } : null;
|
|
130
|
+
};
|
|
131
|
+
let found = locate(taskId, isSafeSlug(slug) ? slug : null);
|
|
132
|
+
if (!found) {
|
|
133
|
+
let feature = isSafeSlug(slug) ? slug : null;
|
|
134
|
+
let featureRun = feature;
|
|
135
|
+
if (!feature) {
|
|
136
|
+
const active = require('./feature-pointer.js').activeEntry({ storeRoot: stateRoot, cwd: sourceRoot, checkout });
|
|
137
|
+
if (active && isSafeSlug(active.slug)) {
|
|
138
|
+
feature = active.slug;
|
|
139
|
+
featureRun = ID_PATTERN.test(String(active.taskId ?? '')) ? active.taskId : active.slug;
|
|
140
|
+
found = locate(taskId, feature);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!found && featureRun) found = locate(featureRun, feature);
|
|
144
|
+
}
|
|
145
|
+
if (!found) return null;
|
|
146
|
+
readRunRecord(fsImpl, found.file, storeDir); // the run store's guarded read: a link or FIFO is refused here
|
|
147
|
+
let stage = null;
|
|
148
|
+
updateTaskState({
|
|
149
|
+
fsImpl, file: found.file, acquire: (impl , target ) => boundedAcquire(impl, target, { retries: 3, waitMs: 5 }),
|
|
150
|
+
// The guarded check again INSIDE the lock, before the unguarded read and the write: a namespace
|
|
151
|
+
// folder swapped for a link since the check above is refused here (null result). A swap after
|
|
152
|
+
// this line and before the rename is the accepted, much narrower window.
|
|
153
|
+
prepare: () => { readRunRecord(fsImpl, found.file, storeDir); },
|
|
154
|
+
build(disk ) {
|
|
155
|
+
const program = Array.isArray(disk?.program) ? disk.program : [];
|
|
156
|
+
const node = disk?.state === 'RUNNING' && Number.isInteger(disk.cursor) ? program[disk.cursor] : null;
|
|
157
|
+
if (!node || node.type !== 'stage') return null;
|
|
158
|
+
const prior = stagePacks(node.packs);
|
|
159
|
+
const last = prior[prior.length - 1];
|
|
160
|
+
if (last && last.taskId === taskId && last.digest === digest) return null;
|
|
161
|
+
node.packs = [...prior.filter((p) => p.taskId !== taskId), { taskId, digest }];
|
|
162
|
+
stage = node.id;
|
|
163
|
+
return disk;
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
return stage ? { runTaskId: found.runTaskId, stage } : null;
|
|
167
|
+
} catch { return null; }
|
|
168
|
+
}
|
|
169
|
+
|
|
73
170
|
/** Interleave stages with their gates: each gate sits immediately after the stage named by its
|
|
74
171
|
* `afterStage`. A gate anchored to an unknown stage is a compile error here, not a silent orphan. */
|
|
75
172
|
function compileProgram(workflow ) {
|
|
@@ -78,6 +175,7 @@ function compileProgram(workflow ) {
|
|
|
78
175
|
for (const stage of workflow.stages || []) {
|
|
79
176
|
nodes.push({
|
|
80
177
|
type: 'stage', id: stage.id, skill: stage.skill, kind: stage.kind,
|
|
178
|
+
agent: stage.agent,
|
|
81
179
|
mutatesSource: Boolean(stage.mutatesSource),
|
|
82
180
|
readinessTemplate: stage.readinessTemplate ?? null,
|
|
83
181
|
optional: Boolean(stage.optional),
|
|
@@ -98,6 +196,29 @@ function compileProgram(workflow ) {
|
|
|
98
196
|
return nodes;
|
|
99
197
|
}
|
|
100
198
|
|
|
199
|
+
/** The stage a loop program adds to a class that has no review stage of its own (trivial-edit). */
|
|
200
|
+
const LOOP_REVIEW_STAGE = Object.freeze({
|
|
201
|
+
id: 'review', skill: 'do-code-review', agent: 'reviewer', kind: 'review', readinessTemplate: null, optional: false,
|
|
202
|
+
purpose: 'Review the delivered change and report an approval status.',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The program the roadmap loop runs for a task class (AU-P1). The loop is unattended, so it takes
|
|
207
|
+
* no optional stage out of the walk: every stage is required, a class without a review stage gets
|
|
208
|
+
* one, and a `gate-b` follows the last review so the class's gate-b policy (core/registry/gates.json)
|
|
209
|
+
* can approve the merge. The feature class already has all of that. workflows.json is not changed:
|
|
210
|
+
* an attended `/do-implement` run keeps its gate-free program.
|
|
211
|
+
*/
|
|
212
|
+
function loopWorkflow(workflow ) {
|
|
213
|
+
if ((workflow.gates || []).some((gate ) => gate.id === 'gate-b')) return workflow;
|
|
214
|
+
const stages = (workflow.stages || []).map((stage ) => ({ ...stage, optional: false }));
|
|
215
|
+
if (!stages.some((stage ) => stage.kind === 'review')) stages.push({ ...LOOP_REVIEW_STAGE });
|
|
216
|
+
const review = stages.filter((stage ) => stage.kind === 'review').at(-1);
|
|
217
|
+
const gate = { id: 'gate-b', name: 'Before commit or merge', afterStage: review.id, kind: 'approval', trigger: 'always',
|
|
218
|
+
prompt: 'Review is complete. Proceed to commit and merge?' };
|
|
219
|
+
return { ...workflow, stages, gates: [...(workflow.gates || []), gate] };
|
|
220
|
+
}
|
|
221
|
+
|
|
101
222
|
class WorkflowOrchestrator {
|
|
102
223
|
|
|
103
224
|
|
|
@@ -106,8 +227,12 @@ class WorkflowOrchestrator {
|
|
|
106
227
|
|
|
107
228
|
|
|
108
229
|
|
|
230
|
+
|
|
109
231
|
|
|
110
232
|
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
111
236
|
/**
|
|
112
237
|
* @param {Object} [options]
|
|
113
238
|
* @param {string} [options.repoRoot] install root owning core/registry + default state dir.
|
|
@@ -123,39 +248,56 @@ class WorkflowOrchestrator {
|
|
|
123
248
|
* `none` (no ledger) records the stage as `unverified` whatever outcome was supplied. Unwired
|
|
124
249
|
* means the stage is not checked, which only a unit test that builds the orchestrator by hand
|
|
125
250
|
* relies on: the CLI always wires the real one.
|
|
251
|
+
* @param {(node: object, run: object) => {refusals: Array<{path: string, line: number, message: string}>, advisories: Array<object>}} [options.planExitCheck]
|
|
252
|
+
* row 134 gate for the do-plan planning stage: every refusal (a constitution MUST marked FAIL or
|
|
253
|
+
* without a row) refuses completion. Unwired means the stage is not checked, as for taskAttemptCheck.
|
|
126
254
|
* @param {object} [options.fsImpl]
|
|
127
255
|
* @param {string|null} [options.featureSlug] the feature a run started here belongs to
|
|
256
|
+
* @param {object} [options.env] the environment `start` reads the caller's identity and the telemetry switch from
|
|
257
|
+
* @param {string} [options.cwd] the directory `start` resolves the caller's run key and the 080 store from
|
|
128
258
|
* @param {() => object} [options.buildAdapters] builds the adapter registry `mcpReach` reads installs
|
|
129
259
|
* through; without it install facts are unknown and no outcome is inferred from them
|
|
130
260
|
*/
|
|
131
|
-
constructor({ repoRoot = REPO_ROOT, projectRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, fsImpl, featureSlug = null, buildAdapters = null } = {}) {
|
|
261
|
+
constructor({ repoRoot = REPO_ROOT, projectRoot, sourceRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, planExitCheck, fsImpl, featureSlug = null, buildAdapters = null, env = process.env, cwd = process.cwd() } = {}) {
|
|
132
262
|
this.fsImpl = fsImpl || fs;
|
|
133
263
|
this.repoRoot = repoRoot;
|
|
134
|
-
this.projectRoot = projectRoot || (stateDir?.endsWith(
|
|
264
|
+
this.projectRoot = projectRoot || (stateDir?.endsWith(RUN_STORE_REL)
|
|
135
265
|
? path.resolve(stateDir, '../../..') : repoRoot);
|
|
266
|
+
this.sourceRoot = sourceRoot || this.projectRoot;
|
|
136
267
|
this.engine = engine || new WorkflowEngine({ repoRoot });
|
|
137
|
-
this.stateDir = stateDir ||
|
|
268
|
+
this.stateDir = stateDir || runStoreDir(this.projectRoot);
|
|
138
269
|
this.readinessEvaluate = readinessEvaluate || null;
|
|
139
270
|
this.taskAttemptCheck = taskAttemptCheck || null;
|
|
271
|
+
this.planExitCheck = planExitCheck || null;
|
|
140
272
|
this.featureSlug = featureSlug;
|
|
141
273
|
this.buildAdapters = buildAdapters;
|
|
274
|
+
this.env = env;
|
|
275
|
+
this.cwd = cwd;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The task's run (115 C-1): `<stateDir>/<featureSlug>/<taskId>.json` for a task id other than its
|
|
279
|
+
* feature's slug, else `<stateDir>/<taskId>.json`; a flat run of a namespaced id is read, and copied
|
|
280
|
+
* into the namespace on its next write, only when it is this feature's or names none (D-3). */
|
|
281
|
+
runRecord(taskId ) {
|
|
282
|
+
const id = assertSafeId(taskId, 'taskId');
|
|
283
|
+
const namespace = isSafeSlug(this.featureSlug) && this.featureSlug !== id ? this.featureSlug : null;
|
|
284
|
+
return recordIn({ base: this.stateDir, namespace, name: `${id}.json`, fsImpl: this.fsImpl, refuseUnsafeFlat: true });
|
|
142
285
|
}
|
|
143
286
|
|
|
144
287
|
runFile(taskId ) {
|
|
145
|
-
return
|
|
288
|
+
return this.runRecord(taskId).file;
|
|
146
289
|
}
|
|
147
290
|
|
|
148
291
|
readRun(taskId ) {
|
|
149
|
-
|
|
150
|
-
if (!this.fsImpl.existsSync(file)) return null;
|
|
151
|
-
return readTaskState(this.fsImpl, file);
|
|
292
|
+
return readRunRecord(this.fsImpl, this.runRecord(taskId).read, this.stateDir);
|
|
152
293
|
}
|
|
153
294
|
|
|
154
295
|
writeRun(run , now , beforeWrite ) {
|
|
155
296
|
run.updatedAt = iso(now);
|
|
156
|
-
|
|
297
|
+
const { file, seed } = this.runRecord(run.taskId);
|
|
298
|
+
this.fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
157
299
|
const expectedRevision = run.revision || 0;
|
|
158
|
-
updateTaskState({ file
|
|
300
|
+
updateTaskState({ file, fsImpl: this.fsImpl, prepare: seed, build: (disk ) => {
|
|
159
301
|
if ((disk?.revision || 0) !== expectedRevision) {
|
|
160
302
|
throw new Error(`Concurrent workflow update for '${run.taskId}'; reload the run before retrying. Nothing was written.`);
|
|
161
303
|
}
|
|
@@ -166,11 +308,15 @@ class WorkflowOrchestrator {
|
|
|
166
308
|
return run;
|
|
167
309
|
}
|
|
168
310
|
|
|
169
|
-
/** Begin a new run. An existing run for the same task id is refused — resume instead.
|
|
170
|
-
|
|
311
|
+
/** Begin a new run. An existing run for the same task id is refused — resume instead.
|
|
312
|
+
* `loopProgram` is the roadmap loop's option only (loopWorkflow): the run journal records it, and no
|
|
313
|
+
* verb passes it. */
|
|
314
|
+
start({ taskId, taskClass, now, loopProgram = false } = {}) {
|
|
171
315
|
assertSafeId(taskId, 'taskId');
|
|
172
316
|
if (this.readRun(taskId)) throw new Error(`Workflow run '${taskId}' already exists; resume it instead of restarting`);
|
|
173
|
-
const
|
|
317
|
+
const resolved = this.engine.resolveWorkflow(taskClass); // unknown classes are rejected loudly
|
|
318
|
+
const workflow = loopProgram ? loopWorkflow(resolved) : resolved;
|
|
319
|
+
const looped = workflow !== resolved; // the feature class already is a loop program: its journal is unchanged
|
|
174
320
|
const program = compileProgram(workflow);
|
|
175
321
|
const run = {
|
|
176
322
|
version: 1,
|
|
@@ -181,28 +327,40 @@ class WorkflowOrchestrator {
|
|
|
181
327
|
state: runState('RUNNING'),
|
|
182
328
|
cursor: 0,
|
|
183
329
|
program,
|
|
184
|
-
history: [{ at: iso(now), action: 'start', detail: `task-class=${taskClass}` }],
|
|
330
|
+
history: [{ at: iso(now), action: 'start', detail: `task-class=${taskClass}${looped ? ' program=roadmap-loop' : ''}` }],
|
|
185
331
|
startedAt: iso(now),
|
|
332
|
+
...(looped ? { programKind: 'roadmap-loop' } : {}),
|
|
186
333
|
// The feature the run belongs to, so another checkout can tell it from a run with the same id.
|
|
187
334
|
...(this.featureSlug ? { featureSlug: this.featureSlug } : {}),
|
|
335
|
+
...this.traceFields(),
|
|
188
336
|
};
|
|
189
337
|
this.writeRun(run, now);
|
|
190
338
|
return this.snapshot(run);
|
|
191
339
|
}
|
|
192
340
|
|
|
341
|
+
/**
|
|
342
|
+
* 088 IC-010 / DD-5: the caller's 080 run key and, when the 080 store already holds that run with
|
|
343
|
+
* a trace id, a copy of the id. Empty with telemetry off, for a solo lineage, or on any fault:
|
|
344
|
+
* nothing here can fail or change a start. No id is minted here and no action is added.
|
|
345
|
+
*/
|
|
346
|
+
traceFields() {
|
|
347
|
+
try {
|
|
348
|
+
const key = runKeyFor(this.env, this.cwd);
|
|
349
|
+
if (!key.traceIds || refuseSolo(key.identity)) return {};
|
|
350
|
+
const out = { runKey: key.runKey };
|
|
351
|
+
if (this.featureSlug) {
|
|
352
|
+
try {
|
|
353
|
+
const store = readTaskState(this.fsImpl, taskAttemptStoreFile(lifecycleProjectRoot(this.cwd), this.featureSlug));
|
|
354
|
+
const record = store && store.runs && Object.hasOwn(store.runs, key.runKey) ? store.runs[key.runKey] : null;
|
|
355
|
+
if (record && typeof record.traceId === 'string' && /^[0-9a-f]{32}$/.test(record.traceId)) out.traceId = record.traceId;
|
|
356
|
+
} catch { /* an unreadable 080 store means no copy */ }
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
} catch { return {}; }
|
|
360
|
+
}
|
|
361
|
+
|
|
193
362
|
snapshot(run ) {
|
|
194
|
-
|
|
195
|
-
const done = run.program.filter((n ) => n.status === 'completed' || n.status === 'skipped' || n.status === 'approved').length;
|
|
196
|
-
return {
|
|
197
|
-
taskId: run.taskId,
|
|
198
|
-
taskClass: run.taskClass,
|
|
199
|
-
workflowName: run.workflowName,
|
|
200
|
-
state: run.state,
|
|
201
|
-
current: current ? { type: current.type, id: current.id, kind: current.kind ?? null, mutatesSource: current.mutatesSource ?? null, readinessTemplate: current.readinessTemplate ?? null } : null,
|
|
202
|
-
awaitingGate: run.state === 'AWAITING_GATE' && current ? { gateId: current.id, name: current.name, prompt: current.prompt } : null,
|
|
203
|
-
progress: { done, total: run.program.length },
|
|
204
|
-
terminalStage: run.terminalStage,
|
|
205
|
-
};
|
|
363
|
+
return snapshotRun(run);
|
|
206
364
|
}
|
|
207
365
|
|
|
208
366
|
status(taskId ) {
|
|
@@ -244,7 +402,10 @@ class WorkflowOrchestrator {
|
|
|
244
402
|
}
|
|
245
403
|
// The ledger is checked here too, before catch-up writes anything, so a refused implementation
|
|
246
404
|
// handoff leaves no backfilled stage behind.
|
|
247
|
-
if (preview.reason === 'reached-candidate')
|
|
405
|
+
if (preview.reason === 'reached-candidate') {
|
|
406
|
+
this.assertTaskAttempts(preview.node, existing || { taskId, taskClass: resolvedClass });
|
|
407
|
+
this.assertPlanExit(preview.node, existing || { taskId, taskClass: resolvedClass });
|
|
408
|
+
}
|
|
248
409
|
const positioned = this.catchUp({ taskId, taskClass: resolvedClass, candidateStageIds: candidates, note, now });
|
|
249
410
|
if (positioned.reason === 'reached-candidate') {
|
|
250
411
|
// A run this handoff started has no earlier stage: every evidence item counts.
|
|
@@ -295,8 +456,9 @@ class WorkflowOrchestrator {
|
|
|
295
456
|
const harness = (registry.harnesses || []).find((item ) => item.id === id);
|
|
296
457
|
return harness ? harness.displayName || harness.id : id;
|
|
297
458
|
};
|
|
298
|
-
const { briefRecords, repoRoot } = stageId ? this.readBriefRecords(taskId) : { briefRecords: [], repoRoot: this.
|
|
299
|
-
|
|
459
|
+
const { briefRecords, repoRoot } = stageId ? this.readBriefRecords(taskId) : { briefRecords: [], repoRoot: this.sourceRoot };
|
|
460
|
+
// Advice picks are project state, kept with the run's records, not in the feature folder's checkout.
|
|
461
|
+
if (stageId) this.recordBriefTierAdvice({ briefRecords, taskId, stageId, repoRoot: this.projectRoot });
|
|
300
462
|
try {
|
|
301
463
|
const result = resolveConsultations({
|
|
302
464
|
taskId, stageId, skill, flags, registry, ledger: {
|
|
@@ -305,7 +467,7 @@ class WorkflowOrchestrator {
|
|
|
305
467
|
return ledger.queryEvidence({ taskId: id });
|
|
306
468
|
},
|
|
307
469
|
},
|
|
308
|
-
requests: new ResearchRequestStore({ projectRoot: this.projectRoot, fsImpl: this.fsImpl, slug: this.featureSlug }),
|
|
470
|
+
requests: new ResearchRequestStore({ projectRoot: this.projectRoot, sourceRoot: this.sourceRoot, fsImpl: this.fsImpl, slug: this.featureSlug }),
|
|
309
471
|
briefRecords,
|
|
310
472
|
readReport: (file ) => {
|
|
311
473
|
const target = path.resolve(repoRoot, file);
|
|
@@ -389,28 +551,28 @@ class WorkflowOrchestrator {
|
|
|
389
551
|
}
|
|
390
552
|
|
|
391
553
|
/** The brief records (`*-brief.json`) of the feature this run belongs to, parsed; unreadable ones
|
|
392
|
-
* are left out.
|
|
393
|
-
* folder
|
|
554
|
+
* are left out. They are feature-folder files, so they are resolved from the checkout the verb runs
|
|
555
|
+
* in with the resolver's `feature_root` (that checkout's folder, else the main checkout's), never
|
|
556
|
+
* from the state root (115 C-10). Resolving the feature spawns the resolver, so it is only done
|
|
557
|
+
* where a feature folder exists above one of the two. */
|
|
394
558
|
readBriefRecords(taskId ) {
|
|
395
|
-
const none = { briefRecords: [], repoRoot: this.
|
|
559
|
+
const none = { briefRecords: [], repoRoot: this.sourceRoot };
|
|
396
560
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
dir = parent;
|
|
404
|
-
}
|
|
561
|
+
const hasFolder = [this.sourceRoot, this.projectRoot].some((root) => {
|
|
562
|
+
for (let dir = path.resolve(root); ; dir = path.dirname(dir)) {
|
|
563
|
+
if (this.fsImpl.existsSync(path.join(dir, 'agent-docs', 'doflow'))) return true;
|
|
564
|
+
if (path.dirname(dir) === dir) return false;
|
|
565
|
+
}
|
|
566
|
+
});
|
|
405
567
|
if (!hasFolder) return none;
|
|
406
|
-
const feature = resolveActiveFeature({ projectRoot: this.
|
|
568
|
+
const feature = resolveActiveFeature({ projectRoot: this.sourceRoot, slug: taskId });
|
|
407
569
|
if (feature.error) return none;
|
|
408
|
-
const exec = path.join(feature.
|
|
570
|
+
const exec = path.join(feature.featureFolder, 'exec');
|
|
409
571
|
const briefRecords = [];
|
|
410
572
|
for (const name of this.fsImpl.readdirSync(exec).filter((entry ) => entry.endsWith('-brief.json')).sort()) {
|
|
411
573
|
try { briefRecords.push(JSON.parse(this.fsImpl.readFileSync(path.join(exec, name), 'utf8'))); } catch { /* an unreadable record contributes nothing */ }
|
|
412
574
|
}
|
|
413
|
-
return { briefRecords, repoRoot: feature.
|
|
575
|
+
return { briefRecords, repoRoot: feature.featureRoot };
|
|
414
576
|
} catch { return none; }
|
|
415
577
|
}
|
|
416
578
|
|
|
@@ -463,15 +625,21 @@ class WorkflowOrchestrator {
|
|
|
463
625
|
* stage is always imported+unverified, whatever the caller says: catch-up importing history
|
|
464
626
|
* cannot import a verification. A 'failed' outcome still advances — recording the failure
|
|
465
627
|
* honestly beats refusing to record it, and a bug reproduction legitimately completes by
|
|
466
|
-
* observing the expected failure — but the record no longer reads as success.
|
|
628
|
+
* observing the expected failure — but the record no longer reads as success.
|
|
629
|
+
*
|
|
630
|
+
* Issue #9 stage 3: the entry names the context packs the stage was handed (`packs`), copied from
|
|
631
|
+
* what `recordStagePack` wrote on this node while it was the run's active stage. A stage handed
|
|
632
|
+
* none gets no `packs`; a malformed item is left out, never an error. */
|
|
467
633
|
completeStage({ taskId, stageId, note, outcome, backfilled = false, consultations, now } = {}) {
|
|
468
634
|
const run = this.requireRun(taskId);
|
|
469
635
|
const node = this.expectOpenStage(run, stageId);
|
|
636
|
+
const packs = stagePacks(node.packs);
|
|
470
637
|
if (outcome !== undefined && !STAGE_OUTCOMES.has(outcome)) {
|
|
471
638
|
throw new Error(`Unknown stage outcome '${outcome}'. Valid: ${[...STAGE_OUTCOMES].join(', ')} — omit the flag to record 'unverified'.`);
|
|
472
639
|
}
|
|
473
640
|
if (node.mutatesSource && node.readinessTemplate) this.assertReady(node, run);
|
|
474
641
|
const attempts = backfilled ? null : this.assertTaskAttempts(node, run);
|
|
642
|
+
this.assertPlanExit(node, run);
|
|
475
643
|
// No ledger for the feature: the stage is recorded, but nothing verified its tasks, and the
|
|
476
644
|
// record says so whatever outcome the caller supplied (FR-006).
|
|
477
645
|
const unledgered = attempts?.state === 'none';
|
|
@@ -483,11 +651,12 @@ class WorkflowOrchestrator {
|
|
|
483
651
|
note: unledgered ? [note, NO_LEDGER_NOTE].filter(Boolean).join(' ') : (note ?? null),
|
|
484
652
|
backfilled: Boolean(backfilled), executionStatus: node.executionStatus, outcome: node.outcome,
|
|
485
653
|
...(consultations ? { consultations } : {}),
|
|
654
|
+
...(packs.length ? { packs } : {}),
|
|
486
655
|
});
|
|
487
656
|
this.advance(run, now);
|
|
488
657
|
this.writeRun(run, now, () => {
|
|
489
658
|
if (run.taskClass !== 'feature') return;
|
|
490
|
-
const request = new ResearchRequestStore({ projectRoot: this.projectRoot, fsImpl: this.fsImpl })
|
|
659
|
+
const request = new ResearchRequestStore({ projectRoot: this.projectRoot, sourceRoot: this.sourceRoot, fsImpl: this.fsImpl })
|
|
491
660
|
.blockingGap(taskId, stageId);
|
|
492
661
|
if (request) throw new Error(`Research request '${request.id}' blocks stage '${stageId}': ${request.failureReason || request.gap || request.status}`);
|
|
493
662
|
});
|
|
@@ -640,15 +809,22 @@ class WorkflowOrchestrator {
|
|
|
640
809
|
* `forced` decision (e.g. approving despite unresolved clarification markers) requires an
|
|
641
810
|
* explicit `note` reason — never a silent override — and is flagged in history for
|
|
642
811
|
* `render-audit.sh` to distinguish from a routine handoff. */
|
|
643
|
-
decideGate({ taskId, gateId, decision, note, forced = false, now } = {}) {
|
|
812
|
+
decideGate({ taskId, gateId, decision, policy, by, note, forced = false, now, bindHead } = {}) {
|
|
644
813
|
assertSafeId(gateId, 'gateId');
|
|
645
|
-
if (
|
|
814
|
+
if (bindHead !== undefined && (policy === undefined || !/^[0-9a-f]{40}$/.test(String(bindHead)))) throw new Error('bindHead is a 40-hex feature head and needs --policy');
|
|
815
|
+
// IC-005: the decider is derived, never named by a caller; `autopilot` is recorded only by --policy.
|
|
816
|
+
if (by !== undefined) throw new Error('decide-gate takes no decider (by); only --policy records autopilot');
|
|
817
|
+
if (policy !== undefined) {
|
|
818
|
+
if (policy !== gateId) throw new Error('--policy must name the gate being decided');
|
|
819
|
+
if (decision !== undefined || forced) throw new Error('--policy refuses --decision and --forced');
|
|
820
|
+
} else if (!GATE_DECISIONS.includes(decision)) throw new Error(`Invalid gate decision '${decision}'; valid: ${GATE_DECISIONS.join(', ')}`);
|
|
646
821
|
if (forced && (!note || !String(note).trim())) throw new Error('A forced gate decision requires a --note reason');
|
|
647
822
|
const run = this.requireRun(taskId);
|
|
648
823
|
if (run.state !== 'AWAITING_GATE') throw new Error(`Run '${run.taskId}' is not awaiting a gate (state=${run.state})`);
|
|
649
824
|
const node = this.currentNode(run);
|
|
650
825
|
if (!node || node.type !== 'gate') throw new Error('Run state says AWAITING_GATE but no gate is at the cursor');
|
|
651
826
|
if (node.id !== gateId) throw new Error(`Expected gate '${node.id}' next, got '${gateId}'`);
|
|
827
|
+
if (policy !== undefined) return this.decideGateByPolicy(run, node, policy, now, bindHead);
|
|
652
828
|
node.status = decision === 'approve' ? 'approved' : 'rejected';
|
|
653
829
|
node.decision = decision;
|
|
654
830
|
run.history.push({ at: iso(now), action: 'decide-gate', node: node.id, detail: decision, note: note ?? null, forced: Boolean(forced) });
|
|
@@ -663,6 +839,94 @@ class WorkflowOrchestrator {
|
|
|
663
839
|
return this.snapshot(run);
|
|
664
840
|
}
|
|
665
841
|
|
|
842
|
+
/** IC-005: the feature checkout cannot grant itself a weaker gate policy. Denies write nothing. */
|
|
843
|
+
decideGateByPolicy(run , node , policyId , now , bindHead ) {
|
|
844
|
+
// Decision C9: the autopilot kill switch binds every policy decision, not only the loop's. Off on
|
|
845
|
+
// any layer (env, sentinel, settings; an unreadable setting fails safe to off) denies before any
|
|
846
|
+
// fact is read, and records nothing.
|
|
847
|
+
const { readKillSwitch } = require('./settings/autopilot.js');
|
|
848
|
+
const killSwitch = readKillSwitch(this.env);
|
|
849
|
+
if (!killSwitch.enabled) {
|
|
850
|
+
return { ...this.snapshot(run), decision: 'deny', reason: 'autopilot-disabled', reasons: ['autopilot-disabled'],
|
|
851
|
+
rule: `${node.id}/autopilot`, policy: { id: policyId, digest: null }, inputs: [], annotations: { switch: killSwitch.switch } };
|
|
852
|
+
}
|
|
853
|
+
// Loaded here, not at module top: every other orchestrator path (and a dry-run install) must not import them.
|
|
854
|
+
const { projectLayerSource } = require('./settings/project-layers.js');
|
|
855
|
+
const { parseSeed } = require('./roadmap/rows.js');
|
|
856
|
+
const { loadPolicy, evaluate } = require('./roadmap/gates.js');
|
|
857
|
+
const { gatherGateFacts } = require('./roadmap/facts.js');
|
|
858
|
+
const root = this.sourceRoot;
|
|
859
|
+
const mainRoot = mainCheckoutOf(root);
|
|
860
|
+
const execOptions = { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] };
|
|
861
|
+
let developHead ;
|
|
862
|
+
let baseBytes = null;
|
|
863
|
+
// The base policy is develop's committed blob, never the worktree or the feature branch: a
|
|
864
|
+
// branch that weakens `never` to `approve` must not approve itself. No textconv filter applies.
|
|
865
|
+
try {
|
|
866
|
+
const head = execFileSync('git', ['rev-parse', '--verify', 'refs/heads/develop'], execOptions).trim();
|
|
867
|
+
developHead = head;
|
|
868
|
+
if (/^[0-9a-f]{40}$/.test(head)) {
|
|
869
|
+
baseBytes = execFileSync('git', ['show', '--no-textconv', `${head}:core/registry/gates.json`], execOptions);
|
|
870
|
+
}
|
|
871
|
+
} catch { /* loadPolicy returns no-policy; never fall back to the feature copy. */ }
|
|
872
|
+
const layers = projectLayerSource(mainRoot, this.fsImpl);
|
|
873
|
+
let overrideBytes = null;
|
|
874
|
+
if (layers.layersFrom) {
|
|
875
|
+
try { overrideBytes = this.fsImpl.readFileSync(path.join(layers.layersFrom, 'gates.override.json'), 'utf8'); }
|
|
876
|
+
catch (error ) { if (error.code !== 'ENOENT') overrideBytes = ''; }
|
|
877
|
+
}
|
|
878
|
+
const loaded = loadPolicy(baseBytes, overrideBytes);
|
|
879
|
+
const policy = { id: policyId, digest: loaded.digest };
|
|
880
|
+
if (!loaded.ok) return { ...this.snapshot(run), decision: 'deny', reasons: loaded.reasons, reason: loaded.reason, policy, inputs: [], ...(loaded.annotations ? { annotations: loaded.annotations } : {}) };
|
|
881
|
+
|
|
882
|
+
const slug = runSlugOf(run, root) || this.featureSlug || run.taskId;
|
|
883
|
+
let seed = null;
|
|
884
|
+
let row = null;
|
|
885
|
+
try {
|
|
886
|
+
seed = parseSeed(this.fsImpl.readFileSync(path.join(mainRoot, 'agent-docs/roadmap/rows.json'), 'utf8'));
|
|
887
|
+
row = seed.rows.find((entry ) => entry.slug === slug && entry.class === run.taskClass) || null;
|
|
888
|
+
} catch (error ) {
|
|
889
|
+
if (error.code === 'seed-typed-status') return { ...this.snapshot(run), decision: 'deny', reason: error.code, reasons: [error.code], policy, inputs: [] };
|
|
890
|
+
// Invalid or unreadable seed means no trustworthy row; evaluate returns row-unknown.
|
|
891
|
+
}
|
|
892
|
+
// The feature folder follows the feature-store rule (resolveFeatureInStore): a linked worktree
|
|
893
|
+
// without the folder would otherwise read no artifacts (a false `unresolved-clarifications`) and
|
|
894
|
+
// register into a missing folder.
|
|
895
|
+
const feature = resolveFeatureInStore({ root, slug });
|
|
896
|
+
if (feature.error) throw new Error(`Cannot resolve policy gate artifacts: ${feature.message}`);
|
|
897
|
+
const facts = gatherGateFacts({ root, mainRoot, registryRoot: this.repoRoot, artifactRoot: feature.featureDir,
|
|
898
|
+
gateId: node.id, policy: loaded.policy, row, rows: seed?.rows, developHead, featureBranch: `feat/${slug}`, fsImpl: this.fsImpl, env: this.env });
|
|
899
|
+
const evaluation = evaluate(loaded.policy, run.taskClass, node.id, facts);
|
|
900
|
+
// A deny changes nothing: the run stays AWAITING_GATE and autopilot never rejects.
|
|
901
|
+
if (evaluation.decision === 'deny') return { ...this.snapshot(run), ...evaluation, policy };
|
|
902
|
+
|
|
903
|
+
const audit = `approved by policy ${policyId}@${loaded.digest} (${evaluation.inputs.length} inputs)`;
|
|
904
|
+
node.status = 'approved';
|
|
905
|
+
node.decision = 'approve';
|
|
906
|
+
// `policy.digest` and every fact's {locator, sha256} let a later reader re-derive this decision.
|
|
907
|
+
// C15: the roadmap loop names the feature head it read before deciding (bindHead). The entry keeps
|
|
908
|
+
// it as `approvedHead` only when the gate's own heads fact evaluated that same head, so the merge
|
|
909
|
+
// binds to it across loop runs; an approval without it falls back to the both-heads digest.
|
|
910
|
+
const heads = facts.find((fact ) => fact.name === 'heads' && !fact.unreadable)?.value;
|
|
911
|
+
const approvedHead = bindHead && heads?.featureHead === bindHead ? { approvedHead: bindHead } : {};
|
|
912
|
+
run.history.push({ at: iso(now), action: 'decide-gate', node: node.id, detail: 'approve', by: 'autopilot',
|
|
913
|
+
policy, inputs: evaluation.inputs, ...approvedHead, note: audit, forced: false });
|
|
914
|
+
run.state = runState('RUNNING');
|
|
915
|
+
run.cursor += 1;
|
|
916
|
+
this.settleCursor(run);
|
|
917
|
+
// The optimistic revision check runs before registering a decision, so a stale run cannot
|
|
918
|
+
// leave a forged approval. A register refusal likewise prevents the journal from advancing.
|
|
919
|
+
this.writeRun(run, now, () => {
|
|
920
|
+
const registered = addDecisions({ featureDir: feature.featureDir, slug, now: now ?? new Date(), fsImpl: this.fsImpl, items: [{
|
|
921
|
+
topic: node.id, statement: `${node.id} ${audit}.`, channel: 'policy', stage: POLICY_GATE_STAGE[node.id],
|
|
922
|
+
rationale: `Every predicate of ${node.id} passed on the inputs digest ${evaluation.inputsDigest}.`,
|
|
923
|
+
source: `git:${developHead}:core/registry/gates.json`,
|
|
924
|
+
}] });
|
|
925
|
+
if (registered.finding) throw new Error(`Cannot register policy approval: ${registered.finding}: ${registered.message}`);
|
|
926
|
+
});
|
|
927
|
+
return { ...this.snapshot(run), ...evaluation, policy };
|
|
928
|
+
}
|
|
929
|
+
|
|
666
930
|
/** Append a pure audit-trail entry — a stage re-run after its own handoff, or an artifact
|
|
667
931
|
* hand-edited after that stage's own handoff — without touching program/cursor/state. Not a
|
|
668
932
|
* stage completion or a gate decision, so it never collides with the state machine's ordering
|
|
@@ -731,6 +995,20 @@ class WorkflowOrchestrator {
|
|
|
731
995
|
return answer;
|
|
732
996
|
}
|
|
733
997
|
|
|
998
|
+
/** Row 134: the planning stage (the `do-plan` stage) cannot complete while the plan's
|
|
999
|
+
* Constitution Check leaves a MUST principle marked FAIL or without a row. There is no waiver:
|
|
1000
|
+
* the plan is revised, or a person amends the constitution. A stage catch-up backfills is checked
|
|
1001
|
+
* too, so a refused plan is never imported as complete. */
|
|
1002
|
+
assertPlanExit(node , run ) {
|
|
1003
|
+
if (!this.planExitCheck || node.skill !== 'do-plan') return null;
|
|
1004
|
+
const answer = this.planExitCheck(node, run);
|
|
1005
|
+
if (!answer || !Array.isArray(answer.refusals)) throw new Error(`Plan exit check returned no usable answer for stage '${node.id}'`);
|
|
1006
|
+
if (answer.refusals.length) {
|
|
1007
|
+
throw new Error(`Stage '${node.id}' cannot be completed: ${answer.refusals.map((f ) => `${f.path}:${f.line} ${f.message}`).join('; ')}`);
|
|
1008
|
+
}
|
|
1009
|
+
return answer;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
734
1012
|
/** Where `catchUp` would stop for these candidates, worked out on a copy of the run's program (or
|
|
735
1013
|
* of a fresh run's) without writing anything. Same stop rules and reason vocabulary as `catchUp`;
|
|
736
1014
|
* `stageId` is what `catchUp` reports as `caughtUpTo`, and `node` the program node it stops on.
|
|
@@ -768,6 +1046,26 @@ class WorkflowOrchestrator {
|
|
|
768
1046
|
}
|
|
769
1047
|
}
|
|
770
1048
|
|
|
1049
|
+
/** The project's main checkout, or `root` itself when the checkouts cannot be listed. Loaded here, not at
|
|
1050
|
+
* module top: every other orchestrator path (and a dry-run install) must not import the resolver. */
|
|
1051
|
+
function mainCheckoutOf(root ) {
|
|
1052
|
+
const { listCheckouts } = require('./checkouts.js');
|
|
1053
|
+
const checkouts = listCheckouts({ cwd: root });
|
|
1054
|
+
return checkouts.ok && checkouts.main ? checkouts.main : root;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* The feature folder follows the feature-store rule, which the feature resolver already applies: the
|
|
1059
|
+
* calling checkout's own folder, else the main checkout's when a linked worktree holds none
|
|
1060
|
+
* (`featureFolder` under `featureRoot`). `featureDir` and `repoRoot` of the result name that folder and
|
|
1061
|
+
* checkout, as `decision --action compact` reads them, so a caller never looks in the wrong checkout.
|
|
1062
|
+
*/
|
|
1063
|
+
function resolveFeatureInStore({ root, slug } ) {
|
|
1064
|
+
const feature = resolveActiveFeature({ projectRoot: root, slug });
|
|
1065
|
+
if (feature.error) return feature;
|
|
1066
|
+
return { ...feature, featureDir: feature.featureFolder, repoRoot: feature.featureRoot };
|
|
1067
|
+
}
|
|
1068
|
+
|
|
771
1069
|
/**
|
|
772
1070
|
* IC-013: fold the finished stage's History into the feature's archive. The task id is the feature
|
|
773
1071
|
* slug. This is housekeeping after a recorded handoff, so it reports and never throws: the
|
|
@@ -777,7 +1075,7 @@ class WorkflowOrchestrator {
|
|
|
777
1075
|
*/
|
|
778
1076
|
function compactAfterHandoff({ taskId, projectRoot } ) {
|
|
779
1077
|
try {
|
|
780
|
-
const feature =
|
|
1078
|
+
const feature = resolveFeatureInStore({ root: projectRoot, slug: taskId });
|
|
781
1079
|
if (feature.error) return { status: 'skipped', reason: feature.message };
|
|
782
1080
|
// A task id such as a plan task (`B.1`) resolves on a feature branch to a folder that was
|
|
783
1081
|
// never created; say that, rather than blaming a missing register.
|
|
@@ -797,41 +1095,242 @@ function compactAfterHandoff({ taskId, projectRoot } ) {
|
|
|
797
1095
|
}
|
|
798
1096
|
}
|
|
799
1097
|
|
|
1098
|
+
/**
|
|
1099
|
+
* IC-001: `orchestrate --action list`, every run the resolver can see as a prompt-free row plus the
|
|
1100
|
+
* records it could not read, by reason code only. Reads and prints; writes nothing.
|
|
1101
|
+
*/
|
|
1102
|
+
/**
|
|
1103
|
+
* The first flag given that a read action does not take, or undefined. Every flag another action
|
|
1104
|
+
* reads is refused before any read: a filter the read does not apply must not look applied, and an
|
|
1105
|
+
* outcome nothing records must not look recorded. `takes` names the flags the action does read.
|
|
1106
|
+
*/
|
|
1107
|
+
function noEffectFlag({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun } , takes = []) {
|
|
1108
|
+
const supplied = (value ) => value !== undefined && value !== null;
|
|
1109
|
+
const given = [
|
|
1110
|
+
['--task-id', supplied(taskId)], ['--task-class', supplied(taskClass)], ['--gate', supplied(gate)],
|
|
1111
|
+
['--decision', supplied(decision)], ['--policy', supplied(policy)],
|
|
1112
|
+
['--forced', forced], ['--note', supplied(note)], ['--reason', supplied(reason)], ['--stage', supplied(stage)],
|
|
1113
|
+
['--node', supplied(node)], ['--verification-plan', supplied(verificationPlan)], ['--scope', supplied(scope)],
|
|
1114
|
+
['--invariants', supplied(invariants)], ['--result', supplied(result)], ['--mcp-outcome', supplied(mcpOutcome)],
|
|
1115
|
+
['--calling-skill', supplied(callingSkill)], ['--slug', supplied(slug)], ['--dry-run', Boolean(dryRun)],
|
|
1116
|
+
].find(([flag, on]) => on && !takes.includes(flag ));
|
|
1117
|
+
return given ? given[0] : undefined;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** A journal time as an ISO instant, else null, so no other journal text reaches a read's output. */
|
|
1121
|
+
const journalTime = (value ) => typeof value === 'string'
|
|
1122
|
+
&& /^\d{4}-\d{2}-\d{2}T/.test(value) && !Number.isNaN(Date.parse(value)) ? value : null;
|
|
1123
|
+
|
|
1124
|
+
/** The journal actions a timeline shows (093 IC-012); any other is counted in `otherEvents`. */
|
|
1125
|
+
const TIMELINE_ACTIONS = new Set(['start', 'complete-stage', 'skip-stage', 'skip-gate', 'decide-gate', 'annotate', 'complete-run']);
|
|
1126
|
+
const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* 093 IC-012: one run's history as closed values only. Each event copies the action, an id-shaped
|
|
1130
|
+
* node, the stage outcome (complete-stage), the gate decision (decide-gate's `detail`), `forced`, and
|
|
1131
|
+
* whether the entry records no work done (the indicators rule, trace/indicators.ts isSynthetic).
|
|
1132
|
+
* Notes, detail text, deciders, policy inputs, consultations and trace ids are never read.
|
|
1133
|
+
*/
|
|
1134
|
+
function timelineOf(run , taskId , featureSlug ) {
|
|
1135
|
+
const at = (value ) => journalTime(value) !== null && ISO_INSTANT.test(value) ? value : null;
|
|
1136
|
+
const events = [];
|
|
1137
|
+
let otherEvents = 0;
|
|
1138
|
+
for (const entry of Array.isArray(run.history) ? run.history : []) {
|
|
1139
|
+
if (!entry || typeof entry !== 'object' || !TIMELINE_ACTIONS.has(entry.action)) {
|
|
1140
|
+
otherEvents += 1;
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
events.push({
|
|
1144
|
+
at: at(entry.at),
|
|
1145
|
+
action: entry.action,
|
|
1146
|
+
node: typeof entry.node === 'string' && ID_PATTERN.test(entry.node) ? entry.node : null,
|
|
1147
|
+
outcome: entry.action === 'complete-stage' && STAGE_OUTCOMES.has(entry.outcome) ? entry.outcome : null,
|
|
1148
|
+
decision: entry.action === 'decide-gate' && GATE_DECISIONS.includes(entry.detail) ? entry.detail : null,
|
|
1149
|
+
forced: entry.forced === true,
|
|
1150
|
+
synthetic: Boolean(entry.backfilled) || Boolean(entry.executionStatus && entry.executionStatus !== 'completed'),
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
v: 1,
|
|
1155
|
+
taskId,
|
|
1156
|
+
featureSlug: typeof featureSlug === 'string' && ID_PATTERN.test(featureSlug) ? featureSlug : null,
|
|
1157
|
+
revision: Number.isInteger(run.revision) && run.revision >= 0 ? run.revision : 0,
|
|
1158
|
+
startedAt: at(run.startedAt),
|
|
1159
|
+
events,
|
|
1160
|
+
otherEvents,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function listRuns({ json, stateRoot, ...flags } ) {
|
|
1165
|
+
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
1166
|
+
// list takes no filter, so a `--slug` must not look like one.
|
|
1167
|
+
const noEffect = noEffectFlag(flags);
|
|
1168
|
+
if (noEffect) return usageError('orchestrate', `'${noEffect}' has no effect on action 'list'`, json);
|
|
1169
|
+
|
|
1170
|
+
const resolverFailure = () => {
|
|
1171
|
+
if (json) console.log(JSON.stringify({ v: 1, error: 'cannot-list-checkouts' }, null, 2));
|
|
1172
|
+
else console.error('[ERROR] orchestrate: cannot list checkouts');
|
|
1173
|
+
return finishRuntime(1);
|
|
1174
|
+
};
|
|
1175
|
+
let resolved ;
|
|
1176
|
+
try { resolved = orchestrationRuns({ stateRoot: stateRoot || process.cwd() }); } catch { return resolverFailure(); }
|
|
1177
|
+
if (!resolved.ok) return resolverFailure();
|
|
1178
|
+
|
|
1179
|
+
const runs = [];
|
|
1180
|
+
const unreadable = [];
|
|
1181
|
+
const textCompare = (a , b ) => a < b ? -1 : a > b ? 1 : 0;
|
|
1182
|
+
const slugCompare = (a , b ) => a === null ? (b === null ? 0 : -1)
|
|
1183
|
+
: b === null ? 1 : textCompare(a, b);
|
|
1184
|
+
const addUnreadable = (entry , reason ) => {
|
|
1185
|
+
const relative = path.relative(runStoreDir(entry.root), entry.file);
|
|
1186
|
+
const parts = relative.split(path.sep);
|
|
1187
|
+
unreadable.push({
|
|
1188
|
+
taskId: path.basename(entry.file, '.json'),
|
|
1189
|
+
featureSlug: parts.length > 1 ? parts[0] : null,
|
|
1190
|
+
origin: entry.origin,
|
|
1191
|
+
reason,
|
|
1192
|
+
});
|
|
1193
|
+
};
|
|
1194
|
+
// One row per run, the copy status reads (C-2): a shadowed flat or another checkout's copy is dropped.
|
|
1195
|
+
for (const entry of selectRunEntries(resolved.entries)) {
|
|
1196
|
+
if (entry.error) {
|
|
1197
|
+
addUnreadable(entry, entry.error);
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const record = entry.record;
|
|
1201
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
1202
|
+
addUnreadable(entry, 'shape');
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (record.version !== 1) {
|
|
1206
|
+
addUnreadable(entry, 'schema-version');
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
const relative = path.relative(runStoreDir(entry.root), entry.file);
|
|
1210
|
+
const parts = relative.split(path.sep);
|
|
1211
|
+
const folderSlug = parts.length > 1 ? parts[0] : null;
|
|
1212
|
+
const stem = path.basename(entry.file, '.json');
|
|
1213
|
+
// Every id a row prints is checked, so no newline or escape byte from a record reaches the text form.
|
|
1214
|
+
if (typeof record.taskId !== 'string' || !ID_PATTERN.test(record.taskId) || record.taskId !== stem
|
|
1215
|
+
|| (folderSlug && typeof record.featureSlug === 'string' && record.featureSlug !== folderSlug)
|
|
1216
|
+
|| (entry.slug !== null && entry.slug !== undefined && !(typeof entry.slug === 'string' && ID_PATTERN.test(entry.slug)))) {
|
|
1217
|
+
addUnreadable(entry, 'id-mismatch');
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (!RUN_STATES.includes(record.state)) {
|
|
1221
|
+
addUnreadable(entry, 'shape');
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
let snapshot ;
|
|
1225
|
+
try { snapshot = snapshotRun(record); } catch {
|
|
1226
|
+
addUnreadable(entry, 'shape');
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
if (snapshot.current && !(typeof snapshot.current.id === 'string' && ID_PATTERN.test(snapshot.current.id))) {
|
|
1230
|
+
addUnreadable(entry, 'shape');
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
const terminal = snapshot.terminalStage;
|
|
1234
|
+
runs.push({
|
|
1235
|
+
taskId: snapshot.taskId,
|
|
1236
|
+
featureSlug: entry.slug,
|
|
1237
|
+
taskClass: snapshot.taskClass,
|
|
1238
|
+
workflowName: snapshot.workflowName,
|
|
1239
|
+
state: snapshot.state,
|
|
1240
|
+
current: snapshot.current,
|
|
1241
|
+
awaitingGate: snapshot.awaitingGate ? { gateId: snapshot.awaitingGate.gateId, name: snapshot.awaitingGate.name } : null,
|
|
1242
|
+
progress: snapshot.progress,
|
|
1243
|
+
// IC-001: the stage's identity only; its gate list (and the prompts in it) stays in status.
|
|
1244
|
+
terminalStage: terminal && typeof terminal === 'object'
|
|
1245
|
+
? { id: terminal.id ?? null, skill: terminal.skill ?? null, kind: terminal.kind ?? null } : null,
|
|
1246
|
+
revision: snapshot.revision,
|
|
1247
|
+
startedAt: journalTime(record.startedAt),
|
|
1248
|
+
updatedAt: journalTime(record.updatedAt),
|
|
1249
|
+
origin: entry.origin,
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
runs.sort((a, b) => {
|
|
1254
|
+
if (a.updatedAt === null && b.updatedAt !== null) return 1;
|
|
1255
|
+
if (a.updatedAt !== null && b.updatedAt === null) return -1;
|
|
1256
|
+
if (a.updatedAt !== null && b.updatedAt !== null) {
|
|
1257
|
+
const byTime = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
1258
|
+
if (byTime !== 0) return byTime;
|
|
1259
|
+
}
|
|
1260
|
+
return slugCompare(a.featureSlug, b.featureSlug) || textCompare(a.taskId, b.taskId);
|
|
1261
|
+
});
|
|
1262
|
+
unreadable.sort((a, b) => textCompare(a.origin, b.origin)
|
|
1263
|
+
|| slugCompare(a.featureSlug, b.featureSlug) || textCompare(a.taskId, b.taskId));
|
|
1264
|
+
|
|
1265
|
+
if (json) {
|
|
1266
|
+
console.log(JSON.stringify({ v: 1, runs, unreadable }, null, 2));
|
|
1267
|
+
} else {
|
|
1268
|
+
for (const row of runs) console.log(`${row.state} ${row.featureSlug || '-'} ${row.taskId} ${row.current ? row.current.id : '-'} rev ${row.revision}`);
|
|
1269
|
+
if (unreadable.length > 0) console.log(`unreadable: ${unreadable.length}`);
|
|
1270
|
+
}
|
|
1271
|
+
return finishRuntime(0);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
800
1274
|
/** CLI handler for `doflow orchestrate`. The run journal lives in the CALLER's project state
|
|
801
1275
|
* (like evidence), while templates come from this install — same two-roots split readiness uses.
|
|
802
1276
|
* Completing a source-mutating gated stage needs a READY readiness record of the stage's template,
|
|
803
1277
|
* made by `doflow readiness` before this call; nothing is evaluated here. */
|
|
804
1278
|
function handleOrchestrateCommand({
|
|
805
|
-
action = 'status', taskId, taskClass, stage, gate, node, decision, note, reason, forced = false,
|
|
806
|
-
verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, json = false, repoRoot, stateRoot, slug = null, buildAdapters = null,
|
|
1279
|
+
action = 'status', taskId, taskClass, stage, gate, node, decision, policy, note, reason, forced = false,
|
|
1280
|
+
verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, json = false, repoRoot, stateRoot, sourceRoot, slug = null, recordSlug, buildAdapters = null, dryRun = false,
|
|
807
1281
|
} = {}) {
|
|
808
1282
|
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
809
1283
|
|
|
1284
|
+
if (action === 'list') {
|
|
1285
|
+
return listRuns({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun, json, stateRoot });
|
|
1286
|
+
}
|
|
1287
|
+
// 093 IC-012: timeline reads one run, so it takes the two flags that name it and refuses the rest.
|
|
1288
|
+
if (action === 'timeline') {
|
|
1289
|
+
const noEffect = noEffectFlag({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun }, ['--task-id', '--slug']);
|
|
1290
|
+
if (noEffect) return usageError('orchestrate', `'${noEffect}' has no effect on action 'timeline'`, json);
|
|
1291
|
+
if (!taskId) return usageError('orchestrate', 'timeline requires --task-id', json);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// `start` writes the run; --dry-run would otherwise be ignored and a real run started.
|
|
1295
|
+
if (action === 'start' && dryRun) return usageError('orchestrate', "'--dry-run' has no effect on action 'start'", json);
|
|
1296
|
+
|
|
810
1297
|
const root = repoRoot || REPO_ROOT;
|
|
811
1298
|
// Same scope rule every other stateful command uses (evidenceRoot(o) in runtime-commands.js):
|
|
812
1299
|
// a bare `stateRoot: stateRoot || process.cwd()` silently ignored `--global`, so `orchestrate`
|
|
813
1300
|
// wrote its journal to $PWD while a `--global` `evidence`/`readiness` call on the same task read
|
|
814
1301
|
// and wrote $HOME — two roots disagreeing about where one task's state lives.
|
|
815
1302
|
const state = stateRoot || process.cwd();
|
|
1303
|
+
// The checkout the verb runs in: its branch and feature folder name the feature; `state` (the main
|
|
1304
|
+
// checkout for a linked worktree, 115 IC-004) holds the records.
|
|
1305
|
+
const source = sourceRoot || state;
|
|
816
1306
|
const safeId = ID_PATTERN.test(String(taskId ?? ''));
|
|
817
|
-
// The feature this task belongs to: recorded on a run this command starts,
|
|
818
|
-
//
|
|
819
|
-
|
|
1307
|
+
// The feature this task belongs to: recorded on a run this command starts, the namespace its run
|
|
1308
|
+
// and readiness record live under (115 C-1), and matched against a run found in another checkout.
|
|
1309
|
+
// The dispatcher passes the call's record slug, computed once for every store the call touches.
|
|
1310
|
+
let featureSlug = recordSlug !== undefined ? recordSlug : safeId ? featureSlugFor({ projectRoot: source, taskId, slug }) : slug;
|
|
820
1311
|
// A run started in another checkout of this repository for the same feature is read and updated
|
|
821
1312
|
// where it is. `start` is always local: an unrelated run with the same id elsewhere neither
|
|
822
1313
|
// refuses nor satisfies it.
|
|
823
1314
|
let runRoot = state;
|
|
824
1315
|
if (safeId && action !== 'start') {
|
|
825
|
-
|
|
1316
|
+
// `status` (and `timeline`, which reads the same run) with no --slug opens the one run the list
|
|
1317
|
+
// action shows for the id, whichever feature namespace holds it (FU-wrnnaq); more than one is
|
|
1318
|
+
// refused until --slug names the feature.
|
|
1319
|
+
const found = findRunFile({ stateRoot: state, taskId, slug: featureSlug, anyNamespace: (action === 'status' || action === 'timeline') && !slug });
|
|
826
1320
|
if (found.status === 'ambiguous') {
|
|
827
|
-
console.error(
|
|
1321
|
+
console.error(found.byFeature
|
|
1322
|
+
? `[ERROR] orchestrate: run '${taskId}' exists under more than one feature (${found.candidates.join(', ')}); pass --slug <feature> to name the one you mean. Nothing was changed.`
|
|
1323
|
+
: `[ERROR] orchestrate: run '${taskId}' exists in more than one other checkout (${found.candidates.join(', ')}); run the command from the checkout that holds the run you mean. Nothing was changed.`);
|
|
828
1324
|
return finishRuntime(1);
|
|
829
1325
|
}
|
|
830
|
-
if (found.status === 'found')
|
|
1326
|
+
if (found.status === 'found') {
|
|
1327
|
+
runRoot = found.root;
|
|
1328
|
+
if (found.slug !== undefined) featureSlug = found.slug;
|
|
1329
|
+
}
|
|
831
1330
|
}
|
|
832
1331
|
const orchestrator = new WorkflowOrchestrator({
|
|
833
|
-
repoRoot: root, projectRoot: runRoot,
|
|
834
|
-
stateDir:
|
|
1332
|
+
repoRoot: root, projectRoot: runRoot, sourceRoot: source,
|
|
1333
|
+
stateDir: runStoreDir(runRoot),
|
|
835
1334
|
featureSlug, buildAdapters,
|
|
836
1335
|
});
|
|
837
1336
|
orchestrator.readinessEvaluate = (node , run ) => {
|
|
@@ -865,12 +1364,28 @@ function handleOrchestrateCommand({
|
|
|
865
1364
|
slug: runSlugOf(run, runRoot) || featureSlug, cwd: state,
|
|
866
1365
|
});
|
|
867
1366
|
|
|
1367
|
+
// Row 134. The plan is read in the checkout the verb runs in first, then where the run lives; a
|
|
1368
|
+
// SHOULD principle marked FAIL is one note, printed once although the check runs twice.
|
|
1369
|
+
let planAdvised = false;
|
|
1370
|
+
orchestrator.planExitCheck = (node , run ) => {
|
|
1371
|
+
const { planStageExit } = require('./plan-content.js');
|
|
1372
|
+
const answer = planStageExit({ roots: [...new Set([source, runRoot])], slug: runSlugOf(run, runRoot) || featureSlug });
|
|
1373
|
+
if (!planAdvised) {
|
|
1374
|
+
planAdvised = true;
|
|
1375
|
+
for (const f of answer.advisories) console.error(`orchestrate: note: ${f.path}:${f.line} ${f.message}`);
|
|
1376
|
+
}
|
|
1377
|
+
return answer;
|
|
1378
|
+
};
|
|
1379
|
+
|
|
868
1380
|
if ((action === 'handoff' || action === 'complete-stage')
|
|
869
1381
|
&& [verificationPlan, scope, invariants].some((v) => typeof v === 'string' && v.trim() !== '')) {
|
|
870
1382
|
console.error(`orchestrate: note: --verification-plan, --scope and --invariants are inputs to doflow-run readiness; this ${action} did not read them.`);
|
|
871
1383
|
}
|
|
872
1384
|
|
|
873
1385
|
try {
|
|
1386
|
+
if (policy !== undefined && (action !== 'decide-gate' || decision !== undefined || forced || policy !== gate)) {
|
|
1387
|
+
return usageError('orchestrate', '--policy requires decide-gate with the matching --gate and refuses --decision and --forced', json);
|
|
1388
|
+
}
|
|
874
1389
|
// `--forced` is only read by the two actions that can override a decision. Accepting it
|
|
875
1390
|
// silently on any other action lets an operator believe a stage completion or skip was
|
|
876
1391
|
// recorded as forced when nothing recorded it — refuse instead of dropping the flag.
|
|
@@ -886,6 +1401,8 @@ function handleOrchestrateCommand({
|
|
|
886
1401
|
if (mcpOutcome !== undefined && action !== 'handoff') {
|
|
887
1402
|
return usageError('orchestrate', `'--mcp-outcome' has no effect on action '${action}' — only handoff records MCP outcomes`, json);
|
|
888
1403
|
}
|
|
1404
|
+
// A catch-up on an absent run starts it implicitly, which counts as a run start for the pointer.
|
|
1405
|
+
const absentBefore = action === 'catch-up' && taskId ? !orchestrator.readRun(taskId) : false;
|
|
889
1406
|
let snapshot;
|
|
890
1407
|
switch (action) {
|
|
891
1408
|
case 'handoff':
|
|
@@ -910,8 +1427,8 @@ function handleOrchestrateCommand({
|
|
|
910
1427
|
snapshot = orchestrator.skipStage({ taskId, stageId: stage, reason: reason ?? note });
|
|
911
1428
|
break;
|
|
912
1429
|
case 'decide-gate':
|
|
913
|
-
if (!taskId || !gate || !decision) return usageError('orchestrate', 'decide-gate requires --task-id, --gate and --decision approve|reject', json);
|
|
914
|
-
snapshot = orchestrator.decideGate({ taskId, gateId: gate, decision, note, forced });
|
|
1430
|
+
if (!taskId || !gate || (policy === undefined && !decision)) return usageError('orchestrate', 'decide-gate requires --task-id, --gate and --decision approve|reject or --policy <gateId>', json);
|
|
1431
|
+
snapshot = orchestrator.decideGate({ taskId, gateId: gate, decision, policy, note, forced });
|
|
915
1432
|
break;
|
|
916
1433
|
case 'annotate':
|
|
917
1434
|
if (!taskId || !node || !note) return usageError('orchestrate', 'annotate requires --task-id, --node and --note', json);
|
|
@@ -921,18 +1438,44 @@ function handleOrchestrateCommand({
|
|
|
921
1438
|
if (!taskId) return usageError('orchestrate', 'status requires --task-id', json);
|
|
922
1439
|
snapshot = orchestrator.status(taskId);
|
|
923
1440
|
break;
|
|
1441
|
+
case 'timeline': {
|
|
1442
|
+
const run = orchestrator.requireRun(taskId);
|
|
1443
|
+
const body = timelineOf(run, taskId, runSlugOf(run, runRoot) || featureSlug);
|
|
1444
|
+
if (json) console.log(JSON.stringify(body, null, 2));
|
|
1445
|
+
else {
|
|
1446
|
+
console.log(`Timeline ${body.taskId} ${body.featureSlug || '-'} rev ${body.revision}`);
|
|
1447
|
+
for (const e of body.events) {
|
|
1448
|
+
console.log(`${e.at || '-'} ${e.action} ${e.node || '-'} ${e.outcome || e.decision || '-'}${e.forced ? ' forced' : ''}${e.synthetic ? ' synthetic' : ''}`);
|
|
1449
|
+
}
|
|
1450
|
+
if (body.otherEvents > 0) console.log(`other events: ${body.otherEvents}`);
|
|
1451
|
+
}
|
|
1452
|
+
return finishRuntime(0);
|
|
1453
|
+
}
|
|
924
1454
|
default:
|
|
925
|
-
return usageError('orchestrate', `unknown action '${action}'; valid: start | status | handoff | catch-up | complete-stage | skip-stage | decide-gate | annotate`, json);
|
|
1455
|
+
return usageError('orchestrate', `unknown action '${action}'; valid: start | status | list | timeline | handoff | catch-up | complete-stage | skip-stage | decide-gate | annotate`, json);
|
|
1456
|
+
}
|
|
1457
|
+
// Row 135: the feature pointer follows a `feature` run: set when it starts (`start`, or the `catch-up` that
|
|
1458
|
+
// starts it), dropped once it completes.
|
|
1459
|
+
if ((action === 'start' || absentBefore) && snapshot.taskClass === 'feature' && snapshot.state !== 'COMPLETED') {
|
|
1460
|
+
require('./feature-pointer.js').recordFeature({ storeRoot: state, cwd: source, slug: featureSlug, taskId });
|
|
1461
|
+
} else if (action !== 'status' && snapshot.taskClass === 'feature' && snapshot.state === 'COMPLETED') {
|
|
1462
|
+
require('./feature-pointer.js').clearCheckout({ storeRoot: state, cwd: source, slug: featureSlug });
|
|
926
1463
|
}
|
|
927
1464
|
// Only a handoff that recorded work compacts; `deferred` and `standalone` recorded nothing.
|
|
928
1465
|
if (action === 'handoff' && (snapshot.disposition === 'completed' || snapshot.disposition === 'annotated')) {
|
|
929
|
-
snapshot = { ...snapshot, compaction: compactAfterHandoff({ taskId, projectRoot:
|
|
1466
|
+
snapshot = { ...snapshot, compaction: compactAfterHandoff({ taskId, projectRoot: source }) };
|
|
930
1467
|
}
|
|
931
1468
|
if (json) { console.log(JSON.stringify(snapshot, null, 2)); return finishRuntime(0); }
|
|
932
1469
|
console.log(`Workflow ${snapshot.taskId} [${snapshot.taskClass}] — ${snapshot.state}`);
|
|
1470
|
+
// `status` with no --slug opens the one run of the id under any feature (FU-wrnnaq): say whose it is.
|
|
1471
|
+
if (action === 'status' && featureSlug) console.log(`Feature: ${featureSlug}`);
|
|
933
1472
|
if (snapshot.disposition === 'standalone') console.log(`Handoff: standalone (${snapshot.reason})`);
|
|
934
1473
|
else if (snapshot.disposition) console.log(`Handoff: ${snapshot.disposition}`);
|
|
935
1474
|
if (snapshot.compaction) console.log(`Compaction: ${snapshot.compaction.status}${snapshot.compaction.reason ? ` — ${snapshot.compaction.reason}` : ''}`);
|
|
1475
|
+
if (snapshot.policy) console.log(snapshot.decision === 'pass'
|
|
1476
|
+
? `${gate} ${snapshot.taskId} approved by policy ${snapshot.policy.id}@${snapshot.policy.digest} (${snapshot.inputs.length} inputs)`
|
|
1477
|
+
: `${gate} ${snapshot.taskId} denied by policy: ${snapshot.reasons.join(', ')}`);
|
|
1478
|
+
if (snapshot.annotations?.hint) console.log(` ${snapshot.annotations.hint}`);
|
|
936
1479
|
if (snapshot.progress) console.log(`Progress: ${snapshot.progress.done}/${snapshot.progress.total}`);
|
|
937
1480
|
if (snapshot.current) {
|
|
938
1481
|
console.log(snapshot.awaitingGate
|
|
@@ -950,4 +1493,4 @@ function handleOrchestrateCommand({
|
|
|
950
1493
|
}
|
|
951
1494
|
}
|
|
952
1495
|
|
|
953
|
-
module.exports = { WorkflowOrchestrator, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand };
|
|
1496
|
+
module.exports = { WorkflowOrchestrator, loopWorkflow, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand, recordStagePack };
|