@khoavu882/doflow 2.0.0-beta.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -11
- package/bin/doflow.js +21 -11
- package/core/.antigravity-plugin/plugin.json +1 -3
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/claude/settings/settings.json +18 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
- package/core/harnesses/codex/agents/core-implementer.toml +1 -0
- package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
- package/core/harnesses/codex/agents/research-writer.toml +1 -1
- package/core/harnesses/codex/agents/reviewer.toml +10 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
- package/core/harnesses/codex/agents/system-architect.toml +1 -1
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow.json +57 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -1
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
- package/core/harnesses/kiro/hooks/session-start.sh +12 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
- package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
- package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +72 -80
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +60 -33
- package/core/registry/contribution-points.json +32 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1542 -277
- package/core/registry/lifecycle.json +2837 -37
- package/core/registry/mcp.json +5 -1
- package/core/registry/models.json +7 -1
- package/core/registry/package-templates/README.md +19 -0
- package/core/registry/package-templates/agent/files/agent.md +9 -0
- package/core/registry/package-templates/agent/template.json +4 -0
- package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
- package/core/registry/package-templates/capability-provider/template.json +11 -0
- package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
- package/core/registry/package-templates/debate-preset/template.json +4 -0
- package/core/registry/package-templates/review-preset/files/preset.json +30 -0
- package/core/registry/package-templates/review-preset/template.json +4 -0
- package/core/registry/package-templates/skill/files/SKILL.md +8 -0
- package/core/registry/package-templates/skill/template.json +4 -0
- package/core/registry/package-templates/worker/files/worker.json +92 -0
- package/core/registry/package-templates/worker/template.json +11 -0
- package/core/registry/packages/doflow/doflow-package.json +15 -0
- package/core/registry/packages/doflow/presets/debate-3.json +26 -0
- package/core/registry/protocols.json +61 -0
- package/core/registry/routes.json +17 -0
- package/core/registry/rules.json +431 -0
- package/core/registry/settings.json +276 -0
- package/core/registry/storage.json +701 -30
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -1
- package/core/registry/workflows.json +55 -0
- package/core/shared/agent-specs/core-implementer.md +3 -0
- package/core/shared/agent-specs/delivery-lead.md +68 -0
- package/core/shared/agent-specs/quality-guardian.md +5 -1
- package/core/shared/agent-specs/research-writer.md +10 -3
- package/core/shared/agent-specs/reviewer.md +53 -0
- package/core/shared/agent-specs/spec-analyst.md +5 -6
- package/core/shared/agent-specs/system-architect.md +5 -1
- package/core/shared/guidance/DOFLOW_CORE.md +1 -5
- package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
- package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
- package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
- package/core/shared/guidance/pointers/codex.md +1 -4
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
- package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
- package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
- package/core/shared/guidance/rules/RULES.md +45 -0
- package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
- package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
- package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
- package/core/shared/scripts/doflow/bin/doflow-run +154 -35
- package/core/shared/skills/do/SKILL.md +12 -10
- package/core/shared/skills/do/references/advice_calls.md +26 -0
- package/core/shared/skills/do/references/pm_routing.md +1 -2
- package/core/shared/skills/do/references/program_mode.md +31 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
- package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
- package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
- package/core/shared/skills/do-code-review/SKILL.md +41 -144
- package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
- package/core/shared/skills/do-code-review/references/tools.md +90 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
- package/core/shared/skills/do-design/SKILL.md +24 -23
- package/core/shared/skills/do-diagnose/SKILL.md +14 -4
- package/core/shared/skills/do-document/SKILL.md +11 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +18 -6
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
- package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
- package/core/shared/skills/do-flow/SKILL.md +8 -9
- package/core/shared/skills/do-flow/references/task_classes.md +2 -2
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +7 -9
- package/core/shared/skills/do-plan/SKILL.md +29 -19
- package/core/shared/skills/do-test/SKILL.md +7 -4
- package/core/shared/templates/doflow/constitution-template.md +9 -3
- package/core/shared/templates/doflow/data-model-template.md +1 -1
- package/core/shared/templates/doflow/design-template.md +113 -16
- package/core/shared/templates/doflow/plan-template.md +11 -10
- package/core/shared/templates/doflow/question-log-template.md +14 -2
- package/core/shared/templates/doflow/requirement-template.md +1 -1
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +226 -113
- package/src/adapters/claude/index.js +39 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +40 -5
- package/src/adapters/codex/index.js +100 -22
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +294 -48
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +38 -13
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +120 -0
- package/src/adapters/pi/index.js +48 -16
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +3 -2
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +21 -2
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +3 -2
- package/src/cli/index.js +338 -33
- package/src/cli/runtime-commands.js +243 -52
- package/src/cli/shared.js +0 -2
- package/src/helper/git.js +1 -1
- package/src/helper/guardrails.js +1 -1
- package/src/helper/marker-merge.js +31 -13
- package/src/helper/package-ids.js +37 -0
- package/src/helper/semver.js +94 -0
- package/src/install/backup.js +105 -15
- package/src/install/context.js +1 -1
- package/src/install/targets.js +31 -16
- package/src/lifecycle/coverage.js +74 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +434 -86
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +686 -32
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +492 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +514 -0
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +27 -12
- package/src/runtime/cli-result.js +11 -4
- package/src/runtime/cli.js +221 -277
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +100 -0
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context/primary-command.js +117 -0
- package/src/runtime/context/primary.js +344 -0
- package/src/runtime/context/summary-store.js +655 -0
- package/src/runtime/context-pack.js +1106 -245
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/evidence-ledger.js +41 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +77 -10
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +565 -17
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +277 -22
- package/src/runtime/inventory/coverage.js +1 -1
- package/src/runtime/inventory/index.js +1 -1
- package/src/runtime/inventory/precedence.js +2 -2
- package/src/runtime/inventory/siblings.js +6 -4
- package/src/runtime/knowledge/code-corpus.js +277 -0
- package/src/runtime/knowledge/code-lexical.js +130 -0
- package/src/runtime/knowledge/code-retrieval.js +307 -0
- package/src/runtime/knowledge/code-validation.js +106 -0
- package/src/runtime/knowledge/gp3-gate.js +74 -0
- package/src/runtime/knowledge/graph-adapter.js +288 -0
- package/src/runtime/knowledge/graph-fallback.js +394 -0
- package/src/runtime/knowledge/hybrid.js +136 -8
- package/src/runtime/knowledge/retrieval.js +34 -1
- package/src/runtime/knowledge/rtk-compress.js +77 -0
- package/src/runtime/knowledge/semble-bridge.py +106 -0
- package/src/runtime/knowledge/semble-lifecycle.js +114 -0
- package/src/runtime/knowledge/semble-provider.js +365 -0
- package/src/runtime/lifecycle/cli.js +16 -6
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +6 -1
- package/src/runtime/lifecycle/overview.js +24 -1
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +3 -3
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +477 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +127 -16
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +402 -73
- package/src/runtime/retrieval-record.js +293 -0
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +270 -0
- package/src/runtime/roadmap/loop.js +1636 -0
- package/src/runtime/roadmap/merge.js +381 -0
- package/src/runtime/roadmap/rows.js +1113 -0
- package/src/runtime/roadmap/seed.js +951 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +63 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +290 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +47 -11
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +202 -56
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +440 -65
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -9
- package/src/runtime/verification/impact.js +617 -0
- package/src/runtime/verification/inflight.js +205 -0
- package/src/runtime/verification/registry.js +176 -0
- package/src/runtime/verification/reuse.js +184 -0
- package/src/runtime/verification/telemetry-emit.js +254 -0
- package/src/runtime/verification/test-selection.js +67 -7
- package/src/runtime/worker/active.js +90 -0
- package/src/runtime/worker/cancel.js +222 -0
- package/src/runtime/worker/catalogue.js +122 -0
- package/src/runtime/worker/command.js +677 -0
- package/src/runtime/worker/evidence.js +288 -0
- package/src/runtime/worker/launch.js +288 -0
- package/src/runtime/worker/models.js +137 -0
- package/src/runtime/worker/parsers.js +218 -0
- package/src/runtime/worker/probes/index.js +706 -0
- package/src/runtime/worker/process-tree.js +415 -0
- package/src/runtime/worker/quota.js +43 -0
- package/src/runtime/worker/readiness.js +713 -0
- package/src/runtime/worker/run-store.js +166 -0
- package/src/runtime/worker/supervisor.js +541 -0
- package/src/runtime/worker/worktree-check.js +252 -0
- package/src/runtime/workflow-engine.js +5 -1
- package/src/runtime/workflow-orchestrator.js +578 -63
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -5
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -210
- package/src/adapters/gemini/index.js +0 -390
|
@@ -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),
|
|
@@ -106,8 +204,12 @@ class WorkflowOrchestrator {
|
|
|
106
204
|
|
|
107
205
|
|
|
108
206
|
|
|
207
|
+
|
|
109
208
|
|
|
110
209
|
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
111
213
|
/**
|
|
112
214
|
* @param {Object} [options]
|
|
113
215
|
* @param {string} [options.repoRoot] install root owning core/registry + default state dir.
|
|
@@ -123,39 +225,56 @@ class WorkflowOrchestrator {
|
|
|
123
225
|
* `none` (no ledger) records the stage as `unverified` whatever outcome was supplied. Unwired
|
|
124
226
|
* means the stage is not checked, which only a unit test that builds the orchestrator by hand
|
|
125
227
|
* relies on: the CLI always wires the real one.
|
|
228
|
+
* @param {(node: object, run: object) => {refusals: Array<{path: string, line: number, message: string}>, advisories: Array<object>}} [options.planExitCheck]
|
|
229
|
+
* row 134 gate for the do-plan planning stage: every refusal (a constitution MUST marked FAIL or
|
|
230
|
+
* without a row) refuses completion. Unwired means the stage is not checked, as for taskAttemptCheck.
|
|
126
231
|
* @param {object} [options.fsImpl]
|
|
127
232
|
* @param {string|null} [options.featureSlug] the feature a run started here belongs to
|
|
233
|
+
* @param {object} [options.env] the environment `start` reads the caller's identity and the telemetry switch from
|
|
234
|
+
* @param {string} [options.cwd] the directory `start` resolves the caller's run key and the 080 store from
|
|
128
235
|
* @param {() => object} [options.buildAdapters] builds the adapter registry `mcpReach` reads installs
|
|
129
236
|
* through; without it install facts are unknown and no outcome is inferred from them
|
|
130
237
|
*/
|
|
131
|
-
constructor({ repoRoot = REPO_ROOT, projectRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, fsImpl, featureSlug = null, buildAdapters = null } = {}) {
|
|
238
|
+
constructor({ repoRoot = REPO_ROOT, projectRoot, sourceRoot, stateDir, engine, readinessEvaluate, taskAttemptCheck, planExitCheck, fsImpl, featureSlug = null, buildAdapters = null, env = process.env, cwd = process.cwd() } = {}) {
|
|
132
239
|
this.fsImpl = fsImpl || fs;
|
|
133
240
|
this.repoRoot = repoRoot;
|
|
134
|
-
this.projectRoot = projectRoot || (stateDir?.endsWith(
|
|
241
|
+
this.projectRoot = projectRoot || (stateDir?.endsWith(RUN_STORE_REL)
|
|
135
242
|
? path.resolve(stateDir, '../../..') : repoRoot);
|
|
243
|
+
this.sourceRoot = sourceRoot || this.projectRoot;
|
|
136
244
|
this.engine = engine || new WorkflowEngine({ repoRoot });
|
|
137
|
-
this.stateDir = stateDir ||
|
|
245
|
+
this.stateDir = stateDir || runStoreDir(this.projectRoot);
|
|
138
246
|
this.readinessEvaluate = readinessEvaluate || null;
|
|
139
247
|
this.taskAttemptCheck = taskAttemptCheck || null;
|
|
248
|
+
this.planExitCheck = planExitCheck || null;
|
|
140
249
|
this.featureSlug = featureSlug;
|
|
141
250
|
this.buildAdapters = buildAdapters;
|
|
251
|
+
this.env = env;
|
|
252
|
+
this.cwd = cwd;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The task's run (115 C-1): `<stateDir>/<featureSlug>/<taskId>.json` for a task id other than its
|
|
256
|
+
* feature's slug, else `<stateDir>/<taskId>.json`; a flat run of a namespaced id is read, and copied
|
|
257
|
+
* into the namespace on its next write, only when it is this feature's or names none (D-3). */
|
|
258
|
+
runRecord(taskId ) {
|
|
259
|
+
const id = assertSafeId(taskId, 'taskId');
|
|
260
|
+
const namespace = isSafeSlug(this.featureSlug) && this.featureSlug !== id ? this.featureSlug : null;
|
|
261
|
+
return recordIn({ base: this.stateDir, namespace, name: `${id}.json`, fsImpl: this.fsImpl, refuseUnsafeFlat: true });
|
|
142
262
|
}
|
|
143
263
|
|
|
144
264
|
runFile(taskId ) {
|
|
145
|
-
return
|
|
265
|
+
return this.runRecord(taskId).file;
|
|
146
266
|
}
|
|
147
267
|
|
|
148
268
|
readRun(taskId ) {
|
|
149
|
-
|
|
150
|
-
if (!this.fsImpl.existsSync(file)) return null;
|
|
151
|
-
return readTaskState(this.fsImpl, file);
|
|
269
|
+
return readRunRecord(this.fsImpl, this.runRecord(taskId).read, this.stateDir);
|
|
152
270
|
}
|
|
153
271
|
|
|
154
272
|
writeRun(run , now , beforeWrite ) {
|
|
155
273
|
run.updatedAt = iso(now);
|
|
156
|
-
|
|
274
|
+
const { file, seed } = this.runRecord(run.taskId);
|
|
275
|
+
this.fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
157
276
|
const expectedRevision = run.revision || 0;
|
|
158
|
-
updateTaskState({ file
|
|
277
|
+
updateTaskState({ file, fsImpl: this.fsImpl, prepare: seed, build: (disk ) => {
|
|
159
278
|
if ((disk?.revision || 0) !== expectedRevision) {
|
|
160
279
|
throw new Error(`Concurrent workflow update for '${run.taskId}'; reload the run before retrying. Nothing was written.`);
|
|
161
280
|
}
|
|
@@ -185,24 +304,35 @@ class WorkflowOrchestrator {
|
|
|
185
304
|
startedAt: iso(now),
|
|
186
305
|
// The feature the run belongs to, so another checkout can tell it from a run with the same id.
|
|
187
306
|
...(this.featureSlug ? { featureSlug: this.featureSlug } : {}),
|
|
307
|
+
...this.traceFields(),
|
|
188
308
|
};
|
|
189
309
|
this.writeRun(run, now);
|
|
190
310
|
return this.snapshot(run);
|
|
191
311
|
}
|
|
192
312
|
|
|
313
|
+
/**
|
|
314
|
+
* 088 IC-010 / DD-5: the caller's 080 run key and, when the 080 store already holds that run with
|
|
315
|
+
* a trace id, a copy of the id. Empty with telemetry off, for a solo lineage, or on any fault:
|
|
316
|
+
* nothing here can fail or change a start. No id is minted here and no action is added.
|
|
317
|
+
*/
|
|
318
|
+
traceFields() {
|
|
319
|
+
try {
|
|
320
|
+
const key = runKeyFor(this.env, this.cwd);
|
|
321
|
+
if (!key.traceIds || refuseSolo(key.identity)) return {};
|
|
322
|
+
const out = { runKey: key.runKey };
|
|
323
|
+
if (this.featureSlug) {
|
|
324
|
+
try {
|
|
325
|
+
const store = readTaskState(this.fsImpl, taskAttemptStoreFile(lifecycleProjectRoot(this.cwd), this.featureSlug));
|
|
326
|
+
const record = store && store.runs && Object.hasOwn(store.runs, key.runKey) ? store.runs[key.runKey] : null;
|
|
327
|
+
if (record && typeof record.traceId === 'string' && /^[0-9a-f]{32}$/.test(record.traceId)) out.traceId = record.traceId;
|
|
328
|
+
} catch { /* an unreadable 080 store means no copy */ }
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
} catch { return {}; }
|
|
332
|
+
}
|
|
333
|
+
|
|
193
334
|
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
|
-
};
|
|
335
|
+
return snapshotRun(run);
|
|
206
336
|
}
|
|
207
337
|
|
|
208
338
|
status(taskId ) {
|
|
@@ -244,7 +374,10 @@ class WorkflowOrchestrator {
|
|
|
244
374
|
}
|
|
245
375
|
// The ledger is checked here too, before catch-up writes anything, so a refused implementation
|
|
246
376
|
// handoff leaves no backfilled stage behind.
|
|
247
|
-
if (preview.reason === 'reached-candidate')
|
|
377
|
+
if (preview.reason === 'reached-candidate') {
|
|
378
|
+
this.assertTaskAttempts(preview.node, existing || { taskId, taskClass: resolvedClass });
|
|
379
|
+
this.assertPlanExit(preview.node, existing || { taskId, taskClass: resolvedClass });
|
|
380
|
+
}
|
|
248
381
|
const positioned = this.catchUp({ taskId, taskClass: resolvedClass, candidateStageIds: candidates, note, now });
|
|
249
382
|
if (positioned.reason === 'reached-candidate') {
|
|
250
383
|
// A run this handoff started has no earlier stage: every evidence item counts.
|
|
@@ -295,8 +428,9 @@ class WorkflowOrchestrator {
|
|
|
295
428
|
const harness = (registry.harnesses || []).find((item ) => item.id === id);
|
|
296
429
|
return harness ? harness.displayName || harness.id : id;
|
|
297
430
|
};
|
|
298
|
-
const { briefRecords, repoRoot } = stageId ? this.readBriefRecords(taskId) : { briefRecords: [], repoRoot: this.
|
|
299
|
-
|
|
431
|
+
const { briefRecords, repoRoot } = stageId ? this.readBriefRecords(taskId) : { briefRecords: [], repoRoot: this.sourceRoot };
|
|
432
|
+
// Advice picks are project state, kept with the run's records, not in the feature folder's checkout.
|
|
433
|
+
if (stageId) this.recordBriefTierAdvice({ briefRecords, taskId, stageId, repoRoot: this.projectRoot });
|
|
300
434
|
try {
|
|
301
435
|
const result = resolveConsultations({
|
|
302
436
|
taskId, stageId, skill, flags, registry, ledger: {
|
|
@@ -305,7 +439,7 @@ class WorkflowOrchestrator {
|
|
|
305
439
|
return ledger.queryEvidence({ taskId: id });
|
|
306
440
|
},
|
|
307
441
|
},
|
|
308
|
-
requests: new ResearchRequestStore({ projectRoot: this.projectRoot, fsImpl: this.fsImpl, slug: this.featureSlug }),
|
|
442
|
+
requests: new ResearchRequestStore({ projectRoot: this.projectRoot, sourceRoot: this.sourceRoot, fsImpl: this.fsImpl, slug: this.featureSlug }),
|
|
309
443
|
briefRecords,
|
|
310
444
|
readReport: (file ) => {
|
|
311
445
|
const target = path.resolve(repoRoot, file);
|
|
@@ -389,28 +523,28 @@ class WorkflowOrchestrator {
|
|
|
389
523
|
}
|
|
390
524
|
|
|
391
525
|
/** The brief records (`*-brief.json`) of the feature this run belongs to, parsed; unreadable ones
|
|
392
|
-
* are left out.
|
|
393
|
-
* folder
|
|
526
|
+
* are left out. They are feature-folder files, so they are resolved from the checkout the verb runs
|
|
527
|
+
* in with the resolver's `feature_root` (that checkout's folder, else the main checkout's), never
|
|
528
|
+
* from the state root (115 C-10). Resolving the feature spawns the resolver, so it is only done
|
|
529
|
+
* where a feature folder exists above one of the two. */
|
|
394
530
|
readBriefRecords(taskId ) {
|
|
395
|
-
const none = { briefRecords: [], repoRoot: this.
|
|
531
|
+
const none = { briefRecords: [], repoRoot: this.sourceRoot };
|
|
396
532
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
dir = parent;
|
|
404
|
-
}
|
|
533
|
+
const hasFolder = [this.sourceRoot, this.projectRoot].some((root) => {
|
|
534
|
+
for (let dir = path.resolve(root); ; dir = path.dirname(dir)) {
|
|
535
|
+
if (this.fsImpl.existsSync(path.join(dir, 'agent-docs', 'doflow'))) return true;
|
|
536
|
+
if (path.dirname(dir) === dir) return false;
|
|
537
|
+
}
|
|
538
|
+
});
|
|
405
539
|
if (!hasFolder) return none;
|
|
406
|
-
const feature = resolveActiveFeature({ projectRoot: this.
|
|
540
|
+
const feature = resolveActiveFeature({ projectRoot: this.sourceRoot, slug: taskId });
|
|
407
541
|
if (feature.error) return none;
|
|
408
|
-
const exec = path.join(feature.
|
|
542
|
+
const exec = path.join(feature.featureFolder, 'exec');
|
|
409
543
|
const briefRecords = [];
|
|
410
544
|
for (const name of this.fsImpl.readdirSync(exec).filter((entry ) => entry.endsWith('-brief.json')).sort()) {
|
|
411
545
|
try { briefRecords.push(JSON.parse(this.fsImpl.readFileSync(path.join(exec, name), 'utf8'))); } catch { /* an unreadable record contributes nothing */ }
|
|
412
546
|
}
|
|
413
|
-
return { briefRecords, repoRoot: feature.
|
|
547
|
+
return { briefRecords, repoRoot: feature.featureRoot };
|
|
414
548
|
} catch { return none; }
|
|
415
549
|
}
|
|
416
550
|
|
|
@@ -463,15 +597,21 @@ class WorkflowOrchestrator {
|
|
|
463
597
|
* stage is always imported+unverified, whatever the caller says: catch-up importing history
|
|
464
598
|
* cannot import a verification. A 'failed' outcome still advances — recording the failure
|
|
465
599
|
* 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.
|
|
600
|
+
* observing the expected failure — but the record no longer reads as success.
|
|
601
|
+
*
|
|
602
|
+
* Issue #9 stage 3: the entry names the context packs the stage was handed (`packs`), copied from
|
|
603
|
+
* what `recordStagePack` wrote on this node while it was the run's active stage. A stage handed
|
|
604
|
+
* none gets no `packs`; a malformed item is left out, never an error. */
|
|
467
605
|
completeStage({ taskId, stageId, note, outcome, backfilled = false, consultations, now } = {}) {
|
|
468
606
|
const run = this.requireRun(taskId);
|
|
469
607
|
const node = this.expectOpenStage(run, stageId);
|
|
608
|
+
const packs = stagePacks(node.packs);
|
|
470
609
|
if (outcome !== undefined && !STAGE_OUTCOMES.has(outcome)) {
|
|
471
610
|
throw new Error(`Unknown stage outcome '${outcome}'. Valid: ${[...STAGE_OUTCOMES].join(', ')} — omit the flag to record 'unverified'.`);
|
|
472
611
|
}
|
|
473
612
|
if (node.mutatesSource && node.readinessTemplate) this.assertReady(node, run);
|
|
474
613
|
const attempts = backfilled ? null : this.assertTaskAttempts(node, run);
|
|
614
|
+
this.assertPlanExit(node, run);
|
|
475
615
|
// No ledger for the feature: the stage is recorded, but nothing verified its tasks, and the
|
|
476
616
|
// record says so whatever outcome the caller supplied (FR-006).
|
|
477
617
|
const unledgered = attempts?.state === 'none';
|
|
@@ -483,11 +623,12 @@ class WorkflowOrchestrator {
|
|
|
483
623
|
note: unledgered ? [note, NO_LEDGER_NOTE].filter(Boolean).join(' ') : (note ?? null),
|
|
484
624
|
backfilled: Boolean(backfilled), executionStatus: node.executionStatus, outcome: node.outcome,
|
|
485
625
|
...(consultations ? { consultations } : {}),
|
|
626
|
+
...(packs.length ? { packs } : {}),
|
|
486
627
|
});
|
|
487
628
|
this.advance(run, now);
|
|
488
629
|
this.writeRun(run, now, () => {
|
|
489
630
|
if (run.taskClass !== 'feature') return;
|
|
490
|
-
const request = new ResearchRequestStore({ projectRoot: this.projectRoot, fsImpl: this.fsImpl })
|
|
631
|
+
const request = new ResearchRequestStore({ projectRoot: this.projectRoot, sourceRoot: this.sourceRoot, fsImpl: this.fsImpl })
|
|
491
632
|
.blockingGap(taskId, stageId);
|
|
492
633
|
if (request) throw new Error(`Research request '${request.id}' blocks stage '${stageId}': ${request.failureReason || request.gap || request.status}`);
|
|
493
634
|
});
|
|
@@ -640,15 +781,22 @@ class WorkflowOrchestrator {
|
|
|
640
781
|
* `forced` decision (e.g. approving despite unresolved clarification markers) requires an
|
|
641
782
|
* explicit `note` reason — never a silent override — and is flagged in history for
|
|
642
783
|
* `render-audit.sh` to distinguish from a routine handoff. */
|
|
643
|
-
decideGate({ taskId, gateId, decision, note, forced = false, now } = {}) {
|
|
784
|
+
decideGate({ taskId, gateId, decision, policy, by, note, forced = false, now, bindHead } = {}) {
|
|
644
785
|
assertSafeId(gateId, 'gateId');
|
|
645
|
-
if (
|
|
786
|
+
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');
|
|
787
|
+
// IC-005: the decider is derived, never named by a caller; `autopilot` is recorded only by --policy.
|
|
788
|
+
if (by !== undefined) throw new Error('decide-gate takes no decider (by); only --policy records autopilot');
|
|
789
|
+
if (policy !== undefined) {
|
|
790
|
+
if (policy !== gateId) throw new Error('--policy must name the gate being decided');
|
|
791
|
+
if (decision !== undefined || forced) throw new Error('--policy refuses --decision and --forced');
|
|
792
|
+
} else if (!GATE_DECISIONS.includes(decision)) throw new Error(`Invalid gate decision '${decision}'; valid: ${GATE_DECISIONS.join(', ')}`);
|
|
646
793
|
if (forced && (!note || !String(note).trim())) throw new Error('A forced gate decision requires a --note reason');
|
|
647
794
|
const run = this.requireRun(taskId);
|
|
648
795
|
if (run.state !== 'AWAITING_GATE') throw new Error(`Run '${run.taskId}' is not awaiting a gate (state=${run.state})`);
|
|
649
796
|
const node = this.currentNode(run);
|
|
650
797
|
if (!node || node.type !== 'gate') throw new Error('Run state says AWAITING_GATE but no gate is at the cursor');
|
|
651
798
|
if (node.id !== gateId) throw new Error(`Expected gate '${node.id}' next, got '${gateId}'`);
|
|
799
|
+
if (policy !== undefined) return this.decideGateByPolicy(run, node, policy, now, bindHead);
|
|
652
800
|
node.status = decision === 'approve' ? 'approved' : 'rejected';
|
|
653
801
|
node.decision = decision;
|
|
654
802
|
run.history.push({ at: iso(now), action: 'decide-gate', node: node.id, detail: decision, note: note ?? null, forced: Boolean(forced) });
|
|
@@ -663,6 +811,94 @@ class WorkflowOrchestrator {
|
|
|
663
811
|
return this.snapshot(run);
|
|
664
812
|
}
|
|
665
813
|
|
|
814
|
+
/** IC-005: the feature checkout cannot grant itself a weaker gate policy. Denies write nothing. */
|
|
815
|
+
decideGateByPolicy(run , node , policyId , now , bindHead ) {
|
|
816
|
+
// Decision C9: the autopilot kill switch binds every policy decision, not only the loop's. Off on
|
|
817
|
+
// any layer (env, sentinel, settings; an unreadable setting fails safe to off) denies before any
|
|
818
|
+
// fact is read, and records nothing.
|
|
819
|
+
const { readKillSwitch } = require('./settings/autopilot.js');
|
|
820
|
+
const killSwitch = readKillSwitch(this.env);
|
|
821
|
+
if (!killSwitch.enabled) {
|
|
822
|
+
return { ...this.snapshot(run), decision: 'deny', reason: 'autopilot-disabled', reasons: ['autopilot-disabled'],
|
|
823
|
+
rule: `${node.id}/autopilot`, policy: { id: policyId, digest: null }, inputs: [], annotations: { switch: killSwitch.switch } };
|
|
824
|
+
}
|
|
825
|
+
// Loaded here, not at module top: every other orchestrator path (and a dry-run install) must not import them.
|
|
826
|
+
const { projectLayerSource } = require('./settings/project-layers.js');
|
|
827
|
+
const { parseSeed } = require('./roadmap/rows.js');
|
|
828
|
+
const { loadPolicy, evaluate } = require('./roadmap/gates.js');
|
|
829
|
+
const { gatherGateFacts } = require('./roadmap/facts.js');
|
|
830
|
+
const root = this.sourceRoot;
|
|
831
|
+
const mainRoot = mainCheckoutOf(root);
|
|
832
|
+
const execOptions = { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] };
|
|
833
|
+
let developHead ;
|
|
834
|
+
let baseBytes = null;
|
|
835
|
+
// The base policy is develop's committed blob, never the worktree or the feature branch: a
|
|
836
|
+
// branch that weakens `never` to `approve` must not approve itself. No textconv filter applies.
|
|
837
|
+
try {
|
|
838
|
+
const head = execFileSync('git', ['rev-parse', '--verify', 'refs/heads/develop'], execOptions).trim();
|
|
839
|
+
developHead = head;
|
|
840
|
+
if (/^[0-9a-f]{40}$/.test(head)) {
|
|
841
|
+
baseBytes = execFileSync('git', ['show', '--no-textconv', `${head}:core/registry/gates.json`], execOptions);
|
|
842
|
+
}
|
|
843
|
+
} catch { /* loadPolicy returns no-policy; never fall back to the feature copy. */ }
|
|
844
|
+
const layers = projectLayerSource(mainRoot, this.fsImpl);
|
|
845
|
+
let overrideBytes = null;
|
|
846
|
+
if (layers.layersFrom) {
|
|
847
|
+
try { overrideBytes = this.fsImpl.readFileSync(path.join(layers.layersFrom, 'gates.override.json'), 'utf8'); }
|
|
848
|
+
catch (error ) { if (error.code !== 'ENOENT') overrideBytes = ''; }
|
|
849
|
+
}
|
|
850
|
+
const loaded = loadPolicy(baseBytes, overrideBytes);
|
|
851
|
+
const policy = { id: policyId, digest: loaded.digest };
|
|
852
|
+
if (!loaded.ok) return { ...this.snapshot(run), decision: 'deny', reasons: loaded.reasons, reason: loaded.reason, policy, inputs: [], ...(loaded.annotations ? { annotations: loaded.annotations } : {}) };
|
|
853
|
+
|
|
854
|
+
const slug = runSlugOf(run, root) || this.featureSlug || run.taskId;
|
|
855
|
+
let seed = null;
|
|
856
|
+
let row = null;
|
|
857
|
+
try {
|
|
858
|
+
seed = parseSeed(this.fsImpl.readFileSync(path.join(mainRoot, 'agent-docs/roadmap/rows.json'), 'utf8'));
|
|
859
|
+
row = seed.rows.find((entry ) => entry.slug === slug && entry.class === run.taskClass) || null;
|
|
860
|
+
} catch (error ) {
|
|
861
|
+
if (error.code === 'seed-typed-status') return { ...this.snapshot(run), decision: 'deny', reason: error.code, reasons: [error.code], policy, inputs: [] };
|
|
862
|
+
// Invalid or unreadable seed means no trustworthy row; evaluate returns row-unknown.
|
|
863
|
+
}
|
|
864
|
+
// The feature folder follows the feature-store rule (resolveFeatureInStore): a linked worktree
|
|
865
|
+
// without the folder would otherwise read no artifacts (a false `unresolved-clarifications`) and
|
|
866
|
+
// register into a missing folder.
|
|
867
|
+
const feature = resolveFeatureInStore({ root, slug });
|
|
868
|
+
if (feature.error) throw new Error(`Cannot resolve policy gate artifacts: ${feature.message}`);
|
|
869
|
+
const facts = gatherGateFacts({ root, mainRoot, registryRoot: this.repoRoot, artifactRoot: feature.featureDir,
|
|
870
|
+
gateId: node.id, policy: loaded.policy, row, rows: seed?.rows, developHead, featureBranch: `feat/${slug}`, fsImpl: this.fsImpl, env: this.env });
|
|
871
|
+
const evaluation = evaluate(loaded.policy, run.taskClass, node.id, facts);
|
|
872
|
+
// A deny changes nothing: the run stays AWAITING_GATE and autopilot never rejects.
|
|
873
|
+
if (evaluation.decision === 'deny') return { ...this.snapshot(run), ...evaluation, policy };
|
|
874
|
+
|
|
875
|
+
const audit = `approved by policy ${policyId}@${loaded.digest} (${evaluation.inputs.length} inputs)`;
|
|
876
|
+
node.status = 'approved';
|
|
877
|
+
node.decision = 'approve';
|
|
878
|
+
// `policy.digest` and every fact's {locator, sha256} let a later reader re-derive this decision.
|
|
879
|
+
// C15: the roadmap loop names the feature head it read before deciding (bindHead). The entry keeps
|
|
880
|
+
// it as `approvedHead` only when the gate's own heads fact evaluated that same head, so the merge
|
|
881
|
+
// binds to it across loop runs; an approval without it falls back to the both-heads digest.
|
|
882
|
+
const heads = facts.find((fact ) => fact.name === 'heads' && !fact.unreadable)?.value;
|
|
883
|
+
const approvedHead = bindHead && heads?.featureHead === bindHead ? { approvedHead: bindHead } : {};
|
|
884
|
+
run.history.push({ at: iso(now), action: 'decide-gate', node: node.id, detail: 'approve', by: 'autopilot',
|
|
885
|
+
policy, inputs: evaluation.inputs, ...approvedHead, note: audit, forced: false });
|
|
886
|
+
run.state = runState('RUNNING');
|
|
887
|
+
run.cursor += 1;
|
|
888
|
+
this.settleCursor(run);
|
|
889
|
+
// The optimistic revision check runs before registering a decision, so a stale run cannot
|
|
890
|
+
// leave a forged approval. A register refusal likewise prevents the journal from advancing.
|
|
891
|
+
this.writeRun(run, now, () => {
|
|
892
|
+
const registered = addDecisions({ featureDir: feature.featureDir, slug, now: now ?? new Date(), fsImpl: this.fsImpl, items: [{
|
|
893
|
+
topic: node.id, statement: `${node.id} ${audit}.`, channel: 'policy', stage: POLICY_GATE_STAGE[node.id],
|
|
894
|
+
rationale: `Every predicate of ${node.id} passed on the inputs digest ${evaluation.inputsDigest}.`,
|
|
895
|
+
source: `git:${developHead}:core/registry/gates.json`,
|
|
896
|
+
}] });
|
|
897
|
+
if (registered.finding) throw new Error(`Cannot register policy approval: ${registered.finding}: ${registered.message}`);
|
|
898
|
+
});
|
|
899
|
+
return { ...this.snapshot(run), ...evaluation, policy };
|
|
900
|
+
}
|
|
901
|
+
|
|
666
902
|
/** Append a pure audit-trail entry — a stage re-run after its own handoff, or an artifact
|
|
667
903
|
* hand-edited after that stage's own handoff — without touching program/cursor/state. Not a
|
|
668
904
|
* stage completion or a gate decision, so it never collides with the state machine's ordering
|
|
@@ -731,6 +967,20 @@ class WorkflowOrchestrator {
|
|
|
731
967
|
return answer;
|
|
732
968
|
}
|
|
733
969
|
|
|
970
|
+
/** Row 134: the planning stage (the `do-plan` stage) cannot complete while the plan's
|
|
971
|
+
* Constitution Check leaves a MUST principle marked FAIL or without a row. There is no waiver:
|
|
972
|
+
* the plan is revised, or a person amends the constitution. A stage catch-up backfills is checked
|
|
973
|
+
* too, so a refused plan is never imported as complete. */
|
|
974
|
+
assertPlanExit(node , run ) {
|
|
975
|
+
if (!this.planExitCheck || node.skill !== 'do-plan') return null;
|
|
976
|
+
const answer = this.planExitCheck(node, run);
|
|
977
|
+
if (!answer || !Array.isArray(answer.refusals)) throw new Error(`Plan exit check returned no usable answer for stage '${node.id}'`);
|
|
978
|
+
if (answer.refusals.length) {
|
|
979
|
+
throw new Error(`Stage '${node.id}' cannot be completed: ${answer.refusals.map((f ) => `${f.path}:${f.line} ${f.message}`).join('; ')}`);
|
|
980
|
+
}
|
|
981
|
+
return answer;
|
|
982
|
+
}
|
|
983
|
+
|
|
734
984
|
/** Where `catchUp` would stop for these candidates, worked out on a copy of the run's program (or
|
|
735
985
|
* of a fresh run's) without writing anything. Same stop rules and reason vocabulary as `catchUp`;
|
|
736
986
|
* `stageId` is what `catchUp` reports as `caughtUpTo`, and `node` the program node it stops on.
|
|
@@ -768,6 +1018,26 @@ class WorkflowOrchestrator {
|
|
|
768
1018
|
}
|
|
769
1019
|
}
|
|
770
1020
|
|
|
1021
|
+
/** The project's main checkout, or `root` itself when the checkouts cannot be listed. Loaded here, not at
|
|
1022
|
+
* module top: every other orchestrator path (and a dry-run install) must not import the resolver. */
|
|
1023
|
+
function mainCheckoutOf(root ) {
|
|
1024
|
+
const { listCheckouts } = require('./checkouts.js');
|
|
1025
|
+
const checkouts = listCheckouts({ cwd: root });
|
|
1026
|
+
return checkouts.ok && checkouts.main ? checkouts.main : root;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* The feature folder follows the feature-store rule, which the feature resolver already applies: the
|
|
1031
|
+
* calling checkout's own folder, else the main checkout's when a linked worktree holds none
|
|
1032
|
+
* (`featureFolder` under `featureRoot`). `featureDir` and `repoRoot` of the result name that folder and
|
|
1033
|
+
* checkout, as `decision --action compact` reads them, so a caller never looks in the wrong checkout.
|
|
1034
|
+
*/
|
|
1035
|
+
function resolveFeatureInStore({ root, slug } ) {
|
|
1036
|
+
const feature = resolveActiveFeature({ projectRoot: root, slug });
|
|
1037
|
+
if (feature.error) return feature;
|
|
1038
|
+
return { ...feature, featureDir: feature.featureFolder, repoRoot: feature.featureRoot };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
771
1041
|
/**
|
|
772
1042
|
* IC-013: fold the finished stage's History into the feature's archive. The task id is the feature
|
|
773
1043
|
* slug. This is housekeeping after a recorded handoff, so it reports and never throws: the
|
|
@@ -777,7 +1047,7 @@ class WorkflowOrchestrator {
|
|
|
777
1047
|
*/
|
|
778
1048
|
function compactAfterHandoff({ taskId, projectRoot } ) {
|
|
779
1049
|
try {
|
|
780
|
-
const feature =
|
|
1050
|
+
const feature = resolveFeatureInStore({ root: projectRoot, slug: taskId });
|
|
781
1051
|
if (feature.error) return { status: 'skipped', reason: feature.message };
|
|
782
1052
|
// A task id such as a plan task (`B.1`) resolves on a feature branch to a folder that was
|
|
783
1053
|
// never created; say that, rather than blaming a missing register.
|
|
@@ -797,41 +1067,242 @@ function compactAfterHandoff({ taskId, projectRoot } ) {
|
|
|
797
1067
|
}
|
|
798
1068
|
}
|
|
799
1069
|
|
|
1070
|
+
/**
|
|
1071
|
+
* IC-001: `orchestrate --action list`, every run the resolver can see as a prompt-free row plus the
|
|
1072
|
+
* records it could not read, by reason code only. Reads and prints; writes nothing.
|
|
1073
|
+
*/
|
|
1074
|
+
/**
|
|
1075
|
+
* The first flag given that a read action does not take, or undefined. Every flag another action
|
|
1076
|
+
* reads is refused before any read: a filter the read does not apply must not look applied, and an
|
|
1077
|
+
* outcome nothing records must not look recorded. `takes` names the flags the action does read.
|
|
1078
|
+
*/
|
|
1079
|
+
function noEffectFlag({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun } , takes = []) {
|
|
1080
|
+
const supplied = (value ) => value !== undefined && value !== null;
|
|
1081
|
+
const given = [
|
|
1082
|
+
['--task-id', supplied(taskId)], ['--task-class', supplied(taskClass)], ['--gate', supplied(gate)],
|
|
1083
|
+
['--decision', supplied(decision)], ['--policy', supplied(policy)],
|
|
1084
|
+
['--forced', forced], ['--note', supplied(note)], ['--reason', supplied(reason)], ['--stage', supplied(stage)],
|
|
1085
|
+
['--node', supplied(node)], ['--verification-plan', supplied(verificationPlan)], ['--scope', supplied(scope)],
|
|
1086
|
+
['--invariants', supplied(invariants)], ['--result', supplied(result)], ['--mcp-outcome', supplied(mcpOutcome)],
|
|
1087
|
+
['--calling-skill', supplied(callingSkill)], ['--slug', supplied(slug)], ['--dry-run', Boolean(dryRun)],
|
|
1088
|
+
].find(([flag, on]) => on && !takes.includes(flag ));
|
|
1089
|
+
return given ? given[0] : undefined;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/** A journal time as an ISO instant, else null, so no other journal text reaches a read's output. */
|
|
1093
|
+
const journalTime = (value ) => typeof value === 'string'
|
|
1094
|
+
&& /^\d{4}-\d{2}-\d{2}T/.test(value) && !Number.isNaN(Date.parse(value)) ? value : null;
|
|
1095
|
+
|
|
1096
|
+
/** The journal actions a timeline shows (093 IC-012); any other is counted in `otherEvents`. */
|
|
1097
|
+
const TIMELINE_ACTIONS = new Set(['start', 'complete-stage', 'skip-stage', 'skip-gate', 'decide-gate', 'annotate', 'complete-run']);
|
|
1098
|
+
const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* 093 IC-012: one run's history as closed values only. Each event copies the action, an id-shaped
|
|
1102
|
+
* node, the stage outcome (complete-stage), the gate decision (decide-gate's `detail`), `forced`, and
|
|
1103
|
+
* whether the entry records no work done (the indicators rule, trace/indicators.ts isSynthetic).
|
|
1104
|
+
* Notes, detail text, deciders, policy inputs, consultations and trace ids are never read.
|
|
1105
|
+
*/
|
|
1106
|
+
function timelineOf(run , taskId , featureSlug ) {
|
|
1107
|
+
const at = (value ) => journalTime(value) !== null && ISO_INSTANT.test(value) ? value : null;
|
|
1108
|
+
const events = [];
|
|
1109
|
+
let otherEvents = 0;
|
|
1110
|
+
for (const entry of Array.isArray(run.history) ? run.history : []) {
|
|
1111
|
+
if (!entry || typeof entry !== 'object' || !TIMELINE_ACTIONS.has(entry.action)) {
|
|
1112
|
+
otherEvents += 1;
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
events.push({
|
|
1116
|
+
at: at(entry.at),
|
|
1117
|
+
action: entry.action,
|
|
1118
|
+
node: typeof entry.node === 'string' && ID_PATTERN.test(entry.node) ? entry.node : null,
|
|
1119
|
+
outcome: entry.action === 'complete-stage' && STAGE_OUTCOMES.has(entry.outcome) ? entry.outcome : null,
|
|
1120
|
+
decision: entry.action === 'decide-gate' && GATE_DECISIONS.includes(entry.detail) ? entry.detail : null,
|
|
1121
|
+
forced: entry.forced === true,
|
|
1122
|
+
synthetic: Boolean(entry.backfilled) || Boolean(entry.executionStatus && entry.executionStatus !== 'completed'),
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
return {
|
|
1126
|
+
v: 1,
|
|
1127
|
+
taskId,
|
|
1128
|
+
featureSlug: typeof featureSlug === 'string' && ID_PATTERN.test(featureSlug) ? featureSlug : null,
|
|
1129
|
+
revision: Number.isInteger(run.revision) && run.revision >= 0 ? run.revision : 0,
|
|
1130
|
+
startedAt: at(run.startedAt),
|
|
1131
|
+
events,
|
|
1132
|
+
otherEvents,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function listRuns({ json, stateRoot, ...flags } ) {
|
|
1137
|
+
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
1138
|
+
// list takes no filter, so a `--slug` must not look like one.
|
|
1139
|
+
const noEffect = noEffectFlag(flags);
|
|
1140
|
+
if (noEffect) return usageError('orchestrate', `'${noEffect}' has no effect on action 'list'`, json);
|
|
1141
|
+
|
|
1142
|
+
const resolverFailure = () => {
|
|
1143
|
+
if (json) console.log(JSON.stringify({ v: 1, error: 'cannot-list-checkouts' }, null, 2));
|
|
1144
|
+
else console.error('[ERROR] orchestrate: cannot list checkouts');
|
|
1145
|
+
return finishRuntime(1);
|
|
1146
|
+
};
|
|
1147
|
+
let resolved ;
|
|
1148
|
+
try { resolved = orchestrationRuns({ stateRoot: stateRoot || process.cwd() }); } catch { return resolverFailure(); }
|
|
1149
|
+
if (!resolved.ok) return resolverFailure();
|
|
1150
|
+
|
|
1151
|
+
const runs = [];
|
|
1152
|
+
const unreadable = [];
|
|
1153
|
+
const textCompare = (a , b ) => a < b ? -1 : a > b ? 1 : 0;
|
|
1154
|
+
const slugCompare = (a , b ) => a === null ? (b === null ? 0 : -1)
|
|
1155
|
+
: b === null ? 1 : textCompare(a, b);
|
|
1156
|
+
const addUnreadable = (entry , reason ) => {
|
|
1157
|
+
const relative = path.relative(runStoreDir(entry.root), entry.file);
|
|
1158
|
+
const parts = relative.split(path.sep);
|
|
1159
|
+
unreadable.push({
|
|
1160
|
+
taskId: path.basename(entry.file, '.json'),
|
|
1161
|
+
featureSlug: parts.length > 1 ? parts[0] : null,
|
|
1162
|
+
origin: entry.origin,
|
|
1163
|
+
reason,
|
|
1164
|
+
});
|
|
1165
|
+
};
|
|
1166
|
+
// One row per run, the copy status reads (C-2): a shadowed flat or another checkout's copy is dropped.
|
|
1167
|
+
for (const entry of selectRunEntries(resolved.entries)) {
|
|
1168
|
+
if (entry.error) {
|
|
1169
|
+
addUnreadable(entry, entry.error);
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
const record = entry.record;
|
|
1173
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
1174
|
+
addUnreadable(entry, 'shape');
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (record.version !== 1) {
|
|
1178
|
+
addUnreadable(entry, 'schema-version');
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
const relative = path.relative(runStoreDir(entry.root), entry.file);
|
|
1182
|
+
const parts = relative.split(path.sep);
|
|
1183
|
+
const folderSlug = parts.length > 1 ? parts[0] : null;
|
|
1184
|
+
const stem = path.basename(entry.file, '.json');
|
|
1185
|
+
// Every id a row prints is checked, so no newline or escape byte from a record reaches the text form.
|
|
1186
|
+
if (typeof record.taskId !== 'string' || !ID_PATTERN.test(record.taskId) || record.taskId !== stem
|
|
1187
|
+
|| (folderSlug && typeof record.featureSlug === 'string' && record.featureSlug !== folderSlug)
|
|
1188
|
+
|| (entry.slug !== null && entry.slug !== undefined && !(typeof entry.slug === 'string' && ID_PATTERN.test(entry.slug)))) {
|
|
1189
|
+
addUnreadable(entry, 'id-mismatch');
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
if (!RUN_STATES.includes(record.state)) {
|
|
1193
|
+
addUnreadable(entry, 'shape');
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
let snapshot ;
|
|
1197
|
+
try { snapshot = snapshotRun(record); } catch {
|
|
1198
|
+
addUnreadable(entry, 'shape');
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (snapshot.current && !(typeof snapshot.current.id === 'string' && ID_PATTERN.test(snapshot.current.id))) {
|
|
1202
|
+
addUnreadable(entry, 'shape');
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
const terminal = snapshot.terminalStage;
|
|
1206
|
+
runs.push({
|
|
1207
|
+
taskId: snapshot.taskId,
|
|
1208
|
+
featureSlug: entry.slug,
|
|
1209
|
+
taskClass: snapshot.taskClass,
|
|
1210
|
+
workflowName: snapshot.workflowName,
|
|
1211
|
+
state: snapshot.state,
|
|
1212
|
+
current: snapshot.current,
|
|
1213
|
+
awaitingGate: snapshot.awaitingGate ? { gateId: snapshot.awaitingGate.gateId, name: snapshot.awaitingGate.name } : null,
|
|
1214
|
+
progress: snapshot.progress,
|
|
1215
|
+
// IC-001: the stage's identity only; its gate list (and the prompts in it) stays in status.
|
|
1216
|
+
terminalStage: terminal && typeof terminal === 'object'
|
|
1217
|
+
? { id: terminal.id ?? null, skill: terminal.skill ?? null, kind: terminal.kind ?? null } : null,
|
|
1218
|
+
revision: snapshot.revision,
|
|
1219
|
+
startedAt: journalTime(record.startedAt),
|
|
1220
|
+
updatedAt: journalTime(record.updatedAt),
|
|
1221
|
+
origin: entry.origin,
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
runs.sort((a, b) => {
|
|
1226
|
+
if (a.updatedAt === null && b.updatedAt !== null) return 1;
|
|
1227
|
+
if (a.updatedAt !== null && b.updatedAt === null) return -1;
|
|
1228
|
+
if (a.updatedAt !== null && b.updatedAt !== null) {
|
|
1229
|
+
const byTime = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
1230
|
+
if (byTime !== 0) return byTime;
|
|
1231
|
+
}
|
|
1232
|
+
return slugCompare(a.featureSlug, b.featureSlug) || textCompare(a.taskId, b.taskId);
|
|
1233
|
+
});
|
|
1234
|
+
unreadable.sort((a, b) => textCompare(a.origin, b.origin)
|
|
1235
|
+
|| slugCompare(a.featureSlug, b.featureSlug) || textCompare(a.taskId, b.taskId));
|
|
1236
|
+
|
|
1237
|
+
if (json) {
|
|
1238
|
+
console.log(JSON.stringify({ v: 1, runs, unreadable }, null, 2));
|
|
1239
|
+
} else {
|
|
1240
|
+
for (const row of runs) console.log(`${row.state} ${row.featureSlug || '-'} ${row.taskId} ${row.current ? row.current.id : '-'} rev ${row.revision}`);
|
|
1241
|
+
if (unreadable.length > 0) console.log(`unreadable: ${unreadable.length}`);
|
|
1242
|
+
}
|
|
1243
|
+
return finishRuntime(0);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
800
1246
|
/** CLI handler for `doflow orchestrate`. The run journal lives in the CALLER's project state
|
|
801
1247
|
* (like evidence), while templates come from this install — same two-roots split readiness uses.
|
|
802
1248
|
* Completing a source-mutating gated stage needs a READY readiness record of the stage's template,
|
|
803
1249
|
* made by `doflow readiness` before this call; nothing is evaluated here. */
|
|
804
1250
|
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,
|
|
1251
|
+
action = 'status', taskId, taskClass, stage, gate, node, decision, policy, note, reason, forced = false,
|
|
1252
|
+
verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, json = false, repoRoot, stateRoot, sourceRoot, slug = null, recordSlug, buildAdapters = null, dryRun = false,
|
|
807
1253
|
} = {}) {
|
|
808
1254
|
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
809
1255
|
|
|
1256
|
+
if (action === 'list') {
|
|
1257
|
+
return listRuns({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun, json, stateRoot });
|
|
1258
|
+
}
|
|
1259
|
+
// 093 IC-012: timeline reads one run, so it takes the two flags that name it and refuses the rest.
|
|
1260
|
+
if (action === 'timeline') {
|
|
1261
|
+
const noEffect = noEffectFlag({ taskId, taskClass, gate, decision, policy, forced, note, reason, stage, node, verificationPlan, scope, invariants, result, mcpOutcome, callingSkill, slug, dryRun }, ['--task-id', '--slug']);
|
|
1262
|
+
if (noEffect) return usageError('orchestrate', `'${noEffect}' has no effect on action 'timeline'`, json);
|
|
1263
|
+
if (!taskId) return usageError('orchestrate', 'timeline requires --task-id', json);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// `start` writes the run; --dry-run would otherwise be ignored and a real run started.
|
|
1267
|
+
if (action === 'start' && dryRun) return usageError('orchestrate', "'--dry-run' has no effect on action 'start'", json);
|
|
1268
|
+
|
|
810
1269
|
const root = repoRoot || REPO_ROOT;
|
|
811
1270
|
// Same scope rule every other stateful command uses (evidenceRoot(o) in runtime-commands.js):
|
|
812
1271
|
// a bare `stateRoot: stateRoot || process.cwd()` silently ignored `--global`, so `orchestrate`
|
|
813
1272
|
// wrote its journal to $PWD while a `--global` `evidence`/`readiness` call on the same task read
|
|
814
1273
|
// and wrote $HOME — two roots disagreeing about where one task's state lives.
|
|
815
1274
|
const state = stateRoot || process.cwd();
|
|
1275
|
+
// The checkout the verb runs in: its branch and feature folder name the feature; `state` (the main
|
|
1276
|
+
// checkout for a linked worktree, 115 IC-004) holds the records.
|
|
1277
|
+
const source = sourceRoot || state;
|
|
816
1278
|
const safeId = ID_PATTERN.test(String(taskId ?? ''));
|
|
817
|
-
// The feature this task belongs to: recorded on a run this command starts,
|
|
818
|
-
//
|
|
819
|
-
|
|
1279
|
+
// The feature this task belongs to: recorded on a run this command starts, the namespace its run
|
|
1280
|
+
// and readiness record live under (115 C-1), and matched against a run found in another checkout.
|
|
1281
|
+
// The dispatcher passes the call's record slug, computed once for every store the call touches.
|
|
1282
|
+
let featureSlug = recordSlug !== undefined ? recordSlug : safeId ? featureSlugFor({ projectRoot: source, taskId, slug }) : slug;
|
|
820
1283
|
// A run started in another checkout of this repository for the same feature is read and updated
|
|
821
1284
|
// where it is. `start` is always local: an unrelated run with the same id elsewhere neither
|
|
822
1285
|
// refuses nor satisfies it.
|
|
823
1286
|
let runRoot = state;
|
|
824
1287
|
if (safeId && action !== 'start') {
|
|
825
|
-
|
|
1288
|
+
// `status` (and `timeline`, which reads the same run) with no --slug opens the one run the list
|
|
1289
|
+
// action shows for the id, whichever feature namespace holds it (FU-wrnnaq); more than one is
|
|
1290
|
+
// refused until --slug names the feature.
|
|
1291
|
+
const found = findRunFile({ stateRoot: state, taskId, slug: featureSlug, anyNamespace: (action === 'status' || action === 'timeline') && !slug });
|
|
826
1292
|
if (found.status === 'ambiguous') {
|
|
827
|
-
console.error(
|
|
1293
|
+
console.error(found.byFeature
|
|
1294
|
+
? `[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.`
|
|
1295
|
+
: `[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
1296
|
return finishRuntime(1);
|
|
829
1297
|
}
|
|
830
|
-
if (found.status === 'found')
|
|
1298
|
+
if (found.status === 'found') {
|
|
1299
|
+
runRoot = found.root;
|
|
1300
|
+
if (found.slug !== undefined) featureSlug = found.slug;
|
|
1301
|
+
}
|
|
831
1302
|
}
|
|
832
1303
|
const orchestrator = new WorkflowOrchestrator({
|
|
833
|
-
repoRoot: root, projectRoot: runRoot,
|
|
834
|
-
stateDir:
|
|
1304
|
+
repoRoot: root, projectRoot: runRoot, sourceRoot: source,
|
|
1305
|
+
stateDir: runStoreDir(runRoot),
|
|
835
1306
|
featureSlug, buildAdapters,
|
|
836
1307
|
});
|
|
837
1308
|
orchestrator.readinessEvaluate = (node , run ) => {
|
|
@@ -865,12 +1336,28 @@ function handleOrchestrateCommand({
|
|
|
865
1336
|
slug: runSlugOf(run, runRoot) || featureSlug, cwd: state,
|
|
866
1337
|
});
|
|
867
1338
|
|
|
1339
|
+
// Row 134. The plan is read in the checkout the verb runs in first, then where the run lives; a
|
|
1340
|
+
// SHOULD principle marked FAIL is one note, printed once although the check runs twice.
|
|
1341
|
+
let planAdvised = false;
|
|
1342
|
+
orchestrator.planExitCheck = (node , run ) => {
|
|
1343
|
+
const { planStageExit } = require('./plan-content.js');
|
|
1344
|
+
const answer = planStageExit({ roots: [...new Set([source, runRoot])], slug: runSlugOf(run, runRoot) || featureSlug });
|
|
1345
|
+
if (!planAdvised) {
|
|
1346
|
+
planAdvised = true;
|
|
1347
|
+
for (const f of answer.advisories) console.error(`orchestrate: note: ${f.path}:${f.line} ${f.message}`);
|
|
1348
|
+
}
|
|
1349
|
+
return answer;
|
|
1350
|
+
};
|
|
1351
|
+
|
|
868
1352
|
if ((action === 'handoff' || action === 'complete-stage')
|
|
869
1353
|
&& [verificationPlan, scope, invariants].some((v) => typeof v === 'string' && v.trim() !== '')) {
|
|
870
1354
|
console.error(`orchestrate: note: --verification-plan, --scope and --invariants are inputs to doflow-run readiness; this ${action} did not read them.`);
|
|
871
1355
|
}
|
|
872
1356
|
|
|
873
1357
|
try {
|
|
1358
|
+
if (policy !== undefined && (action !== 'decide-gate' || decision !== undefined || forced || policy !== gate)) {
|
|
1359
|
+
return usageError('orchestrate', '--policy requires decide-gate with the matching --gate and refuses --decision and --forced', json);
|
|
1360
|
+
}
|
|
874
1361
|
// `--forced` is only read by the two actions that can override a decision. Accepting it
|
|
875
1362
|
// silently on any other action lets an operator believe a stage completion or skip was
|
|
876
1363
|
// recorded as forced when nothing recorded it — refuse instead of dropping the flag.
|
|
@@ -886,6 +1373,8 @@ function handleOrchestrateCommand({
|
|
|
886
1373
|
if (mcpOutcome !== undefined && action !== 'handoff') {
|
|
887
1374
|
return usageError('orchestrate', `'--mcp-outcome' has no effect on action '${action}' — only handoff records MCP outcomes`, json);
|
|
888
1375
|
}
|
|
1376
|
+
// A catch-up on an absent run starts it implicitly, which counts as a run start for the pointer.
|
|
1377
|
+
const absentBefore = action === 'catch-up' && taskId ? !orchestrator.readRun(taskId) : false;
|
|
889
1378
|
let snapshot;
|
|
890
1379
|
switch (action) {
|
|
891
1380
|
case 'handoff':
|
|
@@ -910,8 +1399,8 @@ function handleOrchestrateCommand({
|
|
|
910
1399
|
snapshot = orchestrator.skipStage({ taskId, stageId: stage, reason: reason ?? note });
|
|
911
1400
|
break;
|
|
912
1401
|
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 });
|
|
1402
|
+
if (!taskId || !gate || (policy === undefined && !decision)) return usageError('orchestrate', 'decide-gate requires --task-id, --gate and --decision approve|reject or --policy <gateId>', json);
|
|
1403
|
+
snapshot = orchestrator.decideGate({ taskId, gateId: gate, decision, policy, note, forced });
|
|
915
1404
|
break;
|
|
916
1405
|
case 'annotate':
|
|
917
1406
|
if (!taskId || !node || !note) return usageError('orchestrate', 'annotate requires --task-id, --node and --note', json);
|
|
@@ -921,18 +1410,44 @@ function handleOrchestrateCommand({
|
|
|
921
1410
|
if (!taskId) return usageError('orchestrate', 'status requires --task-id', json);
|
|
922
1411
|
snapshot = orchestrator.status(taskId);
|
|
923
1412
|
break;
|
|
1413
|
+
case 'timeline': {
|
|
1414
|
+
const run = orchestrator.requireRun(taskId);
|
|
1415
|
+
const body = timelineOf(run, taskId, runSlugOf(run, runRoot) || featureSlug);
|
|
1416
|
+
if (json) console.log(JSON.stringify(body, null, 2));
|
|
1417
|
+
else {
|
|
1418
|
+
console.log(`Timeline ${body.taskId} ${body.featureSlug || '-'} rev ${body.revision}`);
|
|
1419
|
+
for (const e of body.events) {
|
|
1420
|
+
console.log(`${e.at || '-'} ${e.action} ${e.node || '-'} ${e.outcome || e.decision || '-'}${e.forced ? ' forced' : ''}${e.synthetic ? ' synthetic' : ''}`);
|
|
1421
|
+
}
|
|
1422
|
+
if (body.otherEvents > 0) console.log(`other events: ${body.otherEvents}`);
|
|
1423
|
+
}
|
|
1424
|
+
return finishRuntime(0);
|
|
1425
|
+
}
|
|
924
1426
|
default:
|
|
925
|
-
return usageError('orchestrate', `unknown action '${action}'; valid: start | status | handoff | catch-up | complete-stage | skip-stage | decide-gate | annotate`, json);
|
|
1427
|
+
return usageError('orchestrate', `unknown action '${action}'; valid: start | status | list | timeline | handoff | catch-up | complete-stage | skip-stage | decide-gate | annotate`, json);
|
|
1428
|
+
}
|
|
1429
|
+
// Row 135: the feature pointer follows a `feature` run: set when it starts (`start`, or the `catch-up` that
|
|
1430
|
+
// starts it), dropped once it completes.
|
|
1431
|
+
if ((action === 'start' || absentBefore) && snapshot.taskClass === 'feature' && snapshot.state !== 'COMPLETED') {
|
|
1432
|
+
require('./feature-pointer.js').recordFeature({ storeRoot: state, cwd: source, slug: featureSlug, taskId });
|
|
1433
|
+
} else if (action !== 'status' && snapshot.taskClass === 'feature' && snapshot.state === 'COMPLETED') {
|
|
1434
|
+
require('./feature-pointer.js').clearCheckout({ storeRoot: state, cwd: source, slug: featureSlug });
|
|
926
1435
|
}
|
|
927
1436
|
// Only a handoff that recorded work compacts; `deferred` and `standalone` recorded nothing.
|
|
928
1437
|
if (action === 'handoff' && (snapshot.disposition === 'completed' || snapshot.disposition === 'annotated')) {
|
|
929
|
-
snapshot = { ...snapshot, compaction: compactAfterHandoff({ taskId, projectRoot:
|
|
1438
|
+
snapshot = { ...snapshot, compaction: compactAfterHandoff({ taskId, projectRoot: source }) };
|
|
930
1439
|
}
|
|
931
1440
|
if (json) { console.log(JSON.stringify(snapshot, null, 2)); return finishRuntime(0); }
|
|
932
1441
|
console.log(`Workflow ${snapshot.taskId} [${snapshot.taskClass}] — ${snapshot.state}`);
|
|
1442
|
+
// `status` with no --slug opens the one run of the id under any feature (FU-wrnnaq): say whose it is.
|
|
1443
|
+
if (action === 'status' && featureSlug) console.log(`Feature: ${featureSlug}`);
|
|
933
1444
|
if (snapshot.disposition === 'standalone') console.log(`Handoff: standalone (${snapshot.reason})`);
|
|
934
1445
|
else if (snapshot.disposition) console.log(`Handoff: ${snapshot.disposition}`);
|
|
935
1446
|
if (snapshot.compaction) console.log(`Compaction: ${snapshot.compaction.status}${snapshot.compaction.reason ? ` — ${snapshot.compaction.reason}` : ''}`);
|
|
1447
|
+
if (snapshot.policy) console.log(snapshot.decision === 'pass'
|
|
1448
|
+
? `${gate} ${snapshot.taskId} approved by policy ${snapshot.policy.id}@${snapshot.policy.digest} (${snapshot.inputs.length} inputs)`
|
|
1449
|
+
: `${gate} ${snapshot.taskId} denied by policy: ${snapshot.reasons.join(', ')}`);
|
|
1450
|
+
if (snapshot.annotations?.hint) console.log(` ${snapshot.annotations.hint}`);
|
|
936
1451
|
if (snapshot.progress) console.log(`Progress: ${snapshot.progress.done}/${snapshot.progress.total}`);
|
|
937
1452
|
if (snapshot.current) {
|
|
938
1453
|
console.log(snapshot.awaitingGate
|
|
@@ -950,4 +1465,4 @@ function handleOrchestrateCommand({
|
|
|
950
1465
|
}
|
|
951
1466
|
}
|
|
952
1467
|
|
|
953
|
-
module.exports = { WorkflowOrchestrator, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand };
|
|
1468
|
+
module.exports = { WorkflowOrchestrator, RUN_STATES, GATE_DECISIONS, STAGE_OUTCOMES, handleOrchestrateCommand, recordStagePack };
|