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
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* story-ops.js — flat Story creation for v2 plan-persist (Stage 3).
|
|
3
|
+
*
|
|
4
|
+
* Under the Story collapse (`docs/roadmap.md` § Stage 3), `/plan` persists
|
|
5
|
+
* zero-or-more Story issues directly — no Epic parent, no reconciler tree,
|
|
6
|
+
* no `deliveryShape` mode matrix. Default is **one Story**; N>1 is gated by
|
|
7
|
+
* the Stage-1 split-policy validator (`assertAcceptancePartition`).
|
|
8
|
+
*
|
|
9
|
+
* Each Story body is the single executable document: Tech Spec stays inline
|
|
10
|
+
* under `## Spec`. Over-budget Specs fail closed (split / tighten) — never
|
|
11
|
+
* spill to `docs/`. Top-level `acceptance[]` / `verify[]` are the machine
|
|
12
|
+
* contract and are synced into the body so the GitHub issue stays complete
|
|
13
|
+
* without requiring the LLM to dual-author the same lists.
|
|
14
|
+
*
|
|
15
|
+
* @module lib/orchestration/plan-persist/story-ops
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { applyBlockedByDependencies } from '../../../providers/github/blocked-by-add.js';
|
|
20
|
+
import { Logger } from '../../Logger.js';
|
|
21
|
+
import { AGENT_LABELS, TYPE_LABELS } from '../../label-constants.js';
|
|
22
|
+
import {
|
|
23
|
+
parse as parseStoryBody,
|
|
24
|
+
serialize as serializeStoryBody,
|
|
25
|
+
} from '../../story-body/story-body.js';
|
|
26
|
+
import { assertSpecWithinBudget } from '../spec-spill.js';
|
|
27
|
+
import { assertAcceptancePartition } from '../split-policy-validator.js';
|
|
28
|
+
import {
|
|
29
|
+
assertSupersedePartition,
|
|
30
|
+
normalizeSupersedes,
|
|
31
|
+
} from './supersede-ops.js';
|
|
32
|
+
|
|
33
|
+
// Story #4540 removed PLAN_RUN_LABEL_PREFIX / normalizePlanRunId /
|
|
34
|
+
// planRunLabel from here. They minted an opaque random-hex label per N>1
|
|
35
|
+
// plan that nothing ever deleted, and their only external consumer was the
|
|
36
|
+
// (now deleted) `--run` resolver. Sibling order survives in the
|
|
37
|
+
// `blocked by #N` body footers this module already writes.
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Marker prefix for the per-Story plan fingerprint appended to every
|
|
41
|
+
* created body. `createStoryIssues` greps open `type::story` issues for
|
|
42
|
+
* `<!-- plan-story: <fingerprint> -->` to decide whether a Story already
|
|
43
|
+
* exists — the idempotency half of the resumable-create contract.
|
|
44
|
+
*/
|
|
45
|
+
const PLAN_FINGERPRINT_MARKER_PREFIX = 'plan-story:';
|
|
46
|
+
|
|
47
|
+
/** Length of the hex fingerprint digest. Collision-free at plan scale. */
|
|
48
|
+
const PLAN_FINGERPRINT_LENGTH = 16;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compute the deterministic identity of one authored Story within a plan.
|
|
52
|
+
*
|
|
53
|
+
* Derived from `slug` + `title` + the **assembled** body, so the fingerprint
|
|
54
|
+
* identifies the authored *content*, not merely a name. Because the marker is
|
|
55
|
+
* what `createStoryIssues` adopts on, that is the whole safety property of the
|
|
56
|
+
* resume path: a fingerprint hit means the open Story on the tracker is
|
|
57
|
+
* byte-identical to what this run would author, so adopting it instead of
|
|
58
|
+
* creating it is a genuine no-op.
|
|
59
|
+
*
|
|
60
|
+
* **Why the body is in scope.** It used to be excluded, on the rationale that
|
|
61
|
+
* creation rewrites the body to substitute real issue ids into `depends_on`
|
|
62
|
+
* footers, so a body-derived fingerprint would differ between an aborted run
|
|
63
|
+
* and its resume. That conflated two different bodies. The fingerprint is
|
|
64
|
+
* computed in `assembleOnePlanStory` over the *assembled* body — whose
|
|
65
|
+
* `depends_on` are still sibling **slugs**, and which is a pure function of
|
|
66
|
+
* `stories.json` plus the shared Spec. The id substitution happens later, and
|
|
67
|
+
* only inside `renderStoryBodyForCreate`, which produces the *posted* body.
|
|
68
|
+
* Nothing ever re-derives a fingerprint from a posted body; the lookup reads
|
|
69
|
+
* the marker that body carries. The assembled body is therefore stable across
|
|
70
|
+
* a run and its resume, and folding it in defeats no lookup.
|
|
71
|
+
*
|
|
72
|
+
* Excluding it cost two silent failures, both closed by this:
|
|
73
|
+
*
|
|
74
|
+
* 1. A later, unrelated plan that reused a slug **and** title adopted the
|
|
75
|
+
* stale open Story — never rewriting its body or Spec, and landing this
|
|
76
|
+
* run's checkpoints, ready-flip, and supersede comments on the wrong
|
|
77
|
+
* issue.
|
|
78
|
+
* 2. A legitimate resume after the operator edited `stories.json` adopted
|
|
79
|
+
* the pre-edit Story and kept its stale body, discarding the edit.
|
|
80
|
+
*
|
|
81
|
+
* With the body in scope both cases simply miss the lookup, and a correct new
|
|
82
|
+
* Story is created; only a genuinely identical Story is ever adopted.
|
|
83
|
+
*
|
|
84
|
+
* The fields are joined on NUL separators, written as the `\u0000` escape and
|
|
85
|
+
* never as a raw byte — a literal NUL would make git classify this file as
|
|
86
|
+
* binary and silently drop its diffs. NUL cannot occur in a slug, a title, or
|
|
87
|
+
* a serialized body, so the join is unambiguous: `{slug:'a-b', title:'c'}` and
|
|
88
|
+
* `{slug:'a', title:'b-c'}` cannot collide the way a hyphen or space separator
|
|
89
|
+
* would let them.
|
|
90
|
+
*
|
|
91
|
+
* @param {{ slug: string, title: string, body?: string }} story
|
|
92
|
+
* @returns {string} Hex digest.
|
|
93
|
+
*/
|
|
94
|
+
export function planStoryFingerprint({ slug, title, body = '' }) {
|
|
95
|
+
return createHash('sha256')
|
|
96
|
+
.update(`${slug}\u0000${title}\u0000${body}`)
|
|
97
|
+
.digest('hex')
|
|
98
|
+
.slice(0, PLAN_FINGERPRINT_LENGTH);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Render the HTML-comment marker carrying a Story's plan fingerprint. It is
|
|
103
|
+
* invisible in GitHub's rendered issue body and survives edits to every
|
|
104
|
+
* other section.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} fingerprint
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
function planFingerprintMarker(fingerprint) {
|
|
110
|
+
return `<!-- ${PLAN_FINGERPRINT_MARKER_PREFIX} ${fingerprint} -->`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Labels the authoring pass is never allowed to set. The `agent::*` axis is
|
|
115
|
+
* the runtime's lifecycle state (persist owns the terminal `agent::ready`
|
|
116
|
+
* flip itself), `type::*` is fixed to `type::story` by the v2 hierarchy, and
|
|
117
|
+
* `persona::*` is a retired axis.
|
|
118
|
+
*/
|
|
119
|
+
const FORBIDDEN_LABEL_PREFIXES = Object.freeze([
|
|
120
|
+
'agent::',
|
|
121
|
+
'type::',
|
|
122
|
+
'persona::',
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
/** GitHub's own label-name ceiling. */
|
|
126
|
+
const MAX_LABEL_LENGTH = 50;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sanitize the author-supplied `labels[]` on a plan Story (Story #4541).
|
|
130
|
+
*
|
|
131
|
+
* The schema descriptor and the authoring prompt both ask for `labels[]`,
|
|
132
|
+
* but persist never read the field — it hard-coded its own list, so every
|
|
133
|
+
* authored label was silently discarded. Rather than keep asking for input
|
|
134
|
+
* that goes nowhere, apply it: drop the axes the runtime owns, drop
|
|
135
|
+
* malformed entries, dedupe, and always guarantee `type::story`.
|
|
136
|
+
*
|
|
137
|
+
* @param {unknown} rawLabels
|
|
138
|
+
* @param {string} slug For the dropped-label warning.
|
|
139
|
+
* @returns {string[]} Sanitized labels, always including `type::story`.
|
|
140
|
+
*/
|
|
141
|
+
export function sanitizeAuthoredLabels(rawLabels, slug) {
|
|
142
|
+
const kept = new Set([TYPE_LABELS.STORY]);
|
|
143
|
+
const dropped = [];
|
|
144
|
+
for (const raw of Array.isArray(rawLabels) ? rawLabels : []) {
|
|
145
|
+
const label = typeof raw === 'string' ? raw.trim() : '';
|
|
146
|
+
if (label === '' || label.length > MAX_LABEL_LENGTH) {
|
|
147
|
+
dropped.push(String(raw));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (label === TYPE_LABELS.STORY) continue;
|
|
151
|
+
if (FORBIDDEN_LABEL_PREFIXES.some((p) => label.startsWith(p))) {
|
|
152
|
+
dropped.push(label);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
kept.add(label);
|
|
156
|
+
}
|
|
157
|
+
if (dropped.length > 0) {
|
|
158
|
+
Logger.warn(
|
|
159
|
+
`[plan-persist] Story "${slug}": dropped ${dropped.length} authored ` +
|
|
160
|
+
`label(s) the runtime owns or cannot apply: ${dropped.join(', ')}.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return [...kept];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function bodyObjectFromTicket(ticket) {
|
|
167
|
+
if (typeof ticket.body === 'string') {
|
|
168
|
+
return parseStoryBody(ticket.body).body;
|
|
169
|
+
}
|
|
170
|
+
if (ticket.body && typeof ticket.body === 'object') {
|
|
171
|
+
return parseStoryBody(ticket.body).body;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Allow top-level structured fields (goal/changes/…) without a `body` key.
|
|
175
|
+
return parseStoryBody({
|
|
176
|
+
goal: ticket.goal ?? '',
|
|
177
|
+
slicing: ticket.slicing ?? '',
|
|
178
|
+
spec: ticket.spec ?? '',
|
|
179
|
+
changes: ticket.changes ?? [],
|
|
180
|
+
acceptance: ticket.acceptance ?? [],
|
|
181
|
+
verify: ticket.verify ?? [],
|
|
182
|
+
references: ticket.references ?? [],
|
|
183
|
+
non_goals: ticket.non_goals ?? [],
|
|
184
|
+
wide: ticket.wide ?? null,
|
|
185
|
+
reason_to_exist: ticket.reason_to_exist ?? null,
|
|
186
|
+
depends_on: ticket.depends_on ?? [],
|
|
187
|
+
estimated_test_files: ticket.estimated_test_files ?? null,
|
|
188
|
+
}).body;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeDependsOn(ticket, bodyObject) {
|
|
192
|
+
if (Array.isArray(ticket.depends_on)) {
|
|
193
|
+
return ticket.depends_on.filter((d) => typeof d === 'string');
|
|
194
|
+
}
|
|
195
|
+
return Array.isArray(bodyObject.depends_on) ? bodyObject.depends_on : [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function arraysEqual(a, b) {
|
|
199
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Top-level `acceptance[]` / `verify[]` are the machine contract (validator
|
|
204
|
+
* SSOT). Sync them into the body so the persisted GitHub issue is complete.
|
|
205
|
+
* When the body already lists the same items, keep them; when the body is
|
|
206
|
+
* empty, fill from top-level; when both disagree, fail closed.
|
|
207
|
+
*
|
|
208
|
+
* @param {object} ticket
|
|
209
|
+
* @param {object} bodyObject
|
|
210
|
+
* @param {'acceptance'|'verify'} field
|
|
211
|
+
*/
|
|
212
|
+
function syncContractFieldFromTopLevel(ticket, bodyObject, field) {
|
|
213
|
+
if (!Array.isArray(ticket[field])) return;
|
|
214
|
+
const topLevel = ticket[field].map(String);
|
|
215
|
+
const bodyValue = Array.isArray(bodyObject[field])
|
|
216
|
+
? bodyObject[field].map(String)
|
|
217
|
+
: [];
|
|
218
|
+
if (bodyValue.length > 0 && !arraysEqual(topLevel, bodyValue)) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`[plan-persist] Story "${ticket.slug ?? ticket.title ?? 'unknown'}" has mismatched top-level and body ${field} arrays`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
bodyObject[field] = topLevel;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Normalize a plan Story ticket into `{ slug, title, bodyObject }`.
|
|
228
|
+
* Accepts either a serialized markdown `body` string or a structured body.
|
|
229
|
+
*
|
|
230
|
+
* `supersedes[]` is a top-level-only field (Story #4535) — it is planning
|
|
231
|
+
* bookkeeping for the `--tickets` source issues, not part of the Story's
|
|
232
|
+
* executable body, so it is deliberately not serialized into the markdown.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} ticket
|
|
235
|
+
* @returns {{ slug: string, title: string, bodyObject: object, depends_on: string[], labels: string[], supersedes: Array<{ id: number, note: string|null }> }}
|
|
236
|
+
*/
|
|
237
|
+
export function normalizeStoryTicket(ticket) {
|
|
238
|
+
if (!ticket || typeof ticket !== 'object') {
|
|
239
|
+
throw new Error('[plan-persist] each story ticket must be an object');
|
|
240
|
+
}
|
|
241
|
+
const slug =
|
|
242
|
+
typeof ticket.slug === 'string' && ticket.slug.trim() !== ''
|
|
243
|
+
? ticket.slug.trim()
|
|
244
|
+
: null;
|
|
245
|
+
if (!slug) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
'[plan-persist] each story ticket requires a non-empty slug',
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const title =
|
|
251
|
+
typeof ticket.title === 'string' && ticket.title.trim() !== ''
|
|
252
|
+
? ticket.title.trim()
|
|
253
|
+
: `Story ${slug}`;
|
|
254
|
+
const bodyObject = bodyObjectFromTicket(ticket);
|
|
255
|
+
syncContractFieldFromTopLevel(ticket, bodyObject, 'acceptance');
|
|
256
|
+
syncContractFieldFromTopLevel(ticket, bodyObject, 'verify');
|
|
257
|
+
const depends_on = normalizeDependsOn(ticket, bodyObject);
|
|
258
|
+
const supersedes = normalizeSupersedes(ticket, slug);
|
|
259
|
+
const labels = sanitizeAuthoredLabels(ticket.labels, slug);
|
|
260
|
+
|
|
261
|
+
return { slug, title, bodyObject, depends_on, labels, supersedes };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Fold optional shared Tech Spec prose into a Story body when the Story has
|
|
266
|
+
* no inline Spec. Specs stay inline; over-budget Specs throw.
|
|
267
|
+
*
|
|
268
|
+
* Precedence: per-Story `body.spec` wins; otherwise `sharedSpec` is used
|
|
269
|
+
* (N===1 convenience only — callers must not share one Spec across N>1).
|
|
270
|
+
*
|
|
271
|
+
* @param {object} bodyObject
|
|
272
|
+
* @param {string} slug
|
|
273
|
+
* @param {object} [opts]
|
|
274
|
+
* @param {string|null} [opts.sharedSpec]
|
|
275
|
+
* @returns {{ bodyObject: object }}
|
|
276
|
+
*/
|
|
277
|
+
export function foldSpecIntoStoryBody(bodyObject, slug, opts = {}) {
|
|
278
|
+
const { sharedSpec = null } = opts;
|
|
279
|
+
|
|
280
|
+
const next = {
|
|
281
|
+
...bodyObject,
|
|
282
|
+
references: Array.isArray(bodyObject.references)
|
|
283
|
+
? [...bodyObject.references]
|
|
284
|
+
: [],
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const inline =
|
|
288
|
+
typeof next.spec === 'string' && next.spec.trim() !== ''
|
|
289
|
+
? next.spec.trim()
|
|
290
|
+
: typeof sharedSpec === 'string' && sharedSpec.trim() !== ''
|
|
291
|
+
? sharedSpec.trim()
|
|
292
|
+
: '';
|
|
293
|
+
|
|
294
|
+
if (inline === '') {
|
|
295
|
+
return { bodyObject: next };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { content } = assertSpecWithinBudget({ storyId: slug, spec: inline });
|
|
299
|
+
next.spec = content;
|
|
300
|
+
return { bodyObject: next };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function assembleOnePlanStory(ticket, opts) {
|
|
304
|
+
const { slug, title, bodyObject, depends_on, labels, supersedes } =
|
|
305
|
+
normalizeStoryTicket(ticket);
|
|
306
|
+
const { bodyObject: folded } = foldSpecIntoStoryBody(bodyObject, slug, {
|
|
307
|
+
sharedSpec: opts.sharedSpec ?? null,
|
|
308
|
+
});
|
|
309
|
+
// Body first: the fingerprint is an identity over the *assembled* content,
|
|
310
|
+
// so it cannot be computed until that content exists.
|
|
311
|
+
const body = serializeStoryBody({ ...folded, depends_on });
|
|
312
|
+
const fingerprint = planStoryFingerprint({ slug, title, body });
|
|
313
|
+
return {
|
|
314
|
+
story: {
|
|
315
|
+
slug,
|
|
316
|
+
title,
|
|
317
|
+
body,
|
|
318
|
+
bodyObject: { ...folded, depends_on },
|
|
319
|
+
acceptance: Array.isArray(folded.acceptance) ? folded.acceptance : [],
|
|
320
|
+
depends_on,
|
|
321
|
+
labels,
|
|
322
|
+
fingerprint,
|
|
323
|
+
supersedes,
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Shared techspec.md is an N===1 convenience only — folding one Spec into
|
|
330
|
+
* every sibling duplicates approach prose and breaks Story-as-SSOT.
|
|
331
|
+
*
|
|
332
|
+
* @param {object[]} tickets
|
|
333
|
+
* @param {string|null|undefined} sharedSpec
|
|
334
|
+
*/
|
|
335
|
+
function assertSharedSpecAllowed(tickets, sharedSpec) {
|
|
336
|
+
if (tickets.length <= 1) return;
|
|
337
|
+
if (typeof sharedSpec !== 'string' || sharedSpec.trim() === '') return;
|
|
338
|
+
throw new Error(
|
|
339
|
+
'[plan-persist] a shared techspec.md cannot be folded into N>1 Stories — ' +
|
|
340
|
+
"put each Story's approach in its own ## Spec so every Story stays a " +
|
|
341
|
+
'complete executable document.',
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Assemble markdown bodies for every Story: normalize → fold spec →
|
|
347
|
+
* assertAcceptancePartition → assertSupersedePartition → serialize.
|
|
348
|
+
*
|
|
349
|
+
* Both partition checks run **before** any GitHub write so a mis-authored
|
|
350
|
+
* plan never leaves Stories live against an inconsistent tracker.
|
|
351
|
+
*
|
|
352
|
+
* @param {object[]} tickets
|
|
353
|
+
* @param {object} [opts]
|
|
354
|
+
* @param {string|null} [opts.sharedSpec]
|
|
355
|
+
* @param {string[]} [opts.planAcceptance]
|
|
356
|
+
* @param {number[]} [opts.sourceTicketIds] Ids passed to `/plan --tickets`.
|
|
357
|
+
* @returns {{ stories: Array<{ slug: string, title: string, body: string, acceptance: string[], depends_on: string[], supersedes: Array<{ id: number, note: string|null }> }> }}
|
|
358
|
+
*/
|
|
359
|
+
export function assemblePlanStories(tickets, opts = {}) {
|
|
360
|
+
if (!Array.isArray(tickets) || tickets.length === 0) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
'[plan-persist] stories payload must be a non-empty array — author at least one Story (default-single).',
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
assertSharedSpecAllowed(tickets, opts.sharedSpec);
|
|
367
|
+
|
|
368
|
+
const stories = tickets.map(
|
|
369
|
+
(ticket) => assembleOnePlanStory(ticket, opts).story,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
assertAcceptancePartition(stories, {
|
|
373
|
+
planAcceptance: opts.planAcceptance,
|
|
374
|
+
});
|
|
375
|
+
assertSupersedePartition(stories, opts.sourceTicketIds ?? []);
|
|
376
|
+
|
|
377
|
+
return { stories };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function orderStoriesByDependencies(stories) {
|
|
381
|
+
const list = Array.isArray(stories) ? stories : [];
|
|
382
|
+
const known = new Set(list.map((story) => story.slug));
|
|
383
|
+
for (const story of list) {
|
|
384
|
+
const unknown = story.depends_on.filter((slug) => !known.has(slug));
|
|
385
|
+
if (unknown.length > 0) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`[plan-persist] Story "${story.slug}" depends on unknown sibling(s): ${unknown.join(', ')}`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const ordered = [];
|
|
392
|
+
const scheduled = new Set();
|
|
393
|
+
const pending = [...list];
|
|
394
|
+
while (pending.length > 0) {
|
|
395
|
+
const index = pending.findIndex((story) =>
|
|
396
|
+
story.depends_on.every((slug) => scheduled.has(slug)),
|
|
397
|
+
);
|
|
398
|
+
if (index === -1) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`[plan-persist] dependency cycle prevents Story creation: ${pending.map((story) => story.slug).join(', ')}`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
const [story] = pending.splice(index, 1);
|
|
404
|
+
ordered.push(story);
|
|
405
|
+
scheduled.add(story.slug);
|
|
406
|
+
}
|
|
407
|
+
return ordered;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Index the open `type::story` backlog so a re-run can recognise Stories a
|
|
412
|
+
* previous, partially-failed persist already created.
|
|
413
|
+
*
|
|
414
|
+
* Two indexes come back. `byFingerprint` is the adoption key — an exact match
|
|
415
|
+
* on the authored content (see {@link planStoryFingerprint}). `idsByTitle` is
|
|
416
|
+
* only used to *warn*: it catches the near miss where a Story with this title
|
|
417
|
+
* is already open but its content differs, which is what an abandoned earlier
|
|
418
|
+
* plan or an edited `stories.json` leaves behind. Adoption deliberately does
|
|
419
|
+
* not key on it — a title is not an identity, and adopting on one would let a
|
|
420
|
+
* later unrelated plan overwrite someone else's Story.
|
|
421
|
+
*
|
|
422
|
+
* Best-effort by construction: a provider with no `listIssuesByLabel` (or a
|
|
423
|
+
* listing that errors) yields empty indexes and the create loop proceeds
|
|
424
|
+
* un-deduplicated, exactly as it did before. That degrades resume, not
|
|
425
|
+
* correctness of a first run — so it warns rather than throws.
|
|
426
|
+
*
|
|
427
|
+
* @param {object} provider
|
|
428
|
+
* @returns {Promise<{
|
|
429
|
+
* byFingerprint: Map<string, { id: number, title: string, url?: string }>,
|
|
430
|
+
* idsByTitle: Map<string, number[]>,
|
|
431
|
+
* }>}
|
|
432
|
+
*/
|
|
433
|
+
async function indexExistingStories(provider) {
|
|
434
|
+
const byFingerprint = new Map();
|
|
435
|
+
const idsByTitle = new Map();
|
|
436
|
+
if (typeof provider?.listIssuesByLabel !== 'function') {
|
|
437
|
+
Logger.warn(
|
|
438
|
+
'[plan-persist] provider does not expose listIssuesByLabel — cannot ' +
|
|
439
|
+
'check for Stories a previous persist already created. A re-run after ' +
|
|
440
|
+
'a mid-creation failure may duplicate them.',
|
|
441
|
+
);
|
|
442
|
+
return { byFingerprint, idsByTitle };
|
|
443
|
+
}
|
|
444
|
+
let issues;
|
|
445
|
+
try {
|
|
446
|
+
issues = await provider.listIssuesByLabel({
|
|
447
|
+
state: 'open',
|
|
448
|
+
labels: TYPE_LABELS.STORY,
|
|
449
|
+
});
|
|
450
|
+
} catch (err) {
|
|
451
|
+
Logger.warn(
|
|
452
|
+
`[plan-persist] open-Story lookup failed (${err.message}) — proceeding ` +
|
|
453
|
+
'without resume; a re-run may duplicate Stories.',
|
|
454
|
+
);
|
|
455
|
+
return { byFingerprint, idsByTitle };
|
|
456
|
+
}
|
|
457
|
+
for (const issue of Array.isArray(issues) ? issues : []) {
|
|
458
|
+
const id = Number(issue?.number ?? issue?.id);
|
|
459
|
+
if (!Number.isInteger(id)) continue;
|
|
460
|
+
const title = issue?.title ?? '';
|
|
461
|
+
if (title !== '') {
|
|
462
|
+
idsByTitle.set(title, [...(idsByTitle.get(title) ?? []), id]);
|
|
463
|
+
}
|
|
464
|
+
const body = typeof issue?.body === 'string' ? issue.body : '';
|
|
465
|
+
const match = body.match(
|
|
466
|
+
new RegExp(
|
|
467
|
+
`<!--\\s*${PLAN_FINGERPRINT_MARKER_PREFIX}\\s*([0-9a-f]+)\\s*-->`,
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
if (!match) continue;
|
|
471
|
+
byFingerprint.set(match[1], {
|
|
472
|
+
id,
|
|
473
|
+
title,
|
|
474
|
+
url: issue.html_url ?? issue.url ?? undefined,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return { byFingerprint, idsByTitle };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Warn when a Story with this title is already open but did **not** match the
|
|
482
|
+
* fingerprint — i.e. its authored content differs from what this run is about
|
|
483
|
+
* to create.
|
|
484
|
+
*
|
|
485
|
+
* This is the visible half of the fingerprint tightening. Keying adoption on
|
|
486
|
+
* content means these cases correctly get a fresh Story rather than a silent
|
|
487
|
+
* stale-body adoption, but the divergent Story stays open, and a duplicate the
|
|
488
|
+
* operator never hears about is its own small trap. Naming it converts silent
|
|
489
|
+
* litter into a decision.
|
|
490
|
+
*
|
|
491
|
+
* @param {{ slug: string, title: string }} story
|
|
492
|
+
* @param {Map<string, number[]>} idsByTitle
|
|
493
|
+
*/
|
|
494
|
+
function warnOnDivergentSameTitleStory(story, idsByTitle) {
|
|
495
|
+
const ids = idsByTitle.get(story.title) ?? [];
|
|
496
|
+
if (ids.length === 0) return;
|
|
497
|
+
Logger.warn(
|
|
498
|
+
`[plan-persist] Story "${story.slug}": ${ids.length} open Story(ies) ` +
|
|
499
|
+
`already carry this exact title (${ids.map((id) => `#${id}`).join(', ')}) ` +
|
|
500
|
+
'but none match the authored content, so a new Story is being created ' +
|
|
501
|
+
'rather than silently adopting a stale body. If that is an abandoned ' +
|
|
502
|
+
'plan or a superseded draft, close it.',
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Render the body actually posted for a Story: the assembled markdown with
|
|
508
|
+
* sibling `depends_on` slugs resolved to real issue ids, plus the invisible
|
|
509
|
+
* plan-fingerprint marker that makes the create loop resumable.
|
|
510
|
+
*
|
|
511
|
+
* @param {object} story
|
|
512
|
+
* @param {Map<string, number>} idBySlug
|
|
513
|
+
* @returns {string}
|
|
514
|
+
*/
|
|
515
|
+
function renderStoryBodyForCreate(story, idBySlug) {
|
|
516
|
+
const dependencyRefs = story.depends_on.map(
|
|
517
|
+
(slug) => `#${idBySlug.get(slug)}`,
|
|
518
|
+
);
|
|
519
|
+
const base =
|
|
520
|
+
dependencyRefs.length === 0
|
|
521
|
+
? story.body
|
|
522
|
+
: serializeStoryBody(
|
|
523
|
+
{ ...story.bodyObject, depends_on: dependencyRefs },
|
|
524
|
+
{ includeFooter: true },
|
|
525
|
+
);
|
|
526
|
+
return `${base}\n\n${planFingerprintMarker(story.fingerprint)}`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Mirror the plan's sibling `depends_on` edges into native GitHub `blocked_by`
|
|
531
|
+
* dependency edges (Story #4544).
|
|
532
|
+
*
|
|
533
|
+
* Ordering is authored as slugs and, until now, survived persist only as
|
|
534
|
+
* `blocked by #N` prose in the body footer. That footer stays — it is what
|
|
535
|
+
* `/deliver`'s resolver falls back on — but a native edge is the durable,
|
|
536
|
+
* machine-readable form: visible in the GitHub UI, readable without parsing
|
|
537
|
+
* markdown, and settable by an operator later for cross-run order.
|
|
538
|
+
*
|
|
539
|
+
* **Non-fatal by design, and deliberately asymmetric with the read path.** A
|
|
540
|
+
* missing native edge is cosmetic here: `renderStoryBodyForCreate` has already
|
|
541
|
+
* written the footer, so ordering is not lost when the dependencies API says
|
|
542
|
+
* no. `/deliver`'s *read* of these edges is a real dispatch gate, which is why
|
|
543
|
+
* that side fails loud. Persist reports the failure and completes.
|
|
544
|
+
*
|
|
545
|
+
* Two shape hazards this crossing has to get right, both silent if missed:
|
|
546
|
+
* `applyBlockedByDependencies` indexes `slugToIssueNumber` with property
|
|
547
|
+
* access, so the `Map` the create loop builds must be flattened to a plain
|
|
548
|
+
* object — a `Map` would yield `undefined` for every lookup, skip every edge,
|
|
549
|
+
* and (being non-fatal) report success having written nothing. And it reads
|
|
550
|
+
* `dependsOn`, not the `depends_on` the assembled Story carries.
|
|
551
|
+
*
|
|
552
|
+
* @param {object} args
|
|
553
|
+
* @param {object} args.provider
|
|
554
|
+
* @param {Array<{ slug: string, depends_on: string[] }>} args.stories
|
|
555
|
+
* @param {Map<string, number>} args.idBySlug
|
|
556
|
+
* @returns {Promise<{ edgesAdded: number, edgesSkipped: number, edgesFailed: number, storiesProcessed: number }|null>}
|
|
557
|
+
* `null` when there was nothing to mirror or no interface to mirror through.
|
|
558
|
+
*/
|
|
559
|
+
async function mirrorNativeDependencyEdges({ provider, stories, idBySlug }) {
|
|
560
|
+
const withEdges = stories.filter((story) => story.depends_on.length > 0);
|
|
561
|
+
if (withEdges.length === 0) return null;
|
|
562
|
+
|
|
563
|
+
if (
|
|
564
|
+
typeof provider?.getDependencyWriteContext !== 'function' ||
|
|
565
|
+
typeof provider?.getTicket !== 'function'
|
|
566
|
+
) {
|
|
567
|
+
Logger.warn(
|
|
568
|
+
'[plan-persist] provider exposes no getDependencyWriteContext/getTicket — ' +
|
|
569
|
+
'skipping native blocked_by edges. Ordering survives in the ' +
|
|
570
|
+
'`blocked by #N` body footers.',
|
|
571
|
+
);
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
const { gh, owner, repo } = provider.getDependencyWriteContext();
|
|
577
|
+
const summary = await applyBlockedByDependencies({
|
|
578
|
+
stories: stories.map((story) => ({
|
|
579
|
+
slug: story.slug,
|
|
580
|
+
dependsOn: story.depends_on,
|
|
581
|
+
})),
|
|
582
|
+
slugToIssueNumber: Object.fromEntries(idBySlug),
|
|
583
|
+
getTicket: (issueNumber) => provider.getTicket(issueNumber),
|
|
584
|
+
owner,
|
|
585
|
+
repo,
|
|
586
|
+
gh,
|
|
587
|
+
});
|
|
588
|
+
if (summary.edgesFailed > 0) {
|
|
589
|
+
Logger.warn(
|
|
590
|
+
`[plan-persist] ${summary.edgesFailed} native blocked_by edge(s) could ` +
|
|
591
|
+
'not be written. Ordering survives in the `blocked by #N` body ' +
|
|
592
|
+
'footers; add the edges by hand if you want them in the GitHub UI.',
|
|
593
|
+
);
|
|
594
|
+
} else {
|
|
595
|
+
Logger.info(
|
|
596
|
+
`[plan-persist] native blocked_by edges: ${summary.edgesAdded} added, ` +
|
|
597
|
+
`${summary.edgesSkipped} already present.`,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return summary;
|
|
601
|
+
} catch (err) {
|
|
602
|
+
Logger.warn(
|
|
603
|
+
`[plan-persist] native blocked_by mirroring failed (${err.message}) — ` +
|
|
604
|
+
'ordering survives in the `blocked by #N` body footers.',
|
|
605
|
+
);
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Create Story issues via `provider.createIssue`, resumably.
|
|
612
|
+
*
|
|
613
|
+
* **Stories are born without `agent::ready`** (Story #4541). They used to
|
|
614
|
+
* carry it in the creating POST while the `story-plan-state` checkpoint was
|
|
615
|
+
* upserted afterwards, so anything that picked a Story up inside that window —
|
|
616
|
+
* or after a comment failure aborted the loop — read the checkpoint as `null`.
|
|
617
|
+
* Creation now applies `type::story` plus the sanitized authored labels only;
|
|
618
|
+
* `markStoriesReady` performs the flip as the terminal step, once every
|
|
619
|
+
* checkpoint is on the ticket.
|
|
620
|
+
*
|
|
621
|
+
* **The loop is resumable, and adoption is content-keyed.** Each body carries
|
|
622
|
+
* a plan-fingerprint marker, and the open `type::story` backlog is indexed by
|
|
623
|
+
* it before the first POST. A Story whose fingerprint already exists is
|
|
624
|
+
* adopted rather than re-created, so a re-run after a 502 at story *k* of *N*
|
|
625
|
+
* completes the cohort instead of minting a second copy of `1..k-1`.
|
|
626
|
+
*
|
|
627
|
+
* The fingerprint covers the assembled body, not just slug + title, so a hit
|
|
628
|
+
* means the open Story *is* what this run would author and adoption changes
|
|
629
|
+
* nothing. A same-named Story whose content has drifted — an abandoned plan, or
|
|
630
|
+
* an edited `stories.json` — misses the lookup, gets a correct new Story, and
|
|
631
|
+
* is named in a warning. Adoption never rewrites a body, so keying it on
|
|
632
|
+
* anything weaker than content would silently ship a stale one.
|
|
633
|
+
*
|
|
634
|
+
* Story #4540 retired the `plan-run::<id>` label this used to apply when
|
|
635
|
+
* N>1. Batch identity was the wrong axis to encode: it could not express an
|
|
636
|
+
* edge to a Story planned in a different run, and ordering already lives in
|
|
637
|
+
* the `blocked by #N` footers written below — which `/deliver`'s resolver
|
|
638
|
+
* reads directly, alongside native GitHub edges, from live state.
|
|
639
|
+
*
|
|
640
|
+
* **Sibling order is mirrored into native GitHub `blocked_by` edges** once
|
|
641
|
+
* every id is known (Story #4544), so plan-created order stops depending on
|
|
642
|
+
* prose. That pass is non-fatal — see `mirrorNativeDependencyEdges`.
|
|
643
|
+
*
|
|
644
|
+
* @param {object} args
|
|
645
|
+
* @param {object} args.provider
|
|
646
|
+
* @param {ReturnType<typeof assemblePlanStories>['stories']} args.stories
|
|
647
|
+
* @param {object} [args.opts]
|
|
648
|
+
* @param {boolean} [args.opts.dryRun=false]
|
|
649
|
+
* @returns {Promise<{
|
|
650
|
+
* created: Array<{ slug: string, id: number, url?: string, title: string, adopted: boolean }>,
|
|
651
|
+
* dependencyEdges: { edgesAdded: number, edgesSkipped: number, edgesFailed: number, storiesProcessed: number }|null,
|
|
652
|
+
* }>}
|
|
653
|
+
*/
|
|
654
|
+
export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
655
|
+
if (typeof provider?.createIssue !== 'function') {
|
|
656
|
+
throw new Error(
|
|
657
|
+
'[plan-persist] provider does not expose createIssue; cannot persist Stories.',
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const list = Array.isArray(stories) ? stories : [];
|
|
662
|
+
|
|
663
|
+
if (opts.dryRun) {
|
|
664
|
+
return {
|
|
665
|
+
created: list.map((s, i) => ({
|
|
666
|
+
slug: s.slug,
|
|
667
|
+
id: -(i + 1),
|
|
668
|
+
title: s.title,
|
|
669
|
+
url: undefined,
|
|
670
|
+
adopted: false,
|
|
671
|
+
})),
|
|
672
|
+
dependencyEdges: null,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const { byFingerprint, idsByTitle } = await indexExistingStories(provider);
|
|
677
|
+
const created = [];
|
|
678
|
+
const idBySlug = new Map();
|
|
679
|
+
|
|
680
|
+
for (const story of orderStoriesByDependencies(list)) {
|
|
681
|
+
const already = byFingerprint.get(story.fingerprint);
|
|
682
|
+
if (!already) warnOnDivergentSameTitleStory(story, idsByTitle);
|
|
683
|
+
if (already) {
|
|
684
|
+
Logger.info(
|
|
685
|
+
`[plan-persist] resuming: Story "${story.slug}" already exists as ` +
|
|
686
|
+
`#${already.id} with byte-identical authored content ` +
|
|
687
|
+
`(plan fingerprint ${story.fingerprint}) — skipping create.`,
|
|
688
|
+
);
|
|
689
|
+
created.push({
|
|
690
|
+
slug: story.slug,
|
|
691
|
+
id: already.id,
|
|
692
|
+
title: story.title,
|
|
693
|
+
url: already.url,
|
|
694
|
+
adopted: true,
|
|
695
|
+
});
|
|
696
|
+
idBySlug.set(story.slug, already.id);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const result = await provider.createIssue({
|
|
701
|
+
title: story.title,
|
|
702
|
+
body: renderStoryBodyForCreate(story, idBySlug),
|
|
703
|
+
labels: [...story.labels],
|
|
704
|
+
});
|
|
705
|
+
const id = result?.id ?? result?.number;
|
|
706
|
+
if (!Number.isInteger(id)) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
`[plan-persist] createIssue for slug "${story.slug}" did not return a numeric id`,
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
created.push({
|
|
712
|
+
slug: story.slug,
|
|
713
|
+
id,
|
|
714
|
+
title: story.title,
|
|
715
|
+
url: result.url,
|
|
716
|
+
adopted: false,
|
|
717
|
+
});
|
|
718
|
+
idBySlug.set(story.slug, id);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Every id is known now — including the adopted ones a resumed run reused —
|
|
722
|
+
// so a re-run mirrors the whole cohort's edges, not just the Stories this
|
|
723
|
+
// invocation happened to POST. Re-application is idempotent.
|
|
724
|
+
const dependencyEdges = await mirrorNativeDependencyEdges({
|
|
725
|
+
provider,
|
|
726
|
+
stories: list,
|
|
727
|
+
idBySlug,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
return { created, dependencyEdges };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Flip every created Story to `agent::ready` — the terminal step of persist
|
|
735
|
+
* (Story #4541).
|
|
736
|
+
*
|
|
737
|
+
* This is what makes `agent::ready` *mean* "fully persisted": by the time it
|
|
738
|
+
* lands, the Story's `story-plan-state` checkpoint is already on the ticket, so
|
|
739
|
+
* a `/deliver` that picks it up cannot read a null checkpoint.
|
|
740
|
+
*
|
|
741
|
+
* Fails closed: an un-flipped Story is invisible to `/deliver`, which is the
|
|
742
|
+
* safe direction — the operator is told exactly which ids need the label.
|
|
743
|
+
*
|
|
744
|
+
* @param {object} args
|
|
745
|
+
* @param {object} args.provider
|
|
746
|
+
* @param {Array<{ id: number, slug: string }>} args.created
|
|
747
|
+
* @returns {Promise<{ readied: number[] }>}
|
|
748
|
+
*/
|
|
749
|
+
export async function markStoriesReady({ provider, created }) {
|
|
750
|
+
if (typeof provider?.updateTicket !== 'function') {
|
|
751
|
+
throw new Error(
|
|
752
|
+
'[plan-persist] provider does not expose updateTicket; cannot flip ' +
|
|
753
|
+
'Stories to agent::ready.',
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
const readied = [];
|
|
757
|
+
const failed = [];
|
|
758
|
+
for (const story of created) {
|
|
759
|
+
try {
|
|
760
|
+
await provider.updateTicket(story.id, {
|
|
761
|
+
labels: { add: [AGENT_LABELS.READY] },
|
|
762
|
+
});
|
|
763
|
+
readied.push(story.id);
|
|
764
|
+
} catch (err) {
|
|
765
|
+
failed.push(`#${story.id} (${story.slug}): ${err.message}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (failed.length > 0) {
|
|
769
|
+
throw new Error(
|
|
770
|
+
`[plan-persist] ${failed.length} Story(ies) were created with their ` +
|
|
771
|
+
'checkpoints but could not be flipped to agent::ready:\n' +
|
|
772
|
+
`${failed.map((f) => ` - ${f}`).join('\n')}\n` +
|
|
773
|
+
'They are invisible to /deliver until the label lands. Re-run persist ' +
|
|
774
|
+
'(it resumes rather than duplicating) or add the label by hand.',
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
return { readied };
|
|
778
|
+
}
|