mandrel 2.0.0 → 2.2.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 +59 -28
- package/.agents/agents/acceptance-critic.md +20 -9
- package/.agents/agents/story-worker.md +45 -48
- package/.agents/audit-checklists/performance.md +1 -1
- package/.agents/docs/SDLC.md +60 -46
- package/.agents/docs/agentrc-reference.json +8 -13
- package/.agents/docs/configuration.md +33 -57
- package/.agents/docs/execution-reference.md +39 -10
- package/.agents/docs/quality-gates.md +17 -19
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +64 -79
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +10 -0
- package/.agents/rules/git-conventions-reference.md +42 -51
- package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
- package/.agents/schemas/agentrc.schema.json +35 -46
- 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/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 +24 -68
- package/.agents/scripts/agents-bootstrap-github.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 +1 -1
- package/.agents/scripts/check-workflow-cli-lint.js +91 -0
- 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/audit-suite/selector.js +323 -23
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -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 +4 -1
- package/.agents/scripts/lib/checks/index.js +1 -1
- 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 +3 -3
- 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-args.js +23 -2
- package/.agents/scripts/lib/close-validation/gates.js +13 -13
- 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/acceptance-eval.js +2 -2
- package/.agents/scripts/lib/config/delivery-routing.js +7 -6
- package/.agents/scripts/lib/config/explain.js +10 -16
- package/.agents/scripts/lib/config/github.js +7 -5
- package/.agents/scripts/lib/config/limits.js +15 -25
- package/.agents/scripts/lib/config/quality.js +11 -14
- package/.agents/scripts/lib/config/runners.js +8 -21
- package/.agents/scripts/lib/config/temp-paths.js +18 -56
- package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
- package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
- package/.agents/scripts/lib/config-settings-schema.js +48 -22
- package/.agents/scripts/lib/dead-exports-knip.js +105 -0
- package/.agents/scripts/lib/dead-exports-mode.js +51 -0
- package/.agents/scripts/lib/duplicate-search.js +38 -7
- 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 -189
- package/.agents/scripts/lib/gh-exec.js +8 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
- package/.agents/scripts/lib/git-utils.js +0 -14
- package/.agents/scripts/lib/json-utils.js +1 -2
- package/.agents/scripts/lib/label-constants.js +0 -15
- package/.agents/scripts/lib/label-taxonomy.js +1 -12
- package/.agents/scripts/lib/observability/active-story-env.js +42 -163
- 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 +2 -2
- package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
- 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 +43 -45
- package/.agents/scripts/lib/orchestration/change-set.js +103 -0
- package/.agents/scripts/lib/orchestration/code-review.js +70 -191
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
- 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/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 +9 -11
- 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/listeners/README.md +7 -3
- 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/merge-block-class.js +76 -20
- package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
- package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
- package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
- package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
- package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
- package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
- package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
- 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/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 +374 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
- 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 +4 -13
- 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 +72 -30
- 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 +12 -8
- 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 +264 -43
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
- package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
- package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
- package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
- package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
- package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
- 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 -286
- package/.agents/scripts/lib/preflight-runner.js +2 -2
- 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/skills/parse-skill.js +16 -3
- package/.agents/scripts/lib/story-adjacency.js +8 -7
- package/.agents/scripts/lib/story-body/story-body.js +81 -13
- package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
- package/.agents/scripts/lib/test-env.js +14 -1
- package/.agents/scripts/lib/test-tiers.js +0 -3
- package/.agents/scripts/lib/ticket-body-sections.js +0 -14
- package/.agents/scripts/lib/validation-evidence.js +31 -59
- package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
- package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
- package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
- package/.agents/scripts/plan-context.js +38 -7
- package/.agents/scripts/plan-critics.js +203 -0
- package/.agents/scripts/plan-persist.js +145 -35
- package/.agents/scripts/plan-run-epilogue.js +83 -38
- 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 +1 -11
- package/.agents/scripts/providers/github/mappers.js +5 -0
- package/.agents/scripts/providers/github/sub-issues.js +0 -47
- package/.agents/scripts/providers/github/tickets.js +33 -153
- package/.agents/scripts/providers/github.js +17 -6
- package/.agents/scripts/quality-preview.js +13 -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 +9 -10
- package/.agents/scripts/single-story-close.js +173 -18
- package/.agents/scripts/single-story-confirm-merge.js +288 -15
- package/.agents/scripts/single-story-init.js +6 -10
- package/.agents/scripts/stories-wave-tick.js +380 -53
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/update-ticket-state.js +8 -50
- 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 +3 -3
- package/.agents/skills/core/scope-triage/SKILL.md +3 -0
- 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 +2 -12
- 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/workflows/audit-architecture.md +3 -4
- package/.agents/workflows/audit-clean-code.md +4 -4
- package/.agents/workflows/audit-documentation.md +4 -5
- package/.agents/workflows/audit-lighthouse.md +8 -0
- package/.agents/workflows/audit-navigability.md +10 -0
- package/.agents/workflows/audit-performance.md +2 -3
- package/.agents/workflows/audit-quality.md +8 -9
- package/.agents/workflows/audit-security.md +1 -2
- package/.agents/workflows/audit-seo.md +10 -0
- package/.agents/workflows/audit-ux-ui.md +7 -0
- package/.agents/workflows/deliver.md +133 -45
- package/.agents/workflows/git-cleanup.md +2 -2
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
- package/.agents/workflows/helpers/code-review.md +14 -12
- package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
- package/.agents/workflows/helpers/deliver-story.md +209 -118
- package/.agents/workflows/helpers/parallel-tooling.md +2 -2
- package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
- package/.agents/workflows/plan.md +239 -19
- package/.agents/workflows/qa-assist.md +6 -6
- package/.agents/workflows/qa-explore.md +3 -3
- package/.agents/workflows/qa-run.md +1 -5
- package/bin/mandrel.js +12 -1
- package/docs/CHANGELOG.md +62 -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 +17 -9
- 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/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
- package/package.json +2 -2
- package/.agents/schemas/epic-perf-report.schema.json +0 -89
- 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/story.heartbeat.schema.json +0 -20
- package/.agents/schemas/risk-verdict.schema.json +0 -53
- package/.agents/schemas/story-perf-summary.schema.json +0 -73
- package/.agents/scripts/analyze-execution.js +0 -444
- package/.agents/scripts/check-prepush-recovery.js +0 -90
- 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 -187
- 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/audit-lens-routing.js +0 -128
- package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
- package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
- 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/merge-watcher.js +0 -665
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
- package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
- 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 -21
- 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 -397
- package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
- package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
- package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
- package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
- package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
- package/.agents/scripts/resolve-plan-run.js +0 -117
- package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* @module lib/orchestration/plan-persist/story-ops
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { applyBlockedByDependencies } from '../../../providers/github/blocked-by-add.js';
|
|
20
|
+
import { Logger } from '../../Logger.js';
|
|
20
21
|
import { AGENT_LABELS, TYPE_LABELS } from '../../label-constants.js';
|
|
21
22
|
import {
|
|
22
23
|
parse as parseStoryBody,
|
|
@@ -24,42 +25,142 @@ import {
|
|
|
24
25
|
} from '../../story-body/story-body.js';
|
|
25
26
|
import { assertSpecWithinBudget } from '../spec-spill.js';
|
|
26
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:';
|
|
27
46
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
47
|
+
/** Length of the hex fingerprint digest. Collision-free at plan scale. */
|
|
48
|
+
const PLAN_FINGERPRINT_LENGTH = 16;
|
|
30
49
|
|
|
31
50
|
/**
|
|
32
|
-
*
|
|
33
|
-
* share this helper so human-readable ids map to one canonical label.
|
|
51
|
+
* Compute the deterministic identity of one authored Story within a plan.
|
|
34
52
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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.
|
|
37
93
|
*/
|
|
38
|
-
export function
|
|
39
|
-
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.replace(/[^a-z0-9._-]+/g, '-');
|
|
44
|
-
if (!token) {
|
|
45
|
-
throw new Error('plan-run id requires a non-empty planRunId');
|
|
46
|
-
}
|
|
47
|
-
return token;
|
|
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);
|
|
48
99
|
}
|
|
49
100
|
|
|
50
101
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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.
|
|
53
105
|
*
|
|
54
|
-
* @param {string}
|
|
106
|
+
* @param {string} fingerprint
|
|
55
107
|
* @returns {string}
|
|
56
108
|
*/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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];
|
|
63
164
|
}
|
|
64
165
|
|
|
65
166
|
function bodyObjectFromTicket(ticket) {
|
|
@@ -126,8 +227,12 @@ function syncContractFieldFromTopLevel(ticket, bodyObject, field) {
|
|
|
126
227
|
* Normalize a plan Story ticket into `{ slug, title, bodyObject }`.
|
|
127
228
|
* Accepts either a serialized markdown `body` string or a structured body.
|
|
128
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
|
+
*
|
|
129
234
|
* @param {object} ticket
|
|
130
|
-
* @returns {{ slug: string, title: string, bodyObject: object, depends_on: string[] }}
|
|
235
|
+
* @returns {{ slug: string, title: string, bodyObject: object, depends_on: string[], labels: string[], supersedes: Array<{ id: number, note: string|null }> }}
|
|
131
236
|
*/
|
|
132
237
|
export function normalizeStoryTicket(ticket) {
|
|
133
238
|
if (!ticket || typeof ticket !== 'object') {
|
|
@@ -150,8 +255,10 @@ export function normalizeStoryTicket(ticket) {
|
|
|
150
255
|
syncContractFieldFromTopLevel(ticket, bodyObject, 'acceptance');
|
|
151
256
|
syncContractFieldFromTopLevel(ticket, bodyObject, 'verify');
|
|
152
257
|
const depends_on = normalizeDependsOn(ticket, bodyObject);
|
|
258
|
+
const supersedes = normalizeSupersedes(ticket, slug);
|
|
259
|
+
const labels = sanitizeAuthoredLabels(ticket.labels, slug);
|
|
153
260
|
|
|
154
|
-
return { slug, title, bodyObject, depends_on };
|
|
261
|
+
return { slug, title, bodyObject, depends_on, labels, supersedes };
|
|
155
262
|
}
|
|
156
263
|
|
|
157
264
|
/**
|
|
@@ -194,11 +301,15 @@ export function foldSpecIntoStoryBody(bodyObject, slug, opts = {}) {
|
|
|
194
301
|
}
|
|
195
302
|
|
|
196
303
|
function assembleOnePlanStory(ticket, opts) {
|
|
197
|
-
const { slug, title, bodyObject, depends_on } =
|
|
304
|
+
const { slug, title, bodyObject, depends_on, labels, supersedes } =
|
|
305
|
+
normalizeStoryTicket(ticket);
|
|
198
306
|
const { bodyObject: folded } = foldSpecIntoStoryBody(bodyObject, slug, {
|
|
199
307
|
sharedSpec: opts.sharedSpec ?? null,
|
|
200
308
|
});
|
|
309
|
+
// Body first: the fingerprint is an identity over the *assembled* content,
|
|
310
|
+
// so it cannot be computed until that content exists.
|
|
201
311
|
const body = serializeStoryBody({ ...folded, depends_on });
|
|
312
|
+
const fingerprint = planStoryFingerprint({ slug, title, body });
|
|
202
313
|
return {
|
|
203
314
|
story: {
|
|
204
315
|
slug,
|
|
@@ -207,6 +318,9 @@ function assembleOnePlanStory(ticket, opts) {
|
|
|
207
318
|
bodyObject: { ...folded, depends_on },
|
|
208
319
|
acceptance: Array.isArray(folded.acceptance) ? folded.acceptance : [],
|
|
209
320
|
depends_on,
|
|
321
|
+
labels,
|
|
322
|
+
fingerprint,
|
|
323
|
+
supersedes,
|
|
210
324
|
},
|
|
211
325
|
};
|
|
212
326
|
}
|
|
@@ -230,13 +344,17 @@ function assertSharedSpecAllowed(tickets, sharedSpec) {
|
|
|
230
344
|
|
|
231
345
|
/**
|
|
232
346
|
* Assemble markdown bodies for every Story: normalize → fold spec →
|
|
233
|
-
* assertAcceptancePartition → serialize.
|
|
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.
|
|
234
351
|
*
|
|
235
352
|
* @param {object[]} tickets
|
|
236
353
|
* @param {object} [opts]
|
|
237
354
|
* @param {string|null} [opts.sharedSpec]
|
|
238
355
|
* @param {string[]} [opts.planAcceptance]
|
|
239
|
-
* @
|
|
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 }> }> }}
|
|
240
358
|
*/
|
|
241
359
|
export function assemblePlanStories(tickets, opts = {}) {
|
|
242
360
|
if (!Array.isArray(tickets) || tickets.length === 0) {
|
|
@@ -254,6 +372,7 @@ export function assemblePlanStories(tickets, opts = {}) {
|
|
|
254
372
|
assertAcceptancePartition(stories, {
|
|
255
373
|
planAcceptance: opts.planAcceptance,
|
|
256
374
|
});
|
|
375
|
+
assertSupersedePartition(stories, opts.sourceTicketIds ?? []);
|
|
257
376
|
|
|
258
377
|
return { stories };
|
|
259
378
|
}
|
|
@@ -289,16 +408,248 @@ function orderStoriesByDependencies(stories) {
|
|
|
289
408
|
}
|
|
290
409
|
|
|
291
410
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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`.
|
|
294
643
|
*
|
|
295
644
|
* @param {object} args
|
|
296
645
|
* @param {object} args.provider
|
|
297
646
|
* @param {ReturnType<typeof assemblePlanStories>['stories']} args.stories
|
|
298
647
|
* @param {object} [args.opts]
|
|
299
|
-
* @param {string} [args.opts.planRunId]
|
|
300
648
|
* @param {boolean} [args.opts.dryRun=false]
|
|
301
|
-
* @returns {Promise<{
|
|
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
|
+
* }>}
|
|
302
653
|
*/
|
|
303
654
|
export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
304
655
|
if (typeof provider?.createIssue !== 'function') {
|
|
@@ -308,9 +659,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
308
659
|
}
|
|
309
660
|
|
|
310
661
|
const list = Array.isArray(stories) ? stories : [];
|
|
311
|
-
const planLabel = list.length > 1 ? planRunLabel(opts.planRunId) : null;
|
|
312
|
-
const labels = [TYPE_LABELS.STORY, AGENT_LABELS.READY];
|
|
313
|
-
if (planLabel) labels.push(planLabel);
|
|
314
662
|
|
|
315
663
|
if (opts.dryRun) {
|
|
316
664
|
return {
|
|
@@ -319,28 +667,40 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
319
667
|
id: -(i + 1),
|
|
320
668
|
title: s.title,
|
|
321
669
|
url: undefined,
|
|
670
|
+
adopted: false,
|
|
322
671
|
})),
|
|
323
|
-
|
|
672
|
+
dependencyEdges: null,
|
|
324
673
|
};
|
|
325
674
|
}
|
|
326
675
|
|
|
676
|
+
const { byFingerprint, idsByTitle } = await indexExistingStories(provider);
|
|
327
677
|
const created = [];
|
|
328
|
-
const
|
|
678
|
+
const idBySlug = new Map();
|
|
679
|
+
|
|
329
680
|
for (const story of orderStoriesByDependencies(list)) {
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
|
|
340
700
|
const result = await provider.createIssue({
|
|
341
701
|
title: story.title,
|
|
342
|
-
body,
|
|
343
|
-
labels: [...labels],
|
|
702
|
+
body: renderStoryBodyForCreate(story, idBySlug),
|
|
703
|
+
labels: [...story.labels],
|
|
344
704
|
});
|
|
345
705
|
const id = result?.id ?? result?.number;
|
|
346
706
|
if (!Number.isInteger(id)) {
|
|
@@ -353,9 +713,66 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
353
713
|
id,
|
|
354
714
|
title: story.title,
|
|
355
715
|
url: result.url,
|
|
716
|
+
adopted: false,
|
|
356
717
|
});
|
|
357
|
-
|
|
718
|
+
idBySlug.set(story.slug, id);
|
|
358
719
|
}
|
|
359
720
|
|
|
360
|
-
|
|
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 };
|
|
361
778
|
}
|