mandrel 1.94.0 → 2.1.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/.agents/README.md +116 -99
- package/.agents/agents/acceptance-critic.md +9 -7
- package/.agents/agents/story-worker.md +45 -51
- package/.agents/audit-checklists/performance.md +1 -1
- package/.agents/docs/SDLC.md +498 -1287
- package/.agents/docs/agentrc-reference.json +185 -80
- package/.agents/docs/configuration.md +132 -190
- package/.agents/docs/execution-reference.md +51 -25
- package/.agents/docs/quality-gates.md +25 -33
- package/.agents/docs/workflows.md +8 -8
- package/.agents/instructions.md +113 -139
- package/.agents/rules/ci-remediation.md +11 -15
- package/.agents/rules/git-conventions-reference.md +48 -58
- package/.agents/rules/git-conventions.md +16 -22
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +83 -254
- package/.agents/schemas/audit-rules.json +59 -1
- package/.agents/schemas/audit-rules.schema.json +33 -1
- package/.agents/schemas/lifecycle/README.md +1 -2
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
- package/.agents/schemas/lifecycle/retro.end.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
- package/.agents/schemas/signal-event.schema.json +3 -3
- package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +22 -66
- package/.agents/scripts/agents-bootstrap-github.js +1 -1
- package/.agents/scripts/audit-to-stories.js +7 -7
- package/.agents/scripts/boot-sweep.js +1 -1
- package/.agents/scripts/bootstrap.js +3 -3
- package/.agents/scripts/check-dead-exports.js +43 -104
- package/.agents/scripts/check-doc-links.js +2 -2
- package/.agents/scripts/check-lifecycle-lint.js +7 -10
- package/.agents/scripts/check-workflow-cli-lint.js +91 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +6 -1
- package/.agents/scripts/deliver-recover.js +122 -0
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/evidence-gate.js +20 -50
- package/.agents/scripts/generate-skills-index.js +17 -1
- package/.agents/scripts/generate-workflows-doc.js +4 -4
- package/.agents/scripts/lib/ITicketingProvider.js +1 -19
- package/.agents/scripts/lib/Logger.js +6 -10
- package/.agents/scripts/lib/audit-suite/runner.js +2 -2
- package/.agents/scripts/lib/audit-suite/selector.js +328 -28
- package/.agents/scripts/lib/audit-to-stories/{seed-epic-from-findings.js → seed-from-findings.js} +9 -9
- package/.agents/scripts/lib/baselines/kernel.js +206 -18
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -15
- package/.agents/scripts/lib/baselines/reader.js +1 -6
- package/.agents/scripts/lib/bdd-runner-detect.js +5 -9
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +32 -33
- package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
- package/.agents/scripts/lib/checks/core-bare-clean.js +6 -3
- package/.agents/scripts/lib/checks/index.js +3 -2
- package/.agents/scripts/lib/checks/loop-health.js +12 -11
- package/.agents/scripts/lib/checks/state.js +17 -248
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +26 -24
- package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
- package/.agents/scripts/lib/cli/standard-args.js +13 -22
- package/.agents/scripts/lib/cli-args.js +39 -9
- package/.agents/scripts/lib/close-validation/gates.js +15 -15
- package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
- package/.agents/scripts/lib/close-validation/runner.js +13 -21
- package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
- package/.agents/scripts/lib/config/ci.js +6 -31
- package/.agents/scripts/lib/config/delivery-routing.js +52 -35
- package/.agents/scripts/lib/config/explain.js +61 -48
- package/.agents/scripts/lib/config/github.js +7 -5
- package/.agents/scripts/lib/config/limits.js +29 -80
- package/.agents/scripts/lib/config/paths.js +0 -2
- package/.agents/scripts/lib/config/quality.js +12 -15
- package/.agents/scripts/lib/config/runners.js +20 -66
- package/.agents/scripts/lib/config/temp-paths.js +30 -63
- package/.agents/scripts/lib/config/worktree-isolation.js +0 -5
- package/.agents/scripts/lib/config-resolver.js +2 -7
- package/.agents/scripts/lib/config-settings-schema-delivery.js +55 -161
- package/.agents/scripts/lib/config-settings-schema-quality.js +17 -16
- package/.agents/scripts/lib/config-settings-schema.js +100 -60
- package/.agents/scripts/lib/dead-exports-knip.js +105 -0
- package/.agents/scripts/lib/dead-exports-mode.js +51 -0
- package/.agents/scripts/lib/dependency-parser.js +3 -2
- package/.agents/scripts/lib/doc-tiers.js +2 -2
- package/.agents/scripts/lib/duplicate-search.js +242 -41
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +23 -14
- package/.agents/scripts/lib/format-generated-json.js +97 -0
- package/.agents/scripts/lib/framework-version.js +19 -190
- package/.agents/scripts/lib/gh-exec.js +8 -0
- package/.agents/scripts/lib/git-branch-cleanup.js +1 -10
- package/.agents/scripts/lib/git-branch-lifecycle.js +17 -180
- package/.agents/scripts/lib/git-utils.js +32 -20
- package/.agents/scripts/lib/github/framework-repo.js +6 -0
- package/.agents/scripts/lib/json-utils.js +1 -2
- package/.agents/scripts/lib/label-constants.js +10 -38
- package/.agents/scripts/lib/label-taxonomy.js +10 -55
- package/.agents/scripts/lib/observability/active-story-env.js +44 -165
- package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
- package/.agents/scripts/lib/observability/signal-validator.js +4 -4
- package/.agents/scripts/lib/observability/signals-writer.js +6 -82
- package/.agents/scripts/lib/observability/source-classifier.js +5 -5
- package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
- package/.agents/scripts/lib/onboard/init-tail.js +1 -3
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +105 -44
- package/.agents/scripts/lib/orchestration/code-review.js +78 -436
- package/.agents/scripts/lib/orchestration/column-sync.js +1 -1
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +4 -4
- package/.agents/scripts/lib/orchestration/context-envelope.js +2 -5
- package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
- package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
- package/.agents/scripts/lib/orchestration/docs-digest.js +8 -8
- package/.agents/scripts/lib/orchestration/file-assumptions.js +7 -13
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
- package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +17 -19
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +6 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +21 -43
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +4 -4
- package/.agents/scripts/lib/orchestration/merge-block-class.js +77 -21
- package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
- package/.agents/scripts/lib/orchestration/phase-runner.js +3 -2
- package/.agents/scripts/lib/orchestration/plan-context.js +354 -282
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +4 -8
- package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
- package/.agents/scripts/lib/orchestration/plan-navigation.js +92 -0
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +71 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +245 -0
- package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +432 -858
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +778 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +47 -115
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
- package/.agents/scripts/lib/orchestration/plan-reachability.js +9 -14
- package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +1 -1
- package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/authoring-context.js +52 -51
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +27 -0
- package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/spec-authoring-grounding.js +1 -1
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +18 -28
- package/.agents/scripts/lib/orchestration/remote-verifier.js +1 -1
- package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
- package/.agents/scripts/lib/orchestration/resolves-token.js +1 -1
- package/.agents/scripts/lib/orchestration/retro-proposals.js +140 -79
- package/.agents/scripts/lib/orchestration/review-depth.js +126 -47
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
- package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +21 -56
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
- package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
- package/.agents/scripts/lib/orchestration/run-epilogue.js +784 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +25 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +86 -41
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +77 -32
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +44 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +275 -46
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-freshness.js +14 -205
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +4 -5
- package/.agents/scripts/lib/orchestration/spec-spill.js +60 -0
- package/.agents/scripts/lib/orchestration/split-policy-validator.js +188 -0
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +49 -0
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +28 -42
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +298 -0
- package/.agents/scripts/lib/orchestration/story-init-remote.js +51 -0
- package/.agents/scripts/lib/orchestration/story-plan-state.js +33 -0
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +1 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +60 -25
- package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +130 -75
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +192 -377
- package/.agents/scripts/lib/orchestration/ticket-validator.js +123 -25
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -72
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +45 -55
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +66 -6
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
- package/.agents/scripts/lib/planning-corpus.js +12 -281
- package/.agents/scripts/lib/preflight-runner.js +4 -4
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -5
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
- package/.agents/scripts/lib/signals/index.js +4 -17
- package/.agents/scripts/lib/signals/read.js +35 -35
- package/.agents/scripts/lib/signals/schema.js +8 -11
- package/.agents/scripts/lib/signals/span-tree.js +7 -7
- package/.agents/scripts/lib/signals/write.js +0 -1
- package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
- package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +1 -1
- package/.agents/scripts/lib/skills/parse-skill.js +16 -3
- package/.agents/scripts/lib/story-adjacency.js +17 -19
- package/.agents/scripts/lib/story-body/story-body.js +130 -75
- package/.agents/scripts/lib/story-plan.js +2 -4
- package/.agents/scripts/lib/templates/decomposer-prompts.js +51 -46
- package/.agents/scripts/lib/templates/spec-author-prompts.js +47 -45
- package/.agents/scripts/lib/test-env.js +14 -1
- package/.agents/scripts/lib/test-tiers.js +0 -3
- package/.agents/scripts/lib/{epic-body-sections.js → ticket-body-sections.js} +25 -39
- package/.agents/scripts/lib/validation-evidence.js +32 -60
- package/.agents/scripts/lib/wave-runner/ready-set.js +38 -12
- package/.agents/scripts/lib/workspace-provisioner.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +72 -25
- package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
- package/.agents/scripts/lint-issue-body.js +71 -40
- package/.agents/scripts/mandrel-update-preflight.js +1 -1
- package/.agents/scripts/notify.js +4 -3
- package/.agents/scripts/plan-context.js +102 -80
- package/.agents/scripts/plan-persist.js +230 -279
- package/.agents/scripts/plan-run-epilogue.js +142 -0
- package/.agents/scripts/post-structured-comment.js +0 -38
- package/.agents/scripts/pr-watch-with-update.js +43 -22
- package/.agents/scripts/providers/github/compose.js +0 -1
- package/.agents/scripts/providers/github/errors.js +0 -19
- package/.agents/scripts/providers/github/issues.js +13 -39
- package/.agents/scripts/providers/github/mappers.js +5 -12
- package/.agents/scripts/providers/github/sub-issues.js +0 -47
- package/.agents/scripts/providers/github/tickets.js +33 -156
- package/.agents/scripts/providers/github.js +17 -6
- package/.agents/scripts/resolve-stories.js +236 -0
- package/.agents/scripts/run-coverage.js +4 -1
- package/.agents/scripts/run-lint.js +2 -2
- package/.agents/scripts/run-verify.js +31 -2
- package/.agents/scripts/signals-view.js +25 -21
- package/.agents/scripts/single-story-close.js +178 -26
- package/.agents/scripts/single-story-confirm-merge.js +313 -24
- package/.agents/scripts/single-story-init.js +35 -30
- package/.agents/scripts/stories-wave-tick.js +85 -10
- package/.agents/scripts/story-plan.js +28 -49
- package/.agents/scripts/update-ticket-state.js +14 -65
- package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
- package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
- package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
- package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
- package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
- package/.agents/skills/core/idea-refinement/SKILL.md +9 -9
- package/.agents/skills/core/scope-triage/SKILL.md +31 -172
- package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
- package/.agents/skills/core/security-and-hardening/reference.md +375 -0
- package/.agents/skills/skills.index.json +3 -53
- package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
- package/.agents/starter-agentrc.json +0 -5
- package/.agents/templates/agent-protocol.md +9 -10
- package/.agents/workflows/audit-architecture.md +6 -7
- package/.agents/workflows/audit-clean-code.md +7 -7
- package/.agents/workflows/audit-dependencies.md +3 -3
- package/.agents/workflows/audit-devops.md +3 -3
- package/.agents/workflows/audit-documentation.md +9 -10
- package/.agents/workflows/audit-lighthouse.md +11 -3
- package/.agents/workflows/audit-navigability.md +13 -2
- package/.agents/workflows/audit-performance.md +5 -6
- package/.agents/workflows/audit-privacy.md +3 -3
- package/.agents/workflows/audit-quality.md +11 -12
- package/.agents/workflows/audit-security.md +4 -5
- package/.agents/workflows/audit-seo.md +13 -3
- package/.agents/workflows/audit-sre.md +3 -3
- package/.agents/workflows/audit-to-stories.md +20 -20
- package/.agents/workflows/audit-ux-ui.md +10 -3
- package/.agents/workflows/deliver.md +177 -176
- package/.agents/workflows/git-cleanup.md +5 -6
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +35 -40
- package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
- package/.agents/workflows/helpers/code-review.md +75 -196
- package/.agents/workflows/helpers/{single-story-deliver-reference.md → deliver-story-reference.md} +83 -44
- package/.agents/workflows/helpers/deliver-story.md +606 -0
- package/.agents/workflows/helpers/diagnose.md +10 -10
- package/.agents/workflows/helpers/parallel-tooling.md +3 -3
- package/.agents/workflows/helpers/signals.md +16 -16
- package/.agents/workflows/helpers/worktree-lifecycle.md +66 -86
- package/.agents/workflows/mandrel-update.md +2 -1
- package/.agents/workflows/plan.md +277 -145
- package/.agents/workflows/qa-assist.md +27 -33
- package/.agents/workflows/qa-explore.md +29 -38
- package/.agents/workflows/qa-run.md +2 -6
- package/README.md +9 -8
- package/bin/mandrel.js +12 -1
- package/docs/CHANGELOG.md +70 -0
- package/lib/cli/registry.js +262 -19
- package/lib/cli/sync-agents.js +157 -0
- package/lib/cli/sync-commands.js +115 -6
- package/lib/cli/sync.js +168 -6
- package/lib/cli/update.js +105 -8
- package/lib/cli/version-helpers.js +131 -0
- package/lib/migrations/README.md +7 -5
- package/lib/migrations/index.js +12 -8
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
- package/package.json +2 -2
- package/.agents/agents/retro.md +0 -42
- package/.agents/personas/architect.md +0 -113
- package/.agents/personas/devops-engineer.md +0 -38
- package/.agents/personas/engineer.md +0 -33
- package/.agents/personas/project-manager.md +0 -114
- package/.agents/personas/qa-engineer.md +0 -95
- package/.agents/personas/security-engineer.md +0 -111
- package/.agents/personas/technical-writer.md +0 -101
- package/.agents/schemas/dispatch-manifest.json +0 -232
- package/.agents/schemas/epic-perf-report.schema.json +0 -89
- package/.agents/schemas/epic-spec.schema.json +0 -153
- package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
- package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
- package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
- package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
- package/.agents/schemas/lifecycle/slice.end.schema.json +0 -21
- package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +0 -20
- package/.agents/schemas/lifecycle/slice.start.schema.json +0 -17
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
- package/.agents/schemas/risk-verdict.schema.json +0 -66
- package/.agents/schemas/story-perf-summary.schema.json +0 -73
- package/.agents/scripts/acceptance-spec-reconciler.js +0 -642
- package/.agents/scripts/analyze-execution.js +0 -444
- package/.agents/scripts/bookkeeping-reconcile.js +0 -117
- package/.agents/scripts/check-prepush-recovery.js +0 -90
- package/.agents/scripts/dispatcher.js +0 -295
- package/.agents/scripts/epic-audit-prepare.js +0 -497
- package/.agents/scripts/epic-audit-recheck.js +0 -274
- package/.agents/scripts/epic-deliver-note-intervention.js +0 -192
- package/.agents/scripts/epic-deliver-preflight.js +0 -462
- package/.agents/scripts/epic-deliver-prepare.js +0 -852
- package/.agents/scripts/epic-execute-record-wave.js +0 -449
- package/.agents/scripts/epic-plan-clarity.js +0 -211
- package/.agents/scripts/epic-plan-healthcheck.js +0 -581
- package/.agents/scripts/epic-reconcile.js +0 -625
- package/.agents/scripts/lib/baseline-snapshot.js +0 -979
- package/.agents/scripts/lib/checks/epic-merge-lock-stale.js +0 -54
- package/.agents/scripts/lib/checks/stale-origin-epic.js +0 -49
- package/.agents/scripts/lib/config/lifecycle.js +0 -40
- package/.agents/scripts/lib/config/preflight.js +0 -58
- package/.agents/scripts/lib/config/retro.js +0 -77
- package/.agents/scripts/lib/epic-merge-lock.js +0 -322
- package/.agents/scripts/lib/epic-plan-clarity.js +0 -181
- package/.agents/scripts/lib/epic-plan-ideation.js +0 -261
- package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
- package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
- package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -219
- package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
- package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
- package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
- package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -270
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +0 -539
- package/.agents/scripts/lib/orchestration/deliver-route.js +0 -173
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -134
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +0 -183
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +0 -801
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +0 -310
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +0 -163
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +0 -140
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +0 -64
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +0 -72
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +0 -156
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +0 -345
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +0 -41
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/reconcile-spawn.js +0 -86
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +0 -391
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +0 -94
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +0 -236
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/risk-verdict.js +0 -105
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +0 -307
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +0 -117
- package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +0 -117
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +0 -621
- package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +0 -186
- package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +0 -50
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +0 -129
- package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +0 -103
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -267
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -210
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +0 -238
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/_bullet-format.js +0 -32
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +0 -203
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +0 -227
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +0 -117
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/stalled-worktree.js +0 -37
- package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +0 -127
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +0 -400
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +0 -276
- package/.agents/scripts/lib/orchestration/epic-runner/wave-scheduler.js +0 -66
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +0 -789
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +0 -676
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-discriminator.js +0 -389
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-format.js +0 -230
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +0 -361
- package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
- package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +0 -306
- package/.agents/scripts/lib/orchestration/finalize/post-handoff-comment.js +0 -489
- package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +0 -88
- package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +0 -270
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +0 -147
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +0 -465
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +0 -501
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +0 -984
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +0 -264
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +0 -284
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/cleaner.js +0 -355
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +0 -673
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +0 -378
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/intervention-recorder.js +0 -140
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +0 -144
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +0 -174
- package/.agents/scripts/lib/orchestration/manifest-builder.js +0 -222
- package/.agents/scripts/lib/orchestration/plan-persist/amend.js +0 -359
- package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +0 -127
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
- package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
- package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
- package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
- package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -33
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
- package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -396
- package/.agents/scripts/lib/orchestration/post-merge-pipeline.js +0 -205
- package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
- package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +0 -152
- package/.agents/scripts/lib/orchestration/retro/phases/checks.js +0 -94
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +0 -571
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +0 -450
- package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +0 -191
- package/.agents/scripts/lib/orchestration/retro-heuristics.js +0 -57
- package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
- package/.agents/scripts/lib/orchestration/retro-runner.js +0 -197
- package/.agents/scripts/lib/orchestration/spec-renderer.js +0 -447
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +0 -747
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +0 -211
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +0 -158
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +0 -446
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +0 -297
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/scope-discovery.js +0 -48
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution-wiring.js +0 -67
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution.js +0 -161
- package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +0 -117
- package/.agents/scripts/lib/orchestration/story-close/cd-out-guard.js +0 -86
- package/.agents/scripts/lib/orchestration/story-close/cleanup-reconciler.js +0 -147
- package/.agents/scripts/lib/orchestration/story-close/close-inputs.js +0 -142
- package/.agents/scripts/lib/orchestration/story-close/comment-bodies.js +0 -62
- package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +0 -658
- package/.agents/scripts/lib/orchestration/story-close/merge-subject.js +0 -198
- package/.agents/scripts/lib/orchestration/story-close/phases/branch-restore.js +0 -105
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +0 -222
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +0 -292
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +0 -270
- package/.agents/scripts/lib/orchestration/story-close/phases/preflight.js +0 -110
- package/.agents/scripts/lib/orchestration/story-close/phases/refresh.js +0 -86
- package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked-emitter.js +0 -112
- package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked.js +0 -157
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +0 -421
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +0 -302
- package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +0 -163
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +0 -690
- package/.agents/scripts/lib/orchestration/wave-marker.js +0 -28
- package/.agents/scripts/lib/orchestration/wave-record-io.js +0 -218
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +0 -145
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +0 -212
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +0 -111
- package/.agents/scripts/lib/presentation/manifest-builder.js +0 -239
- package/.agents/scripts/lib/presentation/manifest-formatter.js +0 -242
- package/.agents/scripts/lib/presentation/manifest-helpers.js +0 -213
- package/.agents/scripts/lib/presentation/manifest-persistence.js +0 -261
- package/.agents/scripts/lib/presentation/manifest-procedures.js +0 -55
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +0 -306
- package/.agents/scripts/lib/presentation/manifest-renderer.js +0 -188
- package/.agents/scripts/lib/presentation/manifest-story-views.js +0 -110
- package/.agents/scripts/lib/push-epic-retry.js +0 -209
- package/.agents/scripts/lib/spec/index.js +0 -36
- package/.agents/scripts/lib/spec/loader.js +0 -425
- package/.agents/scripts/lib/spec/state.js +0 -208
- package/.agents/scripts/lib/story-init/blocker-validator.js +0 -68
- package/.agents/scripts/lib/story-init/branch-initializer.js +0 -408
- package/.agents/scripts/lib/story-init/context-resolver.js +0 -92
- package/.agents/scripts/lib/story-init/donor-precheck.js +0 -207
- package/.agents/scripts/lib/story-init/state-transitioner.js +0 -80
- package/.agents/scripts/lib/story-init/task-graph-builder.js +0 -124
- package/.agents/scripts/lib/story-init/transition-summary.js +0 -34
- package/.agents/scripts/lib/test-reserved-epic-temp-ids.js +0 -35
- package/.agents/scripts/lib/wave-runner/tick.js +0 -754
- package/.agents/scripts/lib/wave-runner/wave-runner-error.js +0 -20
- package/.agents/scripts/lifecycle-emit-story-dispatch.js +0 -194
- package/.agents/scripts/lifecycle-emit.js +0 -510
- package/.agents/scripts/retro-run.js +0 -218
- package/.agents/scripts/slice-phase.js +0 -361
- package/.agents/scripts/standalone-feedback-rollup.js +0 -188
- package/.agents/scripts/story-close.js +0 -294
- package/.agents/scripts/story-init.js +0 -599
- package/.agents/scripts/story-phase.js +0 -369
- package/.agents/scripts/wave-tick.js +0 -464
- package/.agents/skills/core/analyze-execution/SKILL.md +0 -98
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +0 -172
- package/.agents/skills/core/epic-plan-consolidate/examples.md +0 -51
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +0 -441
- package/.agents/skills/core/epic-plan-decompose-author/examples.md +0 -47
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +0 -146
- package/.agents/skills/core/epic-plan-premortem/examples.md +0 -53
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +0 -383
- package/.agents/skills/core/epic-plan-spec-author/examples.md +0 -91
- package/.agents/workflows/helpers/deliver-epic-reference.md +0 -547
- package/.agents/workflows/helpers/deliver-epic-single.md +0 -331
- package/.agents/workflows/helpers/deliver-epic.md +0 -998
- package/.agents/workflows/helpers/deliver-stories.md +0 -450
- package/.agents/workflows/helpers/epic-audit.md +0 -189
- package/.agents/workflows/helpers/epic-deliver-story.md +0 -436
- package/.agents/workflows/helpers/epic-testing.md +0 -125
- package/.agents/workflows/helpers/plan-epic-reference.md +0 -160
- package/.agents/workflows/helpers/plan-epic.md +0 -353
- package/.agents/workflows/helpers/plan-story.md +0 -251
- package/.agents/workflows/helpers/scope-triage-gate.md +0 -108
- package/.agents/workflows/helpers/single-story-deliver.md +0 -557
|
@@ -1,135 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* run-plan-persist.js —
|
|
3
|
-
* (
|
|
2
|
+
* run-plan-persist.js — flat Story persist for the v2 `/plan` collapse
|
|
3
|
+
* (Stage 3 — `docs/roadmap.md`).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* retired 12-phase pipeline's separate persist halves
|
|
7
|
-
* (design §1 Step 3 + §2 mode matrix, issue #4474):
|
|
5
|
+
* Ordered, fail-closed pipeline:
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* zero provider calls). Skipped entirely in single mode (no tickets
|
|
22
|
-
* exist to validate).
|
|
23
|
-
* 4.5. deterministic draft reachability (Epic #4474 PR6, design §4 —
|
|
24
|
-
* the 8.4 critic demoted into persist): route-glob scan of the
|
|
25
|
-
* draft set vs `planning.navigation.navRegistry`, mirroring the
|
|
26
|
-
* `--paranoid` F7 healthcheck mechanics. Orphan surfaces are a
|
|
27
|
-
* NAMED SOFT FAILURE (`code: PLAN_REACHABILITY_ORPHANS`, CLI exit
|
|
28
|
-
* 3) raised before any provider call — the author appends the
|
|
29
|
-
* single reachability Story in one targeted amend and re-runs the
|
|
30
|
-
* persist once. Silent no-op when `planning.navigation` is
|
|
31
|
-
* unconfigured; skip decisions are appended to the plan-metrics
|
|
32
|
-
* ledger (`kind: critic-skip`) for audit.
|
|
33
|
-
* 5. ideation fold — `renderEpicBody` / `openEpicFromOnePager` create
|
|
34
|
-
* the Epic when the run starts from a one-pager (the first provider
|
|
35
|
-
* call of the run). Amend additionally resolves every
|
|
36
|
-
* modify/keep/close slug to its live issue and enforces the close-op
|
|
37
|
-
* confirmation gate (exit 2 without `--explicit-delete`) BEFORE the
|
|
38
|
-
* lease and before any mutation.
|
|
39
|
-
* 6. Epic lease (KEEP — documented double-create at ~80 creations).
|
|
40
|
-
* From here the lease is released on EVERY exit path (success, gate
|
|
41
|
-
* failure, throw) via try/finally.
|
|
42
|
-
* 7. managed Tech Spec / Acceptance Table sections + risk-verdict
|
|
43
|
-
* structured comment + spec-freshness advisory.
|
|
44
|
-
* 8. mode-split mutation:
|
|
45
|
-
* - fan-out: story creation via the structural reconciler
|
|
46
|
-
* (idempotent per-slug creation; the reconciler's state file is
|
|
47
|
-
* the per-slug resume ledger), bracketed by checkpoint-v2 writes
|
|
48
|
-
* so a rate-limit crash resumes losslessly with `--resume`;
|
|
49
|
-
* - single: NO story tree — the `delivery::single` routing marker is
|
|
50
|
-
* applied instead (inert until #4475 lands the deliver-side
|
|
51
|
-
* reader), and `decompose = { ticketCount: 0, shape: "single" }`
|
|
52
|
-
* is checkpointed so delivery-time consumers never misread absence
|
|
53
|
-
* as unplanned;
|
|
54
|
-
* - amend: op-mapped delta — close ops close, modify ops
|
|
55
|
-
* close-and-recreate, add ops create, keep ops are untouched by
|
|
56
|
-
* construction; the state ledger and blocked-by edges are rebuilt
|
|
57
|
-
* over the merged set.
|
|
58
|
-
* 9. inline post-plan healthcheck (the `agent::ready` exit condition,
|
|
59
|
-
* Story #2921).
|
|
60
|
-
* 10. single terminal `agent::ready` flip — the intermediate
|
|
61
|
-
* `agent::review-spec` flip is retired on this surface (its readers
|
|
62
|
-
* were visibility-only; the /deliver start gate needs only
|
|
63
|
-
* `agent::ready`).
|
|
64
|
-
* 11. checkpoint v2 + single `plan-summary` comment carrying the dry-run
|
|
65
|
-
* wave table as closing text (replaces the Phase 9 dispatcher
|
|
66
|
-
* round-trip and the Phase 12 notify). The single-mode summary
|
|
67
|
-
* records `{ deliveryShape: "single", sliceCount, routingReasons }`.
|
|
68
|
-
* 12. temp cleanup ONLY at terminal success — a failed run leaves
|
|
69
|
-
* techspec/acceptance/risk-verdict/tickets artifacts on disk so a
|
|
70
|
-
* `--force`/`--resume` re-persist reuses them (fixes the
|
|
71
|
-
* `plan-phase-cleanup.js` mid-pipeline deletion defect).
|
|
7
|
+
* 1. Ticket validator + file-assumption + DAG + capacity + budget
|
|
8
|
+
* 2. Draft reachability (named soft failure, exit 3)
|
|
9
|
+
* 3. Split-policy partition (`assertAcceptancePartition`) + spec fold/spill
|
|
10
|
+
* 4. Create Story issues (`type::story` + sanitized authored labels —
|
|
11
|
+
* deliberately NOT `agent::ready`), resumably via a plan fingerprint
|
|
12
|
+
* 5. Upsert `story-plan-state` on every created Story; upsert `plan-summary`
|
|
13
|
+
* on the primary Story
|
|
14
|
+
* 6. Flip every Story to `agent::ready` — the terminal step, so `ready`
|
|
15
|
+
* always implies "checkpoints written"
|
|
16
|
+
* 7. Comment on + close the superseded `--tickets` source issues
|
|
17
|
+
* (Story #4535) — bookkeeping only; never fails the run
|
|
18
|
+
* 8. Temp cleanup at terminal success + a stale-plan-dir reap
|
|
72
19
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* `
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
20
|
+
* **Why `agent::ready` moved to the end (Story #4541).** Issues used to be
|
|
21
|
+
* born `agent::ready` in the creating POST while the checkpoints were
|
|
22
|
+
* written afterwards. Anything picking a Story up in that window — or after
|
|
23
|
+
* a comment failure aborted the loop — read the checkpoint as `null`
|
|
24
|
+
* (`story-plan-state.js` degrades missing/malformed to `null`). Creating
|
|
25
|
+
* unlabelled, writing checkpoints, then flipping closes that race.
|
|
26
|
+
*
|
|
27
|
+
* **No authored risk artifact (Story #4542).** Persist neither requires nor
|
|
28
|
+
* accepts a risk verdict, derives no envelope from one, and computes no
|
|
29
|
+
* review routing. Review depth and the acceptance-critic mode are derived from
|
|
30
|
+
* the diff at close time (`review-depth.js#deriveChangeLevel`); `--force-review`
|
|
31
|
+
* is an explicit operator flag, recorded here as a receipt and never inferred.
|
|
32
|
+
*
|
|
33
|
+
* Hard cutover: no Epic parent, no reconciler, no `deliveryShape`, no
|
|
34
|
+
* `--amend` tree cascades. Those surfaces die with Stages 4–5 for any
|
|
35
|
+
* remaining epic-delivery readers.
|
|
82
36
|
*
|
|
83
37
|
* @module lib/orchestration/plan-persist/run-plan-persist
|
|
84
38
|
*/
|
|
85
39
|
|
|
86
|
-
import {
|
|
40
|
+
import { readdir, rm, stat } from 'node:fs/promises';
|
|
87
41
|
import path from 'node:path';
|
|
88
42
|
|
|
89
|
-
import {
|
|
90
|
-
import { verifyBddRunnerPendingTag } from '../../bdd-runner-detect.js';
|
|
43
|
+
import { anchorTempRoot, tempRootFrom } from '../../config/temp-paths.js';
|
|
91
44
|
import { getLimits, PROJECT_ROOT } from '../../config-resolver.js';
|
|
92
|
-
import { openEpicFromOnePager } from '../../epic-plan-ideation.js';
|
|
93
45
|
import { gitSpawn } from '../../git-utils.js';
|
|
94
46
|
import { Logger } from '../../Logger.js';
|
|
95
|
-
import {
|
|
96
|
-
AGENT_LABELS,
|
|
97
|
-
DELIVERY_LABELS,
|
|
98
|
-
TYPE_LABELS,
|
|
99
|
-
} from '../../label-constants.js';
|
|
100
|
-
import { cleanupPhaseTempFiles } from '../../plan-phase-cleanup.js';
|
|
101
|
-
import { loadState, writeSpec, writeState } from '../../spec/index.js';
|
|
102
|
-
import {
|
|
103
|
-
reconcileSubIssueLinks,
|
|
104
|
-
setBlockedByDependencies,
|
|
105
|
-
setEpicLabel,
|
|
106
|
-
warnTicketCapNearLimit,
|
|
107
|
-
} from '../epic-plan-decompose/phases/creation.js';
|
|
108
|
-
import {
|
|
109
|
-
enforceFanOutGate,
|
|
110
|
-
runHealthcheckGate,
|
|
111
|
-
surfaceSoftConflictFindings,
|
|
112
|
-
} from '../epic-plan-decompose/phases/persist.js';
|
|
113
|
-
import {
|
|
114
|
-
buildEpicSpecInput,
|
|
115
|
-
validateTickets,
|
|
116
|
-
} from '../epic-plan-decompose/phases/persist-helpers.js';
|
|
117
|
-
import {
|
|
118
|
-
RECONCILE_CLI,
|
|
119
|
-
spawnReconcilerApply,
|
|
120
|
-
} from '../epic-plan-decompose/phases/reconcile-spawn.js';
|
|
121
|
-
import {
|
|
122
|
-
acquireEpicPlanLease,
|
|
123
|
-
assertNoOpenPlanChildren,
|
|
124
|
-
releaseEpicPlanLease,
|
|
125
|
-
} from '../epic-plan-lease-guard.js';
|
|
126
|
-
import { planEpic } from '../epic-plan-spec/phases/plan-epic.js';
|
|
127
|
-
import { runSpecFreshnessCheck } from '../epic-plan-spec/phases/spec-freshness.js';
|
|
128
|
-
import {
|
|
129
|
-
initialize as initializePlanState,
|
|
130
|
-
read as readPlanState,
|
|
131
|
-
write as writePlanState,
|
|
132
|
-
} from '../epic-plan-state-store.js';
|
|
133
47
|
import { evaluatePlanCritics } from '../plan-critics-evaluate.js';
|
|
134
48
|
import {
|
|
135
49
|
appendCriticSkip,
|
|
@@ -141,184 +55,299 @@ import {
|
|
|
141
55
|
evaluateDraftReachability,
|
|
142
56
|
renderReachabilityOrphans,
|
|
143
57
|
} from '../plan-reachability.js';
|
|
144
|
-
import { resolveReviewRouting } from '../plan-review-routing.js';
|
|
145
|
-
import { deriveRiskEnvelope } from '../planning-risk.js';
|
|
146
|
-
import { renderSpec } from '../spec-renderer.js';
|
|
147
|
-
import {
|
|
148
|
-
formatMissingSectionMessage,
|
|
149
|
-
validateSpecSections,
|
|
150
|
-
} from '../spec-section-validator.js';
|
|
151
58
|
import { upsertStructuredComment } from '../ticketing.js';
|
|
152
59
|
import {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
60
|
+
enforceFanOutGate,
|
|
61
|
+
surfaceSoftConflictFindings,
|
|
62
|
+
} from './fan-out-gate.js';
|
|
63
|
+
import { resolveBaseBranchRef, validateTickets } from './persist-helpers.js';
|
|
64
|
+
import {
|
|
65
|
+
assemblePlanStories,
|
|
66
|
+
createStoryIssues,
|
|
67
|
+
markStoriesReady,
|
|
68
|
+
} from './story-ops.js';
|
|
161
69
|
import {
|
|
162
70
|
buildPlanSummaryCommentBody,
|
|
163
71
|
buildWaveTable,
|
|
164
72
|
PLAN_SUMMARY_COMMENT_TYPE,
|
|
165
73
|
} from './summary.js';
|
|
74
|
+
import { closeSupersededTickets } from './supersede-ops.js';
|
|
166
75
|
|
|
167
|
-
/** Checkpoint schema version written
|
|
168
|
-
|
|
76
|
+
/** Checkpoint schema version written on each Story's story-plan-state. */
|
|
77
|
+
const PLAN_CHECKPOINT_SCHEMA_VERSION_V2 = 2;
|
|
169
78
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// API keeps a single import root for the persist surface.
|
|
173
|
-
export { resolveDeliveryMode };
|
|
79
|
+
/** Structured-comment type for the per-plan Story checkpoint. */
|
|
80
|
+
const STORY_PLAN_STATE_TYPE = 'story-plan-state';
|
|
174
81
|
|
|
175
82
|
/**
|
|
176
|
-
*
|
|
177
|
-
* current checkpoint (or initializes a fresh skeleton), shallow-merges
|
|
178
|
-
* `patch`, and stamps `version: 2`. Field shapes for `planningRisk`,
|
|
179
|
-
* `riskVerdict`, `reviewRouting`, `spec`, and `decompose` are byte-compatible
|
|
180
|
-
* with v1 — v2 is additive only.
|
|
83
|
+
* Write the `story-plan-state` checkpoint on a Story.
|
|
181
84
|
*
|
|
182
85
|
* @param {object} provider
|
|
183
|
-
* @param {number}
|
|
184
|
-
* @param {object}
|
|
185
|
-
* @returns {Promise<object>} the written checkpoint payload
|
|
86
|
+
* @param {number} storyId
|
|
87
|
+
* @param {object} state
|
|
186
88
|
*/
|
|
187
|
-
export async function writeCheckpointV2(provider,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
(await initializePlanState({ provider, epicId }));
|
|
191
|
-
// One-level deep merge for object-valued blocks (`spec`, `decompose`,
|
|
192
|
-
// `persist`, …) so a partial patch (e.g. `persist: { completedAt }`)
|
|
193
|
-
// refines rather than replaces the block — same discipline the v1
|
|
194
|
-
// writers applied by hand with `...currentState.decompose`.
|
|
195
|
-
const merged = { ...current };
|
|
196
|
-
for (const [key, value] of Object.entries(patch ?? {})) {
|
|
197
|
-
const existing = merged[key];
|
|
198
|
-
if (
|
|
199
|
-
value !== null &&
|
|
200
|
-
typeof value === 'object' &&
|
|
201
|
-
!Array.isArray(value) &&
|
|
202
|
-
existing !== null &&
|
|
203
|
-
typeof existing === 'object' &&
|
|
204
|
-
!Array.isArray(existing)
|
|
205
|
-
) {
|
|
206
|
-
merged[key] = { ...existing, ...value };
|
|
207
|
-
} else {
|
|
208
|
-
merged[key] = value;
|
|
209
|
-
}
|
|
89
|
+
export async function writeCheckpointV2(provider, storyId, state) {
|
|
90
|
+
if (!Number.isInteger(storyId)) {
|
|
91
|
+
throw new TypeError('writeCheckpointV2 requires a numeric storyId');
|
|
210
92
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
93
|
+
const body = [
|
|
94
|
+
'### story-plan-state',
|
|
95
|
+
'',
|
|
96
|
+
'```json',
|
|
97
|
+
JSON.stringify(
|
|
98
|
+
{
|
|
99
|
+
version: PLAN_CHECKPOINT_SCHEMA_VERSION_V2,
|
|
100
|
+
storyId,
|
|
101
|
+
...state,
|
|
102
|
+
},
|
|
103
|
+
null,
|
|
104
|
+
2,
|
|
105
|
+
),
|
|
106
|
+
'```',
|
|
107
|
+
].join('\n');
|
|
108
|
+
await upsertStructuredComment(provider, storyId, STORY_PLAN_STATE_TYPE, body);
|
|
109
|
+
return state;
|
|
219
110
|
}
|
|
220
111
|
|
|
221
112
|
/**
|
|
222
|
-
*
|
|
113
|
+
* Enforce the ticket validator's findings and report what the file-assumption
|
|
114
|
+
* gate actually concluded.
|
|
223
115
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* orphaned Epic behind.
|
|
116
|
+
* The return value feeds the posted `plan-summary`'s freshness line, which
|
|
117
|
+
* used to be hard-coded `{ stale: 0, ambiguous: 0 }` — so the comment read
|
|
118
|
+
* "Spec freshness: clean" even on the one run where the gate had *given up*
|
|
119
|
+
* (Story #4541's unresolvable-base-ref downgrade). The summary asserted a
|
|
120
|
+
* clean result precisely when it had the least evidence for one.
|
|
230
121
|
*
|
|
231
|
-
*
|
|
122
|
+
* The mapping is deliberate. A confirmed mismatch is never reported here at
|
|
123
|
+
* all — it throws, and the run posts no summary. The only findings that
|
|
124
|
+
* survive to the summary are ones the gate could not *verify*, because the
|
|
125
|
+
* base ref they were computed against does not resolve; unverifiable is
|
|
126
|
+
* `ambiguous`, not `stale`.
|
|
232
127
|
*
|
|
233
|
-
* @returns {
|
|
128
|
+
* @returns {{ stale: number, ambiguous: number }} Freshness counts for the
|
|
129
|
+
* posted summary.
|
|
234
130
|
*/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
131
|
+
function enforceTicketValidation(validated, { config, settings, cwd }) {
|
|
132
|
+
const validationErrors = validated.errors ?? [];
|
|
133
|
+
const assumptionFailures = validationErrors.filter((error) =>
|
|
134
|
+
error.startsWith('File assumption mismatch:'),
|
|
135
|
+
);
|
|
136
|
+
const blockingErrors = validationErrors.filter(
|
|
137
|
+
(error) => !error.startsWith('File assumption mismatch:'),
|
|
138
|
+
);
|
|
139
|
+
if (blockingErrors.length > 0) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`[plan-persist] ticket validation failed with ${blockingErrors.length} ` +
|
|
142
|
+
`hard error(s):\n${blockingErrors.map((error) => ` - ${error}`).join('\n')}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (assumptionFailures.length === 0) return { stale: 0, ambiguous: 0 };
|
|
146
|
+
// Story #4541: resolve through the canonical `project.baseBranch` (the
|
|
147
|
+
// shape `config-resolver` actually emits) with the legacy settings bag as
|
|
148
|
+
// a fallback — reading a bare `config.baseBranch` meant this probe always
|
|
149
|
+
// targeted the literal `main`.
|
|
150
|
+
const gateBaseRef =
|
|
151
|
+
config?.project?.baseBranch ??
|
|
152
|
+
settings?.baseBranch ??
|
|
153
|
+
resolveBaseBranchRef(config);
|
|
154
|
+
const refResolves =
|
|
155
|
+
gitSpawn(
|
|
156
|
+
cwd ?? process.cwd(),
|
|
157
|
+
'rev-parse',
|
|
158
|
+
'--verify',
|
|
159
|
+
'--quiet',
|
|
160
|
+
`${gateBaseRef}^{commit}`,
|
|
161
|
+
).status === 0;
|
|
162
|
+
if (refResolves) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`[plan-persist] file-assumption gate: ${assumptionFailures.length} ` +
|
|
165
|
+
`mismatch(es):\n${assumptionFailures.map((error) => ` - ${error}`).join('\n')}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
Logger.warn(
|
|
169
|
+
`[plan-persist] file-assumption gate skipped: base ref '${gateBaseRef}' ` +
|
|
170
|
+
`does not resolve — ${assumptionFailures.length} finding(s) downgraded.`,
|
|
171
|
+
);
|
|
172
|
+
return { stale: 0, ambiguous: assumptionFailures.length };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Run the supersede close phase behind a belt-and-braces guard.
|
|
177
|
+
*
|
|
178
|
+
* `closeSupersededTickets` already swallows per-ticket failures, but this
|
|
179
|
+
* phase runs *after* `createIssue`, so an unexpected throw anywhere in it
|
|
180
|
+
* would leave the run half-done with Stories already live. Degrade to a
|
|
181
|
+
* reported failure instead.
|
|
182
|
+
*
|
|
183
|
+
* @returns {Promise<import('./supersede-ops.js').SupersedeReport>}
|
|
184
|
+
*/
|
|
185
|
+
async function runSupersedePhase(args) {
|
|
186
|
+
try {
|
|
187
|
+
return await closeSupersededTickets(args);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
Logger.warn(
|
|
190
|
+
`[plan-persist] supersede close phase failed: ${err.message} — ` +
|
|
191
|
+
'Stories were created; close the source tickets by hand.',
|
|
192
|
+
);
|
|
193
|
+
return {
|
|
194
|
+
enabled: true,
|
|
195
|
+
dryRun: args.dryRun === true,
|
|
196
|
+
reason: `phase-error: ${err.message}`,
|
|
197
|
+
closed: [],
|
|
198
|
+
planned: [],
|
|
199
|
+
skipped: [],
|
|
200
|
+
failed: (args.sourceTicketIds ?? []).map((ticket) => ({
|
|
201
|
+
ticket,
|
|
202
|
+
reason: err.message,
|
|
203
|
+
})),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Render the plan-metrics line for the **posted** `plan-summary` comment,
|
|
210
|
+
* scoped to this invocation.
|
|
211
|
+
*
|
|
212
|
+
* The ordering hazard this closes: the ledger record for the current run is
|
|
213
|
+
* appended by `recordPlanInvocation`'s `finally`, which by construction only
|
|
214
|
+
* fires once `runPlanPersist` has *resolved* — long after this comment body
|
|
215
|
+
* is composed. A plain `since`-filtered ledger read therefore summarized
|
|
216
|
+
* every run except the one being summarized, and on an otherwise-quiet
|
|
217
|
+
* ledger it rendered "plan-metrics: no invocations recorded" onto the very
|
|
218
|
+
* comment reporting the run. (The stdout envelope was always correct: its
|
|
219
|
+
* `attachPlanMetrics` read runs after the wrapper returns.)
|
|
220
|
+
*
|
|
221
|
+
* **The `finally` stays where it is.** It is what guarantees a persist that
|
|
222
|
+
* throws is still recorded, so moving the append earlier — or moving this
|
|
223
|
+
* read later, past the GitHub writes it feeds — would trade a cosmetic bug
|
|
224
|
+
* for a real one. Instead of racing it, fold in a synthetic record standing
|
|
225
|
+
* for the in-flight invocation. It is the same record the wrapper is about to
|
|
226
|
+
* write, minus a final duration; it cannot double-count, because the real one
|
|
227
|
+
* does not exist yet at this point in the pipeline.
|
|
228
|
+
*
|
|
229
|
+
* `ok: true` is honest here: this line is only ever composed on the success
|
|
230
|
+
* path, after `createStoryIssues` has returned.
|
|
231
|
+
*
|
|
232
|
+
* @param {{ config: object, since: string, startedAt: string, mode: string }} args
|
|
233
|
+
* @returns {Promise<string|null>}
|
|
234
|
+
*/
|
|
235
|
+
async function renderRunScopedPlanMetricsLine({
|
|
236
|
+
config,
|
|
237
|
+
since,
|
|
238
|
+
startedAt,
|
|
239
|
+
mode,
|
|
240
240
|
}) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
241
|
+
try {
|
|
242
|
+
const ledger = await readPlanMetrics(null, config);
|
|
243
|
+
const endedAt = new Date().toISOString();
|
|
244
|
+
const inFlight = {
|
|
245
|
+
v: 1,
|
|
246
|
+
cli: 'plan-persist',
|
|
247
|
+
mode,
|
|
248
|
+
epicId: null,
|
|
249
|
+
startedAt,
|
|
250
|
+
endedAt,
|
|
251
|
+
durationMs: Math.max(0, Date.parse(endedAt) - Date.parse(startedAt)) || 0,
|
|
252
|
+
ok: true,
|
|
253
|
+
};
|
|
254
|
+
const summary = summarizePlanMetrics(
|
|
255
|
+
{ ...ledger, entries: [...(ledger.entries ?? []), inFlight] },
|
|
256
|
+
{ since },
|
|
255
257
|
);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
return { epicId: created.id, epic, created: true };
|
|
258
|
+
return renderPlanMetricsSummaryLine(summary);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
Logger.warn(`[plan-persist] plan-metrics summary skipped: ${err.message}`);
|
|
261
|
+
return null;
|
|
263
262
|
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Age after which an abandoned `temp/plan-*` directory is reaped. A plan run
|
|
267
|
+
* that is still being authored is minutes-to-hours old; a week is far past
|
|
268
|
+
* any live run and comfortably past an operator returning to a paused one.
|
|
269
|
+
*/
|
|
270
|
+
const STALE_PLAN_DIR_MS = 7 * 24 * 60 * 60 * 1000;
|
|
264
271
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Reap abandoned `plan-*` directories under the temp root (Story #4541).
|
|
274
|
+
*
|
|
275
|
+
* Terminal-success cleanup only ever removed the *current* run's `planDir`,
|
|
276
|
+
* so every plan that failed a gate, was abandoned mid-authoring, or ran
|
|
277
|
+
* `--dry-run` left its directory behind forever. This sweeps the stragglers
|
|
278
|
+
* on each persist.
|
|
279
|
+
*
|
|
280
|
+
* Best-effort throughout: this is hygiene, never a reason to fail a run that
|
|
281
|
+
* has already created Stories. The current run's own `planDir` is always
|
|
282
|
+
* excluded — its cleanup is the caller's decision.
|
|
283
|
+
*
|
|
284
|
+
* @param {{ config?: object, keepDir?: string|null, now?: number }} args
|
|
285
|
+
* @returns {Promise<{ reaped: string[] }>}
|
|
286
|
+
*/
|
|
287
|
+
export async function reapStalePlanDirs({
|
|
288
|
+
config = {},
|
|
289
|
+
keepDir = null,
|
|
290
|
+
now = Date.now(),
|
|
291
|
+
} = {}) {
|
|
292
|
+
const reaped = [];
|
|
293
|
+
const tempRoot = anchorTempRoot(tempRootFrom(config));
|
|
294
|
+
let entries;
|
|
295
|
+
try {
|
|
296
|
+
entries = await readdir(tempRoot, { withFileTypes: true });
|
|
297
|
+
} catch {
|
|
298
|
+
return { reaped }; // No temp root yet — nothing to reap.
|
|
268
299
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
300
|
+
const keep = keepDir ? path.resolve(keepDir) : null;
|
|
301
|
+
for (const entry of entries) {
|
|
302
|
+
if (!entry.isDirectory() || !entry.name.startsWith('plan-')) continue;
|
|
303
|
+
const dir = path.resolve(tempRoot, entry.name);
|
|
304
|
+
if (keep !== null && dir === keep) continue;
|
|
305
|
+
try {
|
|
306
|
+
const { mtimeMs } = await stat(dir);
|
|
307
|
+
if (now - mtimeMs < STALE_PLAN_DIR_MS) continue;
|
|
308
|
+
await rm(dir, { recursive: true, force: true });
|
|
309
|
+
reaped.push(dir);
|
|
310
|
+
} catch {
|
|
311
|
+
// A racing writer or a permission error: leave it for the next run.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (reaped.length > 0) {
|
|
315
|
+
Logger.info(
|
|
316
|
+
`[plan-persist] reaped ${reaped.length} abandoned plan director(ies) ` +
|
|
317
|
+
`older than 7d under ${tempRoot}.`,
|
|
272
318
|
);
|
|
273
319
|
}
|
|
274
|
-
return {
|
|
320
|
+
return { reaped };
|
|
275
321
|
}
|
|
276
322
|
|
|
277
323
|
/**
|
|
278
|
-
* Execute the
|
|
279
|
-
* order). Modes: fan-out (default), single (`deliveryShape: "single"`),
|
|
280
|
-
* amend (`--amend`).
|
|
324
|
+
* Execute the flat Story persist end to end.
|
|
281
325
|
*
|
|
282
326
|
* @param {{
|
|
283
|
-
*
|
|
284
|
-
* provider: import('../../ITicketingProvider.js').ITicketingProvider,
|
|
327
|
+
* provider: object,
|
|
285
328
|
* artifacts: {
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
* tickets?: Array<object>|null,
|
|
290
|
-
* onePagerContent?: string|null,
|
|
291
|
-
* templateContent?: string|null,
|
|
329
|
+
* stories: Array<object>,
|
|
330
|
+
* techSpecContent?: string|null,
|
|
331
|
+
* planAcceptance?: string[]|null,
|
|
292
332
|
* },
|
|
293
333
|
* config?: object,
|
|
294
|
-
* settings?:
|
|
334
|
+
* settings?: object,
|
|
295
335
|
* opts?: {
|
|
296
|
-
* force?: boolean,
|
|
297
|
-
* resume?: boolean,
|
|
298
|
-
* amend?: boolean,
|
|
299
|
-
* explicitDelete?: boolean,
|
|
300
|
-
* steal?: boolean,
|
|
301
336
|
* forceReview?: boolean,
|
|
302
337
|
* allowOverBudget?: boolean,
|
|
303
338
|
* allowLargeFanOut?: boolean,
|
|
304
|
-
* // test seams (production callers must not set these)
|
|
305
|
-
* skipHealthcheck?: boolean,
|
|
306
339
|
* skipCleanup?: boolean,
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* renderSpecFn?: typeof renderSpec,
|
|
311
|
-
* loadStateFn?: typeof loadState,
|
|
312
|
-
* writeStateFn?: typeof writeState,
|
|
313
|
-
* runHealthcheckFn?: typeof defaultRunPlanHealthcheck,
|
|
314
|
-
* bddProbeFn?: typeof verifyBddRunnerPendingTag,
|
|
315
|
-
* fanOutCounter?: (arg: { path: string }) => number,
|
|
340
|
+
* dryRun?: boolean,
|
|
341
|
+
* planDir?: string,
|
|
342
|
+
* fanOutCounter?: Function,
|
|
316
343
|
* cwd?: string,
|
|
344
|
+
* sourceTicketIds?: number[],
|
|
345
|
+
* sourceTicketOrigin?: 'flag'|'envelope'|'none',
|
|
346
|
+
* closeSuperseded?: boolean,
|
|
317
347
|
* },
|
|
318
348
|
* }} input
|
|
319
349
|
*/
|
|
320
350
|
export async function runPlanPersist({
|
|
321
|
-
epicId: requestedEpicId = null,
|
|
322
351
|
provider,
|
|
323
352
|
artifacts,
|
|
324
353
|
config = {},
|
|
@@ -326,227 +355,92 @@ export async function runPlanPersist({
|
|
|
326
355
|
opts = {},
|
|
327
356
|
}) {
|
|
328
357
|
const {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
tickets = null,
|
|
333
|
-
onePagerContent = null,
|
|
334
|
-
templateContent = null,
|
|
358
|
+
stories: rawStories = null,
|
|
359
|
+
techSpecContent = null,
|
|
360
|
+
planAcceptance = null,
|
|
335
361
|
} = artifacts ?? {};
|
|
336
362
|
const {
|
|
337
|
-
force = false,
|
|
338
|
-
resume = false,
|
|
339
|
-
amend = false,
|
|
340
|
-
explicitDelete = false,
|
|
341
|
-
steal = false,
|
|
342
363
|
forceReview = false,
|
|
343
364
|
allowOverBudget = false,
|
|
344
365
|
allowLargeFanOut = false,
|
|
345
|
-
skipHealthcheck = false,
|
|
346
366
|
skipCleanup = false,
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
writeSpecFn = writeSpec,
|
|
350
|
-
renderSpecFn = renderSpec,
|
|
351
|
-
loadStateFn = loadState,
|
|
352
|
-
writeStateFn = writeState,
|
|
353
|
-
runHealthcheckFn = defaultRunPlanHealthcheck,
|
|
354
|
-
bddProbeFn = verifyBddRunnerPendingTag,
|
|
367
|
+
dryRun = false,
|
|
368
|
+
planDir = null,
|
|
355
369
|
fanOutCounter = undefined,
|
|
356
370
|
cwd = PROJECT_ROOT,
|
|
371
|
+
sourceTicketIds = [],
|
|
372
|
+
sourceTicketOrigin = 'none',
|
|
373
|
+
closeSuperseded = true,
|
|
357
374
|
} = opts;
|
|
358
375
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
if (amend && (force || resume)) {
|
|
366
|
-
throw new Error(
|
|
367
|
-
'[plan-persist] --amend is mutually exclusive with --force/--resume ' +
|
|
368
|
-
'— the amend delta is already an incremental re-persist.',
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
if (amend && onePagerContent) {
|
|
372
|
-
throw new Error(
|
|
373
|
-
'[plan-persist] --amend requires --epic <id> — there is no existing ' +
|
|
374
|
-
'plan to amend in ideation mode.',
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
if (onePagerContent && resume) {
|
|
378
|
-
throw new Error(
|
|
379
|
-
'[plan-persist] --resume requires --epic <id> — the Epic already ' +
|
|
380
|
-
"exists after the first attempt (its number is in the failed run's " +
|
|
381
|
-
'output); an ideation --resume would open a duplicate.',
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
if (onePagerContent && !templateContent) {
|
|
385
|
-
throw new Error(
|
|
386
|
-
'[plan-persist] ideation mode requires the epic-from-idea template ' +
|
|
387
|
-
'content (templateContent).',
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
if (!onePagerContent && !Number.isInteger(requestedEpicId)) {
|
|
391
|
-
throw new Error(
|
|
392
|
-
'[plan-persist] either --epic <id> or --one-pager <path> is required.',
|
|
393
|
-
);
|
|
394
|
-
}
|
|
376
|
+
// Boundary for the plan-metrics summary below: everything this invocation
|
|
377
|
+
// appends to the ledger is stamped at or after this instant, so filtering
|
|
378
|
+
// on it scopes the counts to *this* run rather than every plan ever run
|
|
379
|
+
// through the shared standalone ledger (Story #4541).
|
|
380
|
+
const runStartedAt = opts.metricsSince ?? new Date().toISOString();
|
|
395
381
|
|
|
396
|
-
|
|
397
|
-
// A rejection here has made zero GitHub calls (locked in by the
|
|
398
|
-
// fail-closed-ordering test).
|
|
399
|
-
const sectionCheck = validateSpecSections({ body: techSpecContent });
|
|
400
|
-
if (!sectionCheck.ok) {
|
|
382
|
+
if (!Array.isArray(rawStories) || rawStories.length === 0) {
|
|
401
383
|
throw new Error(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
missing: sectionCheck.missing,
|
|
405
|
-
}),
|
|
384
|
+
'[plan-persist] stories payload must be a non-empty array ' +
|
|
385
|
+
'(--stories <file>). Default is one Story.',
|
|
406
386
|
);
|
|
407
387
|
}
|
|
408
388
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (!riskVerdict || !Array.isArray(riskVerdict.axes)) {
|
|
389
|
+
const maxTickets = getLimits(config).maxTickets;
|
|
390
|
+
if (rawStories.length > maxTickets && !allowOverBudget) {
|
|
412
391
|
throw new Error(
|
|
413
|
-
|
|
414
|
-
|
|
392
|
+
`[plan-persist] Stories (${rawStories.length}) exceed the reviewability ` +
|
|
393
|
+
`budget (${maxTickets}). Re-scope, or rerun with --allow-over-budget.`,
|
|
415
394
|
);
|
|
416
395
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// amend mode every gate runs over the MERGED set — keeps + adds +
|
|
422
|
-
// modifies, closes excluded — so the DAG is validated against the tree
|
|
423
|
-
// that will actually exist post-amend. The skip branch below is fenced
|
|
424
|
-
// by construction: `resolveDeliveryMode` hard-refuses `deliveryShape:
|
|
425
|
-
// "single"` with any tickets payload, so single mode can only reach here
|
|
426
|
-
// with no tickets to validate. ----
|
|
427
|
-
let validated = null;
|
|
428
|
-
let amendPartition = null;
|
|
429
|
-
let reachability = null;
|
|
430
|
-
let gateSet = null;
|
|
431
|
-
if (mode !== 'single') {
|
|
432
|
-
amendPartition = mode === 'amend' ? partitionAmendTickets(tickets) : null;
|
|
433
|
-
gateSet = mode === 'amend' ? buildMergedTicketSet(tickets) : tickets;
|
|
434
|
-
const maxTickets = getLimits(config).maxTickets;
|
|
435
|
-
if (gateSet.length > maxTickets && !allowOverBudget) {
|
|
436
|
-
throw new Error(
|
|
437
|
-
`[plan-persist] Tickets (${gateSet.length}) exceed the reviewability ` +
|
|
438
|
-
`budget (${maxTickets}). Re-scope the Epic into a smaller plan, or ` +
|
|
439
|
-
'rerun with --allow-over-budget after confirming the over-budget ' +
|
|
440
|
-
'rationale on the Epic.',
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
warnTicketCapNearLimit(gateSet, maxTickets, 'plan-persist');
|
|
444
|
-
if (gateSet.length > maxTickets && allowOverBudget) {
|
|
445
|
-
Logger.warn(
|
|
446
|
-
`[plan-persist] Persisting an over-budget decomposition: ${gateSet.length} ` +
|
|
447
|
-
`tickets vs. budget ${maxTickets} (operator override --allow-over-budget).`,
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
Logger.info(
|
|
451
|
-
`[plan-persist] Running cross-validation on ${gateSet.length} tickets` +
|
|
452
|
-
`${mode === 'amend' ? ' (merged amend set)' : ''}...`,
|
|
396
|
+
if (rawStories.length > maxTickets && allowOverBudget) {
|
|
397
|
+
Logger.warn(
|
|
398
|
+
`[plan-persist] Persisting an over-budget plan: ${rawStories.length} ` +
|
|
399
|
+
`Stories vs. budget ${maxTickets} (--allow-over-budget).`,
|
|
453
400
|
);
|
|
454
|
-
|
|
455
|
-
enforceFanOutGate(validated.findings, allowLargeFanOut, 'plan-persist');
|
|
456
|
-
surfaceSoftConflictFindings(validated.findings, 'plan-persist');
|
|
401
|
+
}
|
|
457
402
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const gateBaseRef = config?.baseBranch ?? 'main';
|
|
474
|
-
const refResolves =
|
|
475
|
-
gitSpawn(
|
|
476
|
-
cwd ?? process.cwd(),
|
|
477
|
-
'rev-parse',
|
|
478
|
-
'--verify',
|
|
479
|
-
'--quiet',
|
|
480
|
-
`${gateBaseRef}^{commit}`,
|
|
481
|
-
).status === 0;
|
|
482
|
-
if (refResolves) {
|
|
483
|
-
throw new Error(
|
|
484
|
-
`[plan-persist] file-assumption gate: ${assumptionFailures.length} ` +
|
|
485
|
-
`mismatch(es) between declared assumptions and the base branch:\n` +
|
|
486
|
-
`${assumptionFailures.map((e) => ` - ${e}`).join('\n')}\n` +
|
|
487
|
-
'Fix the Story change declarations (or the plan) and re-run the persist.',
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
Logger.warn(
|
|
491
|
-
`[plan-persist] file-assumption gate skipped: base ref '${gateBaseRef}' ` +
|
|
492
|
-
`does not resolve in this checkout — ${assumptionFailures.length} ` +
|
|
493
|
-
'finding(s) downgraded to warnings.',
|
|
494
|
-
);
|
|
495
|
-
for (const e of assumptionFailures) {
|
|
496
|
-
Logger.warn(`[plan-persist] ${e}`);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
403
|
+
Logger.info(
|
|
404
|
+
`[plan-persist] Running cross-validation on ${rawStories.length} Story ticket(s)...`,
|
|
405
|
+
);
|
|
406
|
+
const validated = validateTickets(rawStories, config, {
|
|
407
|
+
fanOutCounter,
|
|
408
|
+
cwd,
|
|
409
|
+
modelCapacity: opts.modelCapacity,
|
|
410
|
+
});
|
|
411
|
+
enforceFanOutGate(validated.findings, allowLargeFanOut, 'plan-persist');
|
|
412
|
+
surfaceSoftConflictFindings(validated.findings, 'plan-persist');
|
|
413
|
+
const freshness = enforceTicketValidation(validated, {
|
|
414
|
+
config,
|
|
415
|
+
settings,
|
|
416
|
+
cwd,
|
|
417
|
+
});
|
|
499
418
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
Logger.info(`[plan-persist] reachability: ${reachability.reasons[0]}`);
|
|
511
|
-
} else {
|
|
512
|
-
reachability = {
|
|
513
|
-
status: 'skipped',
|
|
514
|
-
reasons: [
|
|
515
|
-
'single-delivery shape — no draft story tree to scan for orphan surfaces.',
|
|
516
|
-
],
|
|
517
|
-
orphans: [],
|
|
518
|
-
scanned: 0,
|
|
519
|
-
};
|
|
419
|
+
const reachability = evaluateDraftReachability({
|
|
420
|
+
tickets: rawStories,
|
|
421
|
+
config,
|
|
422
|
+
});
|
|
423
|
+
if (reachability.status === 'orphans') {
|
|
424
|
+
const err = new Error(renderReachabilityOrphans(reachability));
|
|
425
|
+
err.code = 'PLAN_REACHABILITY_ORPHANS';
|
|
426
|
+
err.orphans = reachability.orphans;
|
|
427
|
+
throw err;
|
|
520
428
|
}
|
|
429
|
+
Logger.info(`[plan-persist] reachability: ${reachability.reasons[0]}`);
|
|
521
430
|
if (reachability.status === 'skipped') {
|
|
522
|
-
// Audit trail for the skip decision (#4474 PR6). Best-effort by
|
|
523
|
-
// contract — a failed append never fails the persist. Logged before
|
|
524
|
-
// the first provider call so even an ideation run that later fails
|
|
525
|
-
// still records the decision (ideation has no Epic id yet, so the
|
|
526
|
-
// record lands on the standalone stream).
|
|
527
431
|
await appendCriticSkip(
|
|
528
432
|
{
|
|
529
433
|
critic: 'reachability',
|
|
530
434
|
reasons: reachability.reasons,
|
|
531
435
|
cli: 'plan-persist',
|
|
532
|
-
epicId: requestedEpicId,
|
|
533
436
|
},
|
|
534
437
|
config,
|
|
535
438
|
);
|
|
536
439
|
}
|
|
537
440
|
|
|
538
|
-
// ---- Step 4.7: folded critic dispatch evaluation (#4496 fix 6 — the
|
|
539
|
-
// former standalone `plan-critics.js` turn). Deterministic and git-local,
|
|
540
|
-
// still zero provider calls: the verdicts are printed (and returned on
|
|
541
|
-
// the result) as part of the pre-write phase, and every skip decision is
|
|
542
|
-
// appended to the plan-metrics ledger exactly as the standalone CLI did,
|
|
543
|
-
// so under-firing stays auditable without a separate invocation on the
|
|
544
|
-
// headless path. Advisory by construction — the deterministic validators
|
|
545
|
-
// above remain the unchanged hard gates. ----
|
|
546
441
|
const critics = evaluatePlanCritics({
|
|
547
|
-
techSpecContent,
|
|
548
|
-
|
|
549
|
-
tickets: gateSet,
|
|
442
|
+
techSpecContent: techSpecContent ?? '',
|
|
443
|
+
tickets: rawStories,
|
|
550
444
|
config,
|
|
551
445
|
});
|
|
552
446
|
for (const decision of [critics.consolidation, critics.premortem]) {
|
|
@@ -556,460 +450,140 @@ export async function runPlanPersist({
|
|
|
556
450
|
decision.reasons.join('; '),
|
|
557
451
|
);
|
|
558
452
|
if (!decision.dispatch) {
|
|
559
|
-
// Best-effort by contract — a failed append never fails the persist.
|
|
560
453
|
await appendCriticSkip(
|
|
561
454
|
{
|
|
562
455
|
critic: decision.critic,
|
|
563
456
|
reasons: decision.reasons,
|
|
564
457
|
cli: 'plan-persist',
|
|
565
|
-
epicId: requestedEpicId,
|
|
566
458
|
},
|
|
567
459
|
config,
|
|
568
460
|
);
|
|
569
461
|
}
|
|
570
462
|
}
|
|
571
463
|
|
|
572
|
-
//
|
|
573
|
-
const {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
provider,
|
|
464
|
+
// Split policy + inline Spec fold (over-budget Specs fail closed — no docs/).
|
|
465
|
+
const { stories } = assemblePlanStories(rawStories, {
|
|
466
|
+
sharedSpec: techSpecContent,
|
|
467
|
+
planAcceptance: planAcceptance ?? undefined,
|
|
468
|
+
sourceTicketIds,
|
|
578
469
|
});
|
|
579
470
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (mode === 'amend') {
|
|
586
|
-
const priorState = loadStateFn(epicId);
|
|
587
|
-
amendTargets = await resolveAmendTargets({
|
|
588
|
-
partition: amendPartition,
|
|
589
|
-
stateMapping: priorState.mapping,
|
|
590
|
-
provider,
|
|
591
|
-
});
|
|
592
|
-
enforceAmendCloseGate({
|
|
593
|
-
epicId,
|
|
594
|
-
targets: amendTargets,
|
|
595
|
-
adds: amendPartition.add,
|
|
596
|
-
explicitDelete,
|
|
597
|
-
});
|
|
598
|
-
Logger.info(
|
|
599
|
-
renderAmendPlanDiff({
|
|
600
|
-
epicId,
|
|
601
|
-
targets: amendTargets,
|
|
602
|
-
adds: amendPartition.add,
|
|
603
|
-
}),
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// ---- Step 6: Epic lease. Every path after a successful acquire runs
|
|
608
|
-
// through the finally below, so the lease is released on success, on a
|
|
609
|
-
// gate failure, and on a throw alike. ----
|
|
610
|
-
await acquireEpicPlanLease({ provider, epicId, config, steal });
|
|
611
|
-
|
|
612
|
-
try {
|
|
613
|
-
// Refuse a duplicate story tree unless this is a deliberate re-persist
|
|
614
|
-
// (`--force` closes + recreates via the reconciler's close ops;
|
|
615
|
-
// `--resume` continues a partial persist). Amend is exempt by
|
|
616
|
-
// definition — its whole purpose is mutating the existing open tree.
|
|
617
|
-
if (mode !== 'amend') {
|
|
618
|
-
await assertNoOpenPlanChildren({
|
|
619
|
-
provider,
|
|
620
|
-
epicId,
|
|
621
|
-
force: force || resume,
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
await initializePlanState({ provider, epicId });
|
|
626
|
-
|
|
627
|
-
// ---- Step 7: managed sections + risk comment + freshness advisory. ----
|
|
628
|
-
// BDD-runner probe (Story #4145): best-effort; a probe failure degrades
|
|
629
|
-
// to "runner present" and never blocks the persist.
|
|
630
|
-
let bddRunner = null;
|
|
631
|
-
try {
|
|
632
|
-
bddRunner = await bddProbeFn({ cwd: PROJECT_ROOT });
|
|
633
|
-
} catch (err) {
|
|
634
|
-
Logger.warn(
|
|
635
|
-
`[plan-persist] BDD runner probe skipped (${err.message}); ` +
|
|
636
|
-
'acceptance disposition derived from risk axes only.',
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
const planningRisk = deriveRiskEnvelope(riskVerdict, { bddRunner });
|
|
640
|
-
if (planningRisk.acceptanceWaivedReason) {
|
|
641
|
-
Logger.info(
|
|
642
|
-
`[plan-persist] Acceptance disposition forced to not-applicable for ` +
|
|
643
|
-
`Epic #${epicId}: ${planningRisk.acceptanceWaivedReason}`,
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Amend always overwrites the managed sections — the amended Tech Spec
|
|
648
|
-
// IS the delta's spec half (planEpic would otherwise short-circuit
|
|
649
|
-
// `already-planned` on the pre-amend sections).
|
|
650
|
-
const planResult = await planEpic(
|
|
651
|
-
epicId,
|
|
652
|
-
provider,
|
|
653
|
-
{ techSpecContent, acceptanceSpecContent },
|
|
654
|
-
settings,
|
|
655
|
-
{ force: force || mode === 'amend', planningRisk },
|
|
656
|
-
);
|
|
657
|
-
|
|
658
|
-
const reviewRouting = resolveReviewRouting({ planningRisk, forceReview });
|
|
659
|
-
Logger.info(`[plan-persist] Review routing: ${reviewRouting.decision}.`);
|
|
660
|
-
|
|
661
|
-
await upsertStructuredComment(
|
|
662
|
-
provider,
|
|
663
|
-
epicId,
|
|
664
|
-
'risk-verdict',
|
|
665
|
-
buildRiskVerdictCommentBody({ epicId, riskVerdict, planningRisk }),
|
|
666
|
-
);
|
|
667
|
-
|
|
668
|
-
const baseBranchRef = settings?.baseBranch ?? 'main';
|
|
669
|
-
const tempRoot = path.resolve(
|
|
670
|
-
PROJECT_ROOT,
|
|
671
|
-
settings?.paths?.tempRoot ?? 'temp',
|
|
672
|
-
);
|
|
673
|
-
const freshness = await runSpecFreshnessCheck({
|
|
674
|
-
epicId,
|
|
675
|
-
techSpecContent,
|
|
676
|
-
baseBranchRef,
|
|
677
|
-
tempRoot,
|
|
678
|
-
provider,
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
// Spec-half checkpoint (v2). A crash after this point resumes with the
|
|
682
|
-
// sections already folded (planEpic short-circuits `already-planned`).
|
|
683
|
-
await writeCheckpointV2(provider, epicId, {
|
|
684
|
-
planningRisk,
|
|
685
|
-
riskVerdict,
|
|
686
|
-
reviewRouting: {
|
|
687
|
-
decision: reviewRouting.decision,
|
|
688
|
-
requiresStop: reviewRouting.requiresStop,
|
|
689
|
-
forceReviewApplied: reviewRouting.forceReviewApplied,
|
|
690
|
-
},
|
|
691
|
-
spec: {
|
|
692
|
-
techSpecPersisted:
|
|
693
|
-
planResult?.techSpecPersisted === true ||
|
|
694
|
-
planResult?.reason === 'already-planned',
|
|
695
|
-
acceptanceTable: planResult?.acceptanceTable ?? 'none',
|
|
696
|
-
completedAt: new Date().toISOString(),
|
|
697
|
-
},
|
|
698
|
-
persist: {
|
|
699
|
-
mode,
|
|
700
|
-
cli: 'plan-persist',
|
|
701
|
-
startedAt: new Date().toISOString(),
|
|
702
|
-
completedAt: null,
|
|
703
|
-
},
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
// ---- Step 8: mode-split mutation. ----
|
|
707
|
-
let reconcile = null;
|
|
708
|
-
let specFilePath = null;
|
|
709
|
-
let ticketCount = 0;
|
|
710
|
-
let single = null;
|
|
711
|
-
let amendSummary = null;
|
|
712
|
-
|
|
713
|
-
if (mode === 'fan-out') {
|
|
714
|
-
// Story creation via the structural reconciler.
|
|
715
|
-
Logger.info(
|
|
716
|
-
`[plan-persist] Rendering spec for Epic #${epicId} (${validated.length} tickets)...`,
|
|
717
|
-
);
|
|
718
|
-
const spec = renderSpecFn(validated, {
|
|
719
|
-
epic: buildEpicSpecInput(epic, epicId),
|
|
720
|
-
});
|
|
721
|
-
specFilePath = writeSpecFn(epicId, spec, { epicsDir: undefined });
|
|
722
|
-
Logger.info(`[plan-persist] Wrote spec → ${specFilePath}`);
|
|
723
|
-
|
|
724
|
-
// Pre-creation checkpoint: marks creation in flight so a rate-limit
|
|
725
|
-
// crash mid-creation leaves a checkpoint pointing at the spec + the
|
|
726
|
-
// reconciler's per-slug state file (the resume ledger). `--resume`
|
|
727
|
-
// re-runs the reconciler, which creates only the missing slugs.
|
|
728
|
-
await writeCheckpointV2(provider, epicId, {
|
|
729
|
-
decompose: { ticketCount: null, completedAt: null },
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
Logger.info(
|
|
733
|
-
`[plan-persist] Spawning epic-reconcile.js --apply --yes for Epic #${epicId}...`,
|
|
734
|
-
);
|
|
735
|
-
reconcile = spawnReconcilerApply({
|
|
736
|
-
spawnSync,
|
|
737
|
-
reconcileCli,
|
|
738
|
-
epicId,
|
|
739
|
-
cwd,
|
|
740
|
-
explicitDelete: force,
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
await reconcileSubIssueLinks(epicId, provider);
|
|
744
|
-
|
|
745
|
-
const postReconcileState = loadStateFn(epicId);
|
|
746
|
-
await setBlockedByDependencies(
|
|
747
|
-
epicId,
|
|
748
|
-
provider,
|
|
749
|
-
spec,
|
|
750
|
-
postReconcileState.mapping,
|
|
751
|
-
);
|
|
752
|
-
|
|
753
|
-
ticketCount = tickets.length;
|
|
754
|
-
// Post-creation checkpoint (the former recordCheckpoint half).
|
|
755
|
-
await writeCheckpointV2(provider, epicId, {
|
|
756
|
-
decompose: {
|
|
757
|
-
ticketCount,
|
|
758
|
-
shape: 'fan-out',
|
|
759
|
-
completedAt: new Date().toISOString(),
|
|
760
|
-
},
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
// A force re-persist over a former single-delivery plan flips the
|
|
764
|
-
// routing shape — drop the stale marker so #4475's reader never sees
|
|
765
|
-
// a fan-out tree labelled single.
|
|
766
|
-
await removeSingleDeliveryMarker(provider, epicId, epic);
|
|
767
|
-
} else if (mode === 'single') {
|
|
768
|
-
// Single-delivery: NO story tree. The delivery::single routing
|
|
769
|
-
// marker (inert until #4475's deliver-side reader) plus the Delivery
|
|
770
|
-
// Slicing table of the persisted Tech Spec are the plan.
|
|
771
|
-
single = {
|
|
772
|
-
deliveryShape: 'single',
|
|
773
|
-
sliceCount: countDeliverySlices(techSpecContent),
|
|
774
|
-
routingReasons: riskVerdict.deliveryShapeRationale
|
|
775
|
-
? [riskVerdict.deliveryShapeRationale]
|
|
776
|
-
: [],
|
|
777
|
-
};
|
|
778
|
-
Logger.info(
|
|
779
|
-
`[plan-persist] Single-delivery mode: applying ${DELIVERY_LABELS.SINGLE} ` +
|
|
780
|
-
`to Epic #${epicId} (no story tree).`,
|
|
781
|
-
);
|
|
782
|
-
await provider.updateTicket(epicId, {
|
|
783
|
-
labels: { add: [DELIVERY_LABELS.SINGLE], remove: [] },
|
|
784
|
-
});
|
|
785
|
-
// Explicit zero-ticket checkpoint so delivery-time consumers read a
|
|
786
|
-
// deliberate single-shape plan, never an unplanned absence.
|
|
787
|
-
await writeCheckpointV2(provider, epicId, {
|
|
788
|
-
decompose: {
|
|
789
|
-
ticketCount: 0,
|
|
790
|
-
shape: 'single',
|
|
791
|
-
completedAt: new Date().toISOString(),
|
|
792
|
-
},
|
|
793
|
-
});
|
|
794
|
-
} else {
|
|
795
|
-
// Amend delta: close-and-recreate is scoped to modify/close slugs
|
|
796
|
-
// only; keeps are untouched by construction (no code path receives
|
|
797
|
-
// them); adds are created fresh. The state ledger and blocked-by
|
|
798
|
-
// edges are rebuilt over the merged set.
|
|
799
|
-
const spec = renderSpecFn(validated, {
|
|
800
|
-
epic: buildEpicSpecInput(epic, epicId),
|
|
801
|
-
});
|
|
802
|
-
specFilePath = writeSpecFn(epicId, spec, { epicsDir: undefined });
|
|
803
|
-
Logger.info(`[plan-persist] Wrote amended spec → ${specFilePath}`);
|
|
804
|
-
|
|
805
|
-
await writeCheckpointV2(provider, epicId, {
|
|
806
|
-
decompose: { ticketCount: null, completedAt: null },
|
|
807
|
-
});
|
|
471
|
+
const { created } = await createStoryIssues({
|
|
472
|
+
provider,
|
|
473
|
+
stories,
|
|
474
|
+
opts: { dryRun },
|
|
475
|
+
});
|
|
808
476
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
477
|
+
const primary = created[0];
|
|
478
|
+
const waveTable = buildWaveTable(
|
|
479
|
+
stories.map((s) => ({
|
|
480
|
+
slug: s.slug,
|
|
481
|
+
title: s.title,
|
|
482
|
+
depends_on: s.depends_on,
|
|
483
|
+
})),
|
|
484
|
+
);
|
|
816
485
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
486
|
+
// Story #4541: `readPlanMetrics` is declared `(epicId, config)` but was
|
|
487
|
+
// called with `config` first, so the ledger path resolver received the
|
|
488
|
+
// config object as an `epicId` and threw its guard on every single run —
|
|
489
|
+
// a throw this try/catch then swallowed into a silently absent summary.
|
|
490
|
+
// v2 persist is always Epic-less, hence the explicit `null`. The `since`
|
|
491
|
+
// filter keeps the counts about this invocation, and the in-flight record
|
|
492
|
+
// is folded in because the wrapper has not written it yet — see
|
|
493
|
+
// `renderRunScopedPlanMetricsLine`.
|
|
494
|
+
const planMetricsLine = await renderRunScopedPlanMetricsLine({
|
|
495
|
+
config,
|
|
496
|
+
since: runStartedAt,
|
|
497
|
+
startedAt: runStartedAt,
|
|
498
|
+
mode: dryRun ? 'dry-run' : 'persist',
|
|
499
|
+
});
|
|
828
500
|
|
|
829
|
-
|
|
830
|
-
|
|
501
|
+
const summaryBody = buildPlanSummaryCommentBody({
|
|
502
|
+
epicId: primary.id,
|
|
503
|
+
ticketCount: created.length,
|
|
504
|
+
forceReview,
|
|
505
|
+
freshness,
|
|
506
|
+
healthcheck: { skipped: true },
|
|
507
|
+
waveTable,
|
|
508
|
+
mode: 'stories',
|
|
509
|
+
planMetricsLine,
|
|
510
|
+
stories: created,
|
|
511
|
+
});
|
|
831
512
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
created: applied.created,
|
|
837
|
-
keptCount: amendTargets.keep.length,
|
|
838
|
-
};
|
|
839
|
-
await writeCheckpointV2(provider, epicId, {
|
|
840
|
-
decompose: {
|
|
841
|
-
ticketCount,
|
|
842
|
-
shape: 'fan-out',
|
|
513
|
+
if (!dryRun) {
|
|
514
|
+
for (const story of created) {
|
|
515
|
+
await writeCheckpointV2(provider, story.id, {
|
|
516
|
+
persist: {
|
|
843
517
|
completedAt: new Date().toISOString(),
|
|
518
|
+
storyCount: created.length,
|
|
519
|
+
primaryStoryId: primary.id,
|
|
520
|
+
stories: created.map((createdStory) => ({
|
|
521
|
+
slug: createdStory.slug,
|
|
522
|
+
id: createdStory.id,
|
|
523
|
+
})),
|
|
844
524
|
},
|
|
845
525
|
});
|
|
846
|
-
|
|
847
|
-
// An amended plan is fan-out-shaped; drop a stale single marker.
|
|
848
|
-
await removeSingleDeliveryMarker(provider, epicId, epic);
|
|
849
526
|
}
|
|
527
|
+
await upsertStructuredComment(
|
|
528
|
+
provider,
|
|
529
|
+
primary.id,
|
|
530
|
+
PLAN_SUMMARY_COMMENT_TYPE,
|
|
531
|
+
summaryBody,
|
|
532
|
+
);
|
|
850
533
|
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
epic,
|
|
857
|
-
runHealthcheckFn,
|
|
858
|
-
tag: 'plan-persist',
|
|
859
|
-
});
|
|
534
|
+
// Terminal step: every checkpoint above is now on every Story, so
|
|
535
|
+
// `agent::ready` can honestly mean "fully persisted" (Story #4541).
|
|
536
|
+
// Anything that picks a Story up from here reads a real checkpoint.
|
|
537
|
+
await markStoriesReady({ provider, created });
|
|
538
|
+
}
|
|
860
539
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
540
|
+
const supersede = await runSupersedePhase({
|
|
541
|
+
provider,
|
|
542
|
+
stories,
|
|
543
|
+
created,
|
|
544
|
+
sourceTicketIds,
|
|
545
|
+
dryRun,
|
|
546
|
+
closeSuperseded,
|
|
547
|
+
});
|
|
548
|
+
// Record which channel the ids came from so a run that superseded nothing
|
|
549
|
+
// says *why* (`none` = neither the envelope nor --source-tickets carried
|
|
550
|
+
// any) rather than reading as a clean no-op — Story #4554.
|
|
551
|
+
supersede.sourceTicketOrigin = sourceTicketOrigin;
|
|
868
552
|
|
|
869
|
-
|
|
870
|
-
// the dry-run wave table as closing text (single mode records the
|
|
871
|
-
// { deliveryShape, sliceCount, routingReasons } routing record
|
|
872
|
-
// instead). ----
|
|
873
|
-
const waveTable = mode === 'single' ? [] : buildWaveTable(validated);
|
|
874
|
-
const checkpoint = await writeCheckpointV2(provider, epicId, {
|
|
875
|
-
persist: { completedAt: new Date().toISOString() },
|
|
876
|
-
});
|
|
877
|
-
// G2 measurement receipt (Epic #4474 PR7): roll the plan-metrics ledger
|
|
878
|
-
// into the summary comment so turns-per-plan / per-mode counts / critic
|
|
879
|
-
// skips are readable off the Epic. Best-effort — a missing ledger
|
|
880
|
-
// yields no line. The in-flight persist invocation itself is stamped by
|
|
881
|
-
// the CLI wrapper *after* this function returns, so it appears in the
|
|
882
|
-
// stdout JSON (and any later re-persist), not in this comment.
|
|
883
|
-
let planMetricsLine = null;
|
|
553
|
+
if (!skipCleanup && planDir) {
|
|
884
554
|
try {
|
|
885
|
-
|
|
886
|
-
await readPlanMetrics(epicId, config),
|
|
887
|
-
);
|
|
888
|
-
if (metricsSummary) {
|
|
889
|
-
planMetricsLine = renderPlanMetricsSummaryLine(metricsSummary);
|
|
890
|
-
}
|
|
555
|
+
await rm(planDir, { recursive: true, force: true });
|
|
891
556
|
} catch (err) {
|
|
892
|
-
Logger.warn(
|
|
893
|
-
`[plan-persist] plan-metrics summary line skipped: ${err.message}`,
|
|
894
|
-
);
|
|
557
|
+
Logger.warn(`[plan-persist] temp cleanup skipped: ${err.message}`);
|
|
895
558
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
buildPlanSummaryCommentBody({
|
|
901
|
-
epicId,
|
|
902
|
-
ticketCount,
|
|
903
|
-
planningRisk,
|
|
904
|
-
reviewRouting,
|
|
905
|
-
freshness,
|
|
906
|
-
healthcheck,
|
|
907
|
-
waveTable,
|
|
908
|
-
mode,
|
|
909
|
-
planMetricsLine,
|
|
910
|
-
single,
|
|
911
|
-
amend: amendSummary,
|
|
912
|
-
}),
|
|
913
|
-
);
|
|
559
|
+
}
|
|
560
|
+
// Terminal-success cleanup only ever removes *this* run's planDir, so
|
|
561
|
+
// abandoned ones accumulated forever. Sweep them (Story #4541).
|
|
562
|
+
await reapStalePlanDirs({ config, keepDir: skipCleanup ? planDir : null });
|
|
914
563
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const cleanup = skipCleanup
|
|
918
|
-
? { deleted: [], missing: [], failed: [], skipped: true }
|
|
919
|
-
: await cleanupPhaseTempFiles({ phase: 'persist', epicId });
|
|
564
|
+
const adopted = created.filter((story) => story.adopted);
|
|
565
|
+
if (adopted.length > 0) {
|
|
920
566
|
Logger.info(
|
|
921
|
-
`[plan-persist]
|
|
922
|
-
|
|
567
|
+
`[plan-persist] resumed ${adopted.length} of ${created.length} Story(ies) ` +
|
|
568
|
+
`from a previous persist: ${adopted.map((s2) => `#${s2.id}`).join(', ')}.`,
|
|
923
569
|
);
|
|
924
|
-
if (cleanup.deleted.length > 0) {
|
|
925
|
-
Logger.info(
|
|
926
|
-
`[plan-persist] 🧹 Cleaned up ${cleanup.deleted.length} temp file(s).`,
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
return {
|
|
931
|
-
epicId,
|
|
932
|
-
epicCreated: created,
|
|
933
|
-
mode,
|
|
934
|
-
ticketCount,
|
|
935
|
-
checkpoint,
|
|
936
|
-
planningRisk,
|
|
937
|
-
reviewRouting,
|
|
938
|
-
freshness,
|
|
939
|
-
healthcheck,
|
|
940
|
-
reachability,
|
|
941
|
-
critics,
|
|
942
|
-
reconcile,
|
|
943
|
-
specPath: specFilePath,
|
|
944
|
-
waveTable,
|
|
945
|
-
single,
|
|
946
|
-
amend: amendSummary,
|
|
947
|
-
cleanup,
|
|
948
|
-
labelTransition: 'ready',
|
|
949
|
-
};
|
|
950
|
-
} finally {
|
|
951
|
-
// Lease release on EVERY exit path (success, gate failure, throw).
|
|
952
|
-
// Best-effort by contract — releaseEpicPlanLease never throws.
|
|
953
|
-
await releaseEpicPlanLease({ provider, epicId, config });
|
|
954
570
|
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
/**
|
|
958
|
-
* Drop a stale `delivery::single` marker when a fan-out-shaped persist
|
|
959
|
-
* (full re-persist or amend) lands over a formerly single-delivery plan.
|
|
960
|
-
* No-op — and no API call — when the fetched Epic never carried it.
|
|
961
|
-
*
|
|
962
|
-
* @param {object} provider
|
|
963
|
-
* @param {number} epicId
|
|
964
|
-
* @param {{ labels?: string[] }} epic the Epic as fetched at step 5
|
|
965
|
-
*/
|
|
966
|
-
async function removeSingleDeliveryMarker(provider, epicId, epic) {
|
|
967
|
-
if (!epic?.labels?.includes(DELIVERY_LABELS.SINGLE)) return;
|
|
968
571
|
Logger.info(
|
|
969
|
-
`[plan-persist]
|
|
970
|
-
|
|
572
|
+
`[plan-persist] Persisted ${created.length} Story(ies)` +
|
|
573
|
+
`; primary #${primary.id} is agent::ready.`,
|
|
971
574
|
);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
/**
|
|
978
|
-
* Render the `risk-verdict` structured-comment body. Lifted from
|
|
979
|
-
* `epic-plan-spec/phases/run-spec-phase.js` so the collapsed surface posts
|
|
980
|
-
* the byte-identical audit-trail comment (axis table + fenced-JSON record)
|
|
981
|
-
* downstream tooling parses.
|
|
982
|
-
*
|
|
983
|
-
* @param {{ epicId: number, riskVerdict: import('../planning-risk.js').RiskVerdict, planningRisk: import('../planning-risk.js').PlanningRiskEnvelope }} input
|
|
984
|
-
* @returns {string}
|
|
985
|
-
*/
|
|
986
|
-
function buildRiskVerdictCommentBody({ epicId, riskVerdict, planningRisk }) {
|
|
987
|
-
const axisRows = planningRisk.axes.map(
|
|
988
|
-
(entry) => `| ${entry.axis} | ${entry.level} | ${entry.rationale} |`,
|
|
575
|
+
Logger.info(
|
|
576
|
+
`[plan-persist] Deliver with: /deliver ${created.map((s2) => s2.id).join(' ')}`,
|
|
989
577
|
);
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
stories: created,
|
|
581
|
+
primaryStoryId: primary.id,
|
|
582
|
+
forceReview,
|
|
583
|
+
critics,
|
|
584
|
+
reachability,
|
|
585
|
+
freshness,
|
|
586
|
+
waveTable,
|
|
587
|
+
supersede,
|
|
999
588
|
};
|
|
1000
|
-
const waiverNote = planningRisk.acceptanceWaivedReason
|
|
1001
|
-
? ['', `> ⚠️ **Acceptance waived** — ${planningRisk.acceptanceWaivedReason}`]
|
|
1002
|
-
: [];
|
|
1003
|
-
return [
|
|
1004
|
-
`### 🧭 Planning Risk Verdict — ${planningRisk.overallLevel} · ${planningRisk.gateDecision}`,
|
|
1005
|
-
'',
|
|
1006
|
-
riskVerdict.summary,
|
|
1007
|
-
'',
|
|
1008
|
-
...axisTable,
|
|
1009
|
-
...waiverNote,
|
|
1010
|
-
'',
|
|
1011
|
-
'```json',
|
|
1012
|
-
JSON.stringify(record, null, 2),
|
|
1013
|
-
'```',
|
|
1014
|
-
].join('\n');
|
|
1015
589
|
}
|