mandrel 1.59.0 → 1.61.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.
Files changed (267) hide show
  1. package/.agents/README.md +86 -44
  2. package/.agents/docs/SDLC.md +135 -141
  3. package/.agents/docs/configuration.md +77 -20
  4. package/.agents/docs/quality-gates.md +796 -0
  5. package/.agents/docs/workflows.md +6 -9
  6. package/.agents/instructions.md +12 -11
  7. package/.agents/personas/architect.md +1 -1
  8. package/.agents/personas/product.md +1 -1
  9. package/.agents/personas/project-manager.md +14 -14
  10. package/.agents/personas/technical-writer.md +1 -1
  11. package/.agents/rules/changelog-style.md +5 -5
  12. package/.agents/rules/git-conventions.md +3 -3
  13. package/.agents/runtime-deps.json +2 -2
  14. package/.agents/schemas/agentrc.schema.json +3 -3
  15. package/.agents/schemas/dispatch-manifest.json +4 -4
  16. package/.agents/schemas/epic-spec.schema.json +15 -45
  17. package/.agents/schemas/lifecycle/README.md +1 -1
  18. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
  20. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
  21. package/.agents/schemas/validation-evidence.schema.json +1 -1
  22. package/.agents/scripts/README.md +2 -2
  23. package/.agents/scripts/acceptance-eval.js +1 -1
  24. package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
  25. package/.agents/scripts/agents-bootstrap-github.js +23 -119
  26. package/.agents/scripts/analyze-execution.js +2 -2
  27. package/.agents/scripts/audit-to-stories.js +1 -1
  28. package/.agents/scripts/check-doc-links.js +2 -3
  29. package/.agents/scripts/diagnose-friction.js +1 -1
  30. package/.agents/scripts/dispatcher.js +2 -2
  31. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  32. package/.agents/scripts/epic-audit-prepare.js +3 -3
  33. package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
  34. package/.agents/scripts/epic-deliver-preflight.js +6 -6
  35. package/.agents/scripts/epic-deliver-prepare.js +1 -1
  36. package/.agents/scripts/epic-execute-record-wave.js +4 -4
  37. package/.agents/scripts/epic-plan-healthcheck.js +6 -10
  38. package/.agents/scripts/epic-plan-spec-validate.js +1 -1
  39. package/.agents/scripts/epic-reconcile.js +11 -29
  40. package/.agents/scripts/evidence-gate.js +1 -1
  41. package/.agents/scripts/generate-workflows-doc.js +1 -1
  42. package/.agents/scripts/hierarchy-gate.js +7 -11
  43. package/.agents/scripts/lib/ITicketingProvider.js +1 -1
  44. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  45. package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
  46. package/.agents/scripts/lib/baseline-snapshot.js +7 -7
  47. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  48. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  49. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
  50. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  51. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +47 -1
  52. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  53. package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
  54. package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
  55. package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
  56. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
  57. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  58. package/.agents/scripts/lib/config/explain.js +1 -1
  59. package/.agents/scripts/lib/config/runners.js +2 -2
  60. package/.agents/scripts/lib/config/runtime.js +1 -1
  61. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  62. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  63. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  64. package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
  65. package/.agents/scripts/lib/config-settings-schema.js +3 -3
  66. package/.agents/scripts/lib/detect-package-manager.js +72 -0
  67. package/.agents/scripts/lib/duplicate-search.js +1 -1
  68. package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
  69. package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
  70. package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
  71. package/.agents/scripts/lib/errors/index.js +4 -4
  72. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
  73. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  74. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  75. package/.agents/scripts/lib/findings/promote-finding.js +10 -10
  76. package/.agents/scripts/lib/label-constants.js +3 -4
  77. package/.agents/scripts/lib/label-taxonomy.js +5 -10
  78. package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
  79. package/.agents/scripts/lib/onboard/init-tail.js +218 -0
  80. package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
  81. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  82. package/.agents/scripts/lib/orchestration/code-review.js +5 -5
  83. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
  84. package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
  85. package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
  86. package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
  87. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +9 -25
  88. package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
  89. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +8 -8
  90. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
  91. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
  92. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
  93. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +26 -13
  94. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +1 -1
  95. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
  96. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +2 -2
  97. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
  98. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
  99. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
  100. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
  101. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +6 -21
  102. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
  103. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -1
  104. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -2
  105. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
  106. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
  107. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +8 -8
  108. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -4
  109. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
  110. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
  111. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
  112. package/.agents/scripts/lib/orchestration/file-assumptions.js +2 -2
  113. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
  114. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
  115. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
  116. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -3
  117. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
  118. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +1 -1
  119. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
  120. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
  121. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
  122. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
  123. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
  124. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  125. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
  126. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
  127. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +1 -1
  128. package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
  129. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
  130. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
  131. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
  132. package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
  133. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
  134. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
  135. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
  136. package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
  137. package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
  139. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  140. package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
  141. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  142. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
  143. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +2 -2
  144. package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
  145. package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
  146. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -2
  147. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
  148. package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
  149. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -12
  150. package/.agents/scripts/lib/orchestration/ticketing/reads.js +8 -8
  151. package/.agents/scripts/lib/orchestration/ticketing/state.js +3 -3
  152. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +2 -2
  153. package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -1
  154. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  155. package/.agents/scripts/lib/preflight-runner.js +1 -1
  156. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
  157. package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
  158. package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
  159. package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
  160. package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
  161. package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
  162. package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
  163. package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
  164. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
  165. package/.agents/scripts/lib/signals/schema.js +1 -1
  166. package/.agents/scripts/lib/spec/index.js +1 -1
  167. package/.agents/scripts/lib/spec/loader.js +2 -2
  168. package/.agents/scripts/lib/spec/state.js +7 -16
  169. package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
  170. package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
  171. package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
  172. package/.agents/scripts/lib/story-lifecycle.js +8 -8
  173. package/.agents/scripts/lib/story-plan.js +1 -1
  174. package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
  175. package/.agents/scripts/lib/wave-runner/tick.js +1 -1
  176. package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
  177. package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
  178. package/.agents/scripts/lifecycle-emit.js +1 -1
  179. package/.agents/scripts/providers/github/board-add.js +1 -1
  180. package/.agents/scripts/providers/github/errors.js +1 -1
  181. package/.agents/scripts/providers/github/mappers.js +2 -2
  182. package/.agents/scripts/providers/github/tickets.js +4 -4
  183. package/.agents/scripts/resync-status-column.js +1 -1
  184. package/.agents/scripts/retro-run.js +2 -2
  185. package/.agents/scripts/run-lint.js +1 -1
  186. package/.agents/scripts/single-story-init.js +1 -1
  187. package/.agents/scripts/stories-wave-tick.js +5 -5
  188. package/.agents/scripts/story-close.js +1 -1
  189. package/.agents/scripts/story-init.js +13 -16
  190. package/.agents/scripts/story-phase.js +5 -5
  191. package/.agents/scripts/story-plan.js +3 -3
  192. package/.agents/scripts/sync-branch-from-base.js +1 -1
  193. package/.agents/scripts/validate-docs-freshness.js +1 -1
  194. package/.agents/scripts/wave-tick.js +1 -1
  195. package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
  196. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
  197. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
  198. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
  199. package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
  200. package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
  201. package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
  202. package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
  203. package/.agents/skills/core/scope-triage/SKILL.md +9 -10
  204. package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
  205. package/.agents/skills/skills.index.json +7 -7
  206. package/.agents/templates/agent-protocol.md +2 -2
  207. package/.agents/workflows/agents-update.md +16 -31
  208. package/.agents/workflows/audit-architecture.md +2 -2
  209. package/.agents/workflows/audit-clean-code.md +2 -2
  210. package/.agents/workflows/audit-dependencies.md +1 -1
  211. package/.agents/workflows/audit-devops.md +1 -1
  212. package/.agents/workflows/audit-documentation.md +2 -2
  213. package/.agents/workflows/audit-lighthouse.md +1 -1
  214. package/.agents/workflows/audit-performance.md +2 -2
  215. package/.agents/workflows/audit-privacy.md +1 -1
  216. package/.agents/workflows/audit-quality.md +2 -2
  217. package/.agents/workflows/audit-security.md +2 -2
  218. package/.agents/workflows/audit-seo.md +1 -1
  219. package/.agents/workflows/audit-sre.md +1 -1
  220. package/.agents/workflows/audit-to-stories.md +10 -10
  221. package/.agents/workflows/audit-ux-ui.md +1 -1
  222. package/.agents/workflows/deliver.md +85 -0
  223. package/.agents/workflows/explain.md +3 -3
  224. package/.agents/workflows/git-merge-pr.md +1 -1
  225. package/.agents/workflows/git-pr-all.md +13 -10
  226. package/.agents/workflows/git-push.md +6 -3
  227. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  228. package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
  229. package/.agents/workflows/helpers/agents-sync-config.md +3 -2
  230. package/.agents/workflows/helpers/code-review.md +5 -5
  231. package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +43 -43
  232. package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
  233. package/.agents/workflows/helpers/diagnose.md +1 -1
  234. package/.agents/workflows/helpers/epic-audit.md +6 -6
  235. package/.agents/workflows/helpers/epic-deliver-story.md +13 -13
  236. package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
  237. package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
  238. package/.agents/workflows/helpers/epic-testing.md +3 -3
  239. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  240. package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
  241. package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
  242. package/.agents/workflows/helpers/signals.md +1 -1
  243. package/.agents/workflows/helpers/single-story-deliver.md +11 -11
  244. package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
  245. package/.agents/workflows/plan.md +131 -0
  246. package/.agents/workflows/qa-explore.md +1 -1
  247. package/.agents/workflows/qa-run-harness.md +1 -1
  248. package/README.md +19 -39
  249. package/bin/mandrel.js +235 -16
  250. package/docs/CHANGELOG.md +1173 -0
  251. package/lib/cli/doctor.js +45 -3
  252. package/lib/cli/init.js +97 -36
  253. package/lib/cli/registry.js +41 -145
  254. package/lib/cli/sync.js +122 -23
  255. package/lib/cli/uninstall.js +42 -7
  256. package/lib/cli/update.js +524 -210
  257. package/lib/cli/version-helpers.js +59 -0
  258. package/package.json +7 -6
  259. package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
  260. package/.agents/workflows/onboard.md +0 -208
  261. package/lib/cli/__tests__/migrate.test.js +0 -268
  262. package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
  263. package/lib/cli/__tests__/sync.test.js +0 -372
  264. package/lib/cli/__tests__/update-major.test.js +0 -217
  265. package/lib/cli/__tests__/update.test.js +0 -696
  266. package/lib/cli/__tests__/version-check.test.js +0 -398
  267. package/lib/migrations/__tests__/index.test.js +0 -216
@@ -100,7 +100,7 @@ export function renderBaselineFrictionBody({ rows, epicId, storyId } = {}) {
100
100
  const triage = [
101
101
  '**Triage:**',
102
102
  `1. Open each suspect Story above and run \`npm run maintainability:update\` (or \`npm run crap:update\`) on **its** branch, then commit with a \`baseline-refresh:\` subject and re-close it.`,
103
- `2. Re-run \`/story-deliver ${storyId}\` once the suspect Story's refresh has merged into \`epic/${epicId}\`.`,
103
+ `2. Re-run \`/deliver ${storyId}\` once the suspect Story's refresh has merged into \`epic/${epicId}\`.`,
104
104
  `3. If the suspect column reads \`_unknown_\`, the path has no commit on \`epic/${epicId}\` — investigate the baseline file directly before refreshing.`,
105
105
  ];
106
106
 
@@ -127,7 +127,7 @@ export async function runGatesAndRefresh(
127
127
  * Read the parent Epic's judged `planningRisk` envelope off its
128
128
  * `epic-plan-state` checkpoint so the Story-scope review can inherit the
129
129
  * Epic's review depth (Story #3940). Best-effort and total — it reuses the
130
- * shared `read` from `epic-plan-state-store.js` (the same reader `/epic-plan
130
+ * shared `read` from `epic-plan-state-store.js` (the same reader `/plan
131
131
  * --resume` and `epic-audit-prepare.js`'s `resolveRiskRoutedLenses` use, no
132
132
  * third bespoke reader) and never fails the close:
133
133
  *
@@ -137,7 +137,7 @@ export async function runGatesAndRefresh(
137
137
  *
138
138
  * A `null` result threads through `runStoryCodeReview` → `runCodeReview`
139
139
  * unchanged, so depth resolves from diff width alone (`standard`), preserving
140
- * today's behaviour for an Epic that skipped `/epic-plan`.
140
+ * today's behaviour for an Epic that skipped `/plan`.
141
141
  *
142
142
  * @param {{ provider: object, epicId: number|null|undefined, readPlanStateFn?: typeof readPlanState }} args
143
143
  * @returns {Promise<{ overallLevel?: string, axes?: Array<object> }|null>}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Story body schema validator (v5.33+).
3
3
  *
4
- * Enforces the four-section structured body shape on 3-tier Stories emitted
4
+ * Enforces the four-section structured body shape on 2-tier Stories emitted
5
5
  * by the decomposer. The canonical decomposition serializes every Story
6
6
  * `body` to a **markdown string** via `serialize()` from
7
7
  * `lib/story-body/story-body.js` (the decompose-author skill mandates this,
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * Only `type: 'story'` tickets are validated; Feature/Epic tickets and
19
19
  * null/empty bodies pass through. There is no `type::task` ticket layer in
20
- * the 3-tier hierarchy (Epic → Feature → Story).
20
+ * the 2-tier hierarchy (Epic → Story).
21
21
  *
22
22
  * Required after parse/normalize: a non-empty `goal`, and non-empty
23
23
  * `changes`, `acceptance`, and `verify` arrays — and `changes` items must
@@ -110,7 +110,7 @@ function vagueVerbWithoutTarget(bullet) {
110
110
  * - it has no body (null / undefined / empty-or-whitespace string — there
111
111
  * is nothing to inspect).
112
112
  *
113
- * Under the 3-tier hierarchy (Epic → Feature → Story), Stories carry the
113
+ * Under the 2-tier hierarchy (Epic → Story), Stories carry the
114
114
  * implementation scope inline. A canonical decomposition serializes the
115
115
  * Story body to a markdown string, so a *string* body is NOT skipped here
116
116
  * (Story #3906) — `validateTaskBodyShape` parses it back into structured
@@ -127,7 +127,7 @@ function vagueVerbWithoutTarget(bullet) {
127
127
  */
128
128
  function shouldSkipTicket(ticket) {
129
129
  if (!ticket) return true;
130
- // Only Stories carry an inline implementation contract in the 3-tier
130
+ // Only Stories carry an inline implementation contract in the 2-tier
131
131
  // world. Features (and everything else) use narrative bodies.
132
132
  if (ticket.type !== 'story') return true;
133
133
  const body = ticket.body;
@@ -197,7 +197,7 @@ export function validateTaskBodyShape(ticket) {
197
197
  }
198
198
  errors.push(...collectChangesErrors(prefix, body.changes));
199
199
  errors.push(...collectAcceptanceErrors(prefix, body.acceptance));
200
- // Tier-suffix validation is always enforced on Story bodies (3-tier world).
200
+ // Tier-suffix validation is always enforced on Story bodies (2-tier world).
201
201
  errors.push(...collectVerifyErrors(prefix, body.verify));
202
202
  errors.push(...collectReferencesErrors(prefix, body.references));
203
203
  return errors;
@@ -359,7 +359,7 @@ function collectVerifyErrors(prefix, rawVerify) {
359
359
  }
360
360
 
361
361
  /**
362
- * Validate every 3-tier Story in `tickets` whose `body` is a structured
362
+ * Validate every 2-tier Story in `tickets` whose `body` is a structured
363
363
  * object. Returns an array of error strings (one per offending slug); empty
364
364
  * array means clean.
365
365
  *
@@ -96,7 +96,7 @@ export function normalizeOperatorHandle(raw) {
96
96
  * observability artifact never wedges the lease preflight.
97
97
  *
98
98
  * This is the shared liveness source the lease guards thread into
99
- * `acquireLease` via `heartbeatAt`; `/epic-plan` and `/epic-deliver` both
99
+ * `acquireLease` via `heartbeatAt`; `/plan` and `/deliver` both
100
100
  * reuse it so a live foreign claim actually refuses.
101
101
  *
102
102
  * @param {object} args
@@ -170,7 +170,7 @@ function collectStoryProducerPaths(story) {
170
170
  }
171
171
 
172
172
  /**
173
- * Resolve the Story-identifying slug for a 3-tier Story. A Story is its
173
+ * Resolve the Story-identifying slug for a 2-tier Story. A Story is its
174
174
  * own implementation unit (Epic #3238) — there is no parent Task — so the
175
175
  * producer/consumer indices key on the Story's own `slug`.
176
176
  */
@@ -187,7 +187,7 @@ function storySlugOf(story) {
187
187
  * producers.
188
188
  *
189
189
  * `taskSlug` is retained in the entry shape for finding/render
190
- * compatibility; in the 3-tier model it carries the Story's own slug since
190
+ * compatibility; in the 2-tier model it carries the Story's own slug since
191
191
  * the Story is the implementation unit.
192
192
  */
193
193
  function indexProducers(stories) {
@@ -39,15 +39,6 @@ export const DEFAULT_TASK_SIZING = Object.freeze({
39
39
  maxAcceptance: 14,
40
40
  });
41
41
 
42
- /**
43
- * Soft prose guidance for how many Stories a Feature typically decomposes
44
- * into before the Feature scope smells like two Features. Rendered into the
45
- * decomposer prompt's Feature-count sentence from this single constant so
46
- * the prompt prose cannot drift from the SSOT module (Story #3874). Advisory
47
- * only — no validator finding keys off it.
48
- */
49
- export const SOFT_STORIES_PER_FEATURE = 7;
50
-
51
42
  /**
52
43
  * `DELIVERABLE_GRANULARITY_GUIDANCE` is the **single source of truth** for the
53
44
  * deliverable-granularity definition of a Story (Story #3777). It is stated
@@ -339,7 +330,7 @@ function computeStorySizingFindings(story, sizing) {
339
330
  * `findings`; the AC-visible `errors[]` channel is the rendered
340
331
  * subset where `severity === 'hard'`.
341
332
  *
342
- * 3-tier (Epic #3238): each Story is its own implementation unit and
333
+ * 2-tier (Epic #3238): each Story is its own implementation unit and
343
334
  * carries the `body` (acceptance / changes / wide) that the sizing layers
344
335
  * score. There is no Task tier, so findings are computed directly over
345
336
  * `stories`.
@@ -200,9 +200,8 @@ function makeMemoizedGitRunner(runner) {
200
200
  * path was deleted between planning and decomposition) — refuse to decompose
201
201
  * because the resulting Task would be unimplementable as written.
202
202
  *
203
- * Only Tasks are scanned; Features and Stories carry narrative copy, not
204
- * implementation paths, and their bodies routinely reference docs/templates
205
- * the freshness regex would (correctly) ignore.
203
+ * Only Stories are scanned they are the implementation unit; the Epic
204
+ * carries narrative copy, not implementation paths.
206
205
  *
207
206
  * @param {object} opts
208
207
  * @param {object[]} opts.tickets - Validated ticket hierarchy.
@@ -416,7 +415,6 @@ function renderMissLine({ slug, path }) {
416
415
 
417
416
  function indexTicketsBySlug(tickets) {
418
417
  const ticketBySlug = new Map();
419
- const features = [];
420
418
  const stories = [];
421
419
  const slugAdjacency = new Map();
422
420
  for (const t of tickets) {
@@ -429,75 +427,37 @@ function indexTicketsBySlug(tickets) {
429
427
  ticketBySlug.set(t.slug, t);
430
428
  }
431
429
  slugAdjacency.set(t.slug, t.depends_on ?? []);
432
- if (t.type === 'feature') features.push(t);
433
- else if (t.type === 'story') stories.push(t);
430
+ if (t.type === 'story') stories.push(t);
434
431
  }
435
- return { ticketBySlug, features, stories, slugAdjacency };
432
+ return { ticketBySlug, stories, slugAdjacency };
436
433
  }
437
434
 
438
- function assertEachTypePresent({ features, stories }) {
439
- if (features.length === 0)
435
+ /**
436
+ * 2-tier invariant (Story #4041): the decomposer emits Stories only — every
437
+ * ticket in the backlog must be `type: "story"` and at least one must be
438
+ * present. Any other type (the retired `feature`/`task` tiers, or planner
439
+ * hallucinations) HARD-rejects the decomposition.
440
+ */
441
+ function assertAllTicketsAreStories({ tickets, stories }) {
442
+ const nonStories = (tickets ?? []).filter((t) => t.type !== 'story');
443
+ if (nonStories.length > 0) {
444
+ const list = nonStories
445
+ .map((t) => `"${t.title}" (${t.slug ?? '<no slug>'}, type: ${t.type})`)
446
+ .join(', ');
440
447
  throw new Error(
441
- 'Cross-Validation Failed: Backlog must contain at least one Feature.',
448
+ `Cross-Validation Failed: ${nonStories.length} ticket(s) are not Stories: ${list}. ` +
449
+ 'The 2-tier hierarchy (Epic → Story) admits type "story" only — there is no Feature or Task tier.',
442
450
  );
451
+ }
443
452
  if (stories.length === 0)
444
453
  throw new Error(
445
454
  'Cross-Validation Failed: Backlog must contain at least one Story.',
446
455
  );
447
456
  }
448
457
 
449
- function assertHierarchy({ stories, ticketBySlug }) {
450
- for (const story of stories) {
451
- if (!story.parent_slug)
452
- throw new Error(
453
- `Cross-Validation Failed: Story "${story.title}" must have a parent_slug.`,
454
- );
455
- const parent = ticketBySlug.get(story.parent_slug);
456
- if (!parent || parent.type !== 'feature')
457
- throw new Error(
458
- `Cross-Validation Failed: Story "${story.title}" parent must be a Feature.`,
459
- );
460
- }
461
- }
462
-
463
- /**
464
- * Deterministic invariant (Story #3777): a Feature MUST decompose into at
465
- * least two Stories. A single-Story Feature is the work of a Story, not a
466
- * Feature — the Feature wrapper is dead weight and signals decomposition at
467
- * module/task granularity rather than deliverable granularity. HARD-reject
468
- * the decomposition, naming the offending Feature(s) and telling the planner
469
- * to collapse them, in the same throw-on-violation style as the surrounding
470
- * hierarchy invariants.
471
- */
472
- function assertNoSingleStoryFeature({ features, stories }) {
473
- const storyCountByParent = new Map();
474
- for (const story of stories) {
475
- if (!story.parent_slug) continue;
476
- storyCountByParent.set(
477
- story.parent_slug,
478
- (storyCountByParent.get(story.parent_slug) ?? 0) + 1,
479
- );
480
- }
481
- const undersized = features.filter(
482
- (feature) => (storyCountByParent.get(feature.slug) ?? 0) < 2,
483
- );
484
- if (undersized.length === 0) return;
485
- const list = undersized
486
- .map((feature) => {
487
- const count = storyCountByParent.get(feature.slug) ?? 0;
488
- return `"${feature.title}" (${feature.slug}, ${count} ${count === 1 ? 'Story' : 'Stories'})`;
489
- })
490
- .join(', ');
491
- throw new Error(
492
- `Cross-Validation Failed: ${undersized.length} Feature(s) decompose into fewer than two Stories: ${list}. ` +
493
- 'Every Feature MUST contain at least two Stories — a single-Story Feature is the work of a Story, not a Feature. ' +
494
- 'Collapse each offending Feature: drop the Feature wrapper and attach its lone Story to a sibling Feature, or merge the Feature into another.',
495
- );
496
- }
497
-
498
458
  /**
499
459
  * Return true when a Story object carries inline acceptance + verify
500
- * arrays — the 3-tier shape (Epic #3078) where the Story is itself the
460
+ * arrays — the inline-contract shape (Epic #3078) where the Story is itself the
501
461
  * implementation unit and acceptance / verify live on the Story body
502
462
  * rather than in child Task tickets.
503
463
  *
@@ -506,7 +466,7 @@ function assertNoSingleStoryFeature({ features, stories }) {
506
466
  * `acceptance[]` (no `verify[]`) cannot be implemented without a
507
467
  * verification handle, and a Story with only `verify[]` (no
508
468
  * `acceptance[]`) carries no observable criterion. Requiring both is the
509
- * inline-contract invariant every Story must satisfy in the 3-tier model.
469
+ * inline-contract invariant every Story must satisfy.
510
470
  */
511
471
  function hasInlineAcceptanceAndVerify(story) {
512
472
  if (story === null || typeof story !== 'object') return false;
@@ -520,7 +480,7 @@ function hasInlineAcceptanceAndVerify(story) {
520
480
  }
521
481
 
522
482
  function assertEveryStoryHasInlineContract({ stories }) {
523
- // 3-tier (Epic #3078 / #3238): every Story is its own implementation
483
+ // Every Story is its own implementation
524
484
  // unit and MUST carry a non-empty inline contract — top-level
525
485
  // `acceptance[]` AND `verify[]`. A Story missing either is the legacy
526
486
  // 4-tier shape that expected child Tasks; there is no Task tier any
@@ -577,12 +537,9 @@ function attachFindingsAndErrors(tickets, findings, errors) {
577
537
  }
578
538
 
579
539
  export function validateAndNormalizeTickets(tickets, opts = {}) {
580
- const { ticketBySlug, features, stories, slugAdjacency } =
581
- indexTicketsBySlug(tickets);
540
+ const { ticketBySlug, stories, slugAdjacency } = indexTicketsBySlug(tickets);
582
541
 
583
- assertEachTypePresent({ features, stories });
584
- assertHierarchy({ stories, ticketBySlug });
585
- assertNoSingleStoryFeature({ features, stories });
542
+ assertAllTicketsAreStories({ tickets, stories });
586
543
  assertEveryStoryHasInlineContract({ stories });
587
544
  assertNoUnknownDeps({ tickets, ticketBySlug });
588
545
 
@@ -680,9 +637,7 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
680
637
  // Internal helpers exposed for unit tests; not part of the public surface.
681
638
  export const _internal = {
682
639
  indexTicketsBySlug,
683
- assertEachTypePresent,
684
- assertHierarchy,
685
- assertNoSingleStoryFeature,
640
+ assertAllTicketsAreStories,
686
641
  assertEveryStoryHasInlineContract,
687
642
  assertNoUnknownDeps,
688
643
  assertAcyclic,
@@ -291,7 +291,7 @@ async function processCascadeParentLocked(
291
291
  if (!allDone) return { cascadedTo, failed };
292
292
 
293
293
  // EXCLUSION: Epics do not auto-close via cascade. Epics close via
294
- // formal /epic-deliver (its own machinery handles branch merges,
294
+ // formal /deliver (its own machinery handles branch merges,
295
295
  // PR-driven `Closes #N` auto-close, and a recovery transition in
296
296
  // `epic-deliver-finalize.js`).
297
297
  //
@@ -305,13 +305,6 @@ async function processCascadeParentLocked(
305
305
  // defense-in-depth path when a Story's tasklist references a
306
306
  // planning ticket directly.
307
307
  //
308
- // Features auto-close via cascade. A Feature is a purely
309
- // hierarchical grouping — no standalone branch, no merge step.
310
- // When its last child Story closes, the Feature is complete by
311
- // definition. Operators who need Feature-level AC verification
312
- // should encode it in the final child Story, not rely on a manual
313
- // close step.
314
- //
315
308
  // Reuse the parentSnapshot from the idempotency check above — it is
316
309
  // a fresh read (cache was invalidated before the getTicket call) and
317
310
  // the parent's type label is invariant within a single cascade lock
@@ -371,7 +364,7 @@ async function processCascadeParentLocked(
371
364
  * If yes, transitions parent to DONE and cascades up.
372
365
  *
373
366
  * Parents run strictly sequentially in input order (Story #4017 —
374
- * fan-out is <= 1 under the 3-tier hierarchy, so the former
367
+ * fan-out is <= 1 under the 2-tier hierarchy, so the former
375
368
  * shared-ancestor grouping / parallel dispatch was deleted); concurrent
376
369
  * transitions against a shared ancestor would race the "all children
377
370
  * done?" check. Within each parent, sibling reads fan out via
@@ -415,7 +408,7 @@ export async function cascadeCompletion(provider, ticketId, opts = {}) {
415
408
  // resume reconciler can strip the `parent: #N` orchestrator footer
416
409
  // from a Story body (see Issue 2 in #2982); without the body marker
417
410
  // the cascade silently returned `{ cascadedTo: [], failed: [] }` and
418
- // left intermediate Feature tickets stranded OPEN. The native link is
411
+ // left intermediate parent tickets stranded OPEN. The native link is
419
412
  // independent of body text, so consult it when the first two
420
413
  // strategies came back empty.
421
414
  if (
@@ -442,7 +435,7 @@ export async function cascadeCompletion(provider, ticketId, opts = {}) {
442
435
  return { cascadedTo: [], failed: [] };
443
436
  }
444
437
 
445
- // Story #4017 — under the 3-tier hierarchy a ticket has at most one
438
+ // Story #4017 — under the 2-tier hierarchy a ticket has at most one
446
439
  // parent, so the shared-ancestor grouping / parallel-group dispatch
447
440
  // machinery was deleted. Parents (fan-out <= 1
448
441
  // in practice; the loop stays general for the body-reference fallback)
@@ -557,7 +550,7 @@ export async function cascadeParentState(provider, ticketId, opts = {}) {
557
550
  if (parsedParents.length === 0) return { cascadedTo: [], failed: [] };
558
551
 
559
552
  // Story #4017 — sequential per-parent walk (fan-out <= 1 under the
560
- // 3-tier hierarchy); see cascadeCompletion for the rationale.
553
+ // 2-tier hierarchy); see cascadeCompletion for the rationale.
561
554
  const cascadedTo = [];
562
555
  const failed = [];
563
556
  for (const parentId of parsedParents) {
@@ -76,8 +76,8 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
76
76
  // downstream workflow steps don't have to infer install state from
77
77
  // node_modules presence.
78
78
  'story-init',
79
- // Story #908 — /story-deliver upserts a `story-run-progress` snapshot
80
- // on each Story per Task transition. The /epic-deliver aggregator and
79
+ // Story #908 — /deliver upserts a `story-run-progress` snapshot
80
+ // on each Story per Task transition. The /deliver aggregator and
81
81
  // the epic-runner progress reporter both read this comment to derive
82
82
  // Story-level state without re-fetching ticket labels.
83
83
  'story-run-progress',
@@ -98,7 +98,7 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
98
98
  // operator can correct drift before Phase 8 decomposes from a stale
99
99
  // spec. Advisory: the run continues regardless of the report contents.
100
100
  'spec-freshness',
101
- // Story #2681 — `/epic-deliver` Phase 4 epic-audit helper upserts an
101
+ // Story #2681 — `/deliver` Phase 4 epic-audit helper upserts an
102
102
  // `audit-results` comment on the Epic listing the per-lens findings
103
103
  // returned by the change-set audit pass. The marker was prescribed by
104
104
  // `helpers/epic-audit.md` Step 4 long before it was added to this
@@ -126,7 +126,7 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
126
126
  'epic-handoff',
127
127
  // Story #2899 (Epic #2880, F13) — `epic-deliver-preflight.js` upserts a
128
128
  // `delivery-preflight` comment on the Epic at the start of
129
- // /epic-deliver Phase 1, surfacing estimated story count, install cost,
129
+ // /deliver Phase 1, surfacing estimated story count, install cost,
130
130
  // wave count, GitHub API request volume, Claude quota burn, and any
131
131
  // threshold breaches against `delivery.preflight.max*`. One entry per
132
132
  // Epic; re-runs replace prior content.
@@ -137,7 +137,7 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
137
137
  // `close-validate.end`. One entry per Epic; re-ticks with the same
138
138
  // findings upsert in place (`upsertStructuredComment` diffs by body).
139
139
  'recurring-failure-class',
140
- // Story #3061 (Epic #3051) — the /epic-deliver §2e Idle Watchdog
140
+ // Story #3061 (Epic #3051) — the /deliver §2e Idle Watchdog
141
141
  // subsection instructs the parent host LLM to upsert a `wave-stall`
142
142
  // comment on the Epic whenever an in-flight Story has been silent for
143
143
  // longer than the configured cadence. `wave-tick.js --check-idle`
@@ -153,7 +153,7 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
153
153
  'risk-verdict',
154
154
  // Story #4019 — `epic-plan-lease-guard.js` upserts a `plan-lease`
155
155
  // comment on the Epic at lease-acquire time, recording the claiming
156
- // operator and the claim timestamp. `/epic-plan` emits no
156
+ // operator and the claim timestamp. `/plan` emits no
157
157
  // `story.heartbeat`, so this claim-time is the liveness signal that
158
158
  // makes the documented `--steal` contract decidable: a foreign claim
159
159
  // older than the lease TTL is reclaimed automatically; a fresh one
@@ -268,8 +268,8 @@ export const _structuredCommentCache = new WeakMap();
268
268
  /**
269
269
  * Build a well-formed ticket snapshot for a Story that has zero child
270
270
  * Tasks. Story #3097 (Wave-0 additive, Epic #3078 Strategy B) — the
271
- * 3-tier hierarchy collapses Epic → Feature → Story → Task into
272
- * Epic → Feature → Story, so a Story may legitimately have no Task
271
+ * 2-tier hierarchy collapses Epic → Story → Task into
272
+ * Epic → Story, so a Story may legitimately have no Task
273
273
  * children. Read-side callers that expect a `subTickets` array on the
274
274
  * Story can route through this helper to materialise an empty-children
275
275
  * snapshot without paying a provider round-trip and without risk of
@@ -66,13 +66,13 @@ registerCascadeRunner(async (provider, ticketId, opts) => {
66
66
  /**
67
67
  * Transition a Story ticket directly to a new `agent::*` state without
68
68
  * walking a Task cascade. Story #3097 (Wave-0 additive, Epic #3078
69
- * Strategy B) — in the 3-tier hierarchy a Story has no Task children, so
69
+ * Strategy B) — in the 2-tier hierarchy a Story has no Task children, so
70
70
  * the canonical `transitionTicketState` upward-cascade path
71
71
  * (`cascadeParentState`) is the only meaningful walk. This helper is a
72
- * thin wrapper that pins `cascade: true` (so the parent Feature/Epic
72
+ * thin wrapper that pins `cascade: true` (so the parent Epic
73
73
  * still receives derived-state updates) and is intentionally a no-op
74
74
  * difference from `transitionTicketState` in 4-tier mode — the helper
75
- * exists so 3-tier callers can opt into a name that documents intent
75
+ * exists so 2-tier callers can opt into a name that documents intent
76
76
  * (and so F8 can pivot the implementation to skip the now-impossible
77
77
  * Task-fan-in without rewriting call sites). The wrapper preserves every
78
78
  * `opts` field the caller supplies; only `cascade` defaults to `true`
@@ -3,7 +3,7 @@
3
3
  * `epic-execute-record-wave.js`.
4
4
  *
5
5
  * The CLI fires curated webhook events at every wave boundary (started,
6
- * progress, blocked, unblocked) so the host-LLM-driven `/epic-deliver` path
6
+ * progress, blocked, unblocked) so the host-LLM-driven `/deliver` path
7
7
  * mirrors the wave-loop emits in
8
8
  * `lib/orchestration/epic-runner/phases/iterate-waves.js`. Each helper here
9
9
  * is fire-and-forget — webhook misconfig or a transient Slack outage must
@@ -45,7 +45,7 @@ export function buildNotifyFn(injectedNotify, config, provider, defaultNotify) {
45
45
  * fire-and-forget (the emit helpers swallow webhook misconfiguration), but
46
46
  * we still serialise them so the order matches the wave-loop emits in
47
47
  * `lib/orchestration/epic-runner/phases/iterate-waves.js` for the host-LLM
48
- * driven /epic-deliver path.
48
+ * driven /deliver path.
49
49
  */
50
50
  export async function emitWaveBoundaryNotifications({
51
51
  injectedNotify,
@@ -32,7 +32,7 @@ import { parseStoryAgentReturn } from './epic-runner/sub-agent-return.js';
32
32
  /** Valid wave-level rollup statuses. */
33
33
  export const VALID_RESULT_STATUSES = new Set(['complete', 'blocked', 'failed']);
34
34
 
35
- /** Per-Story return statuses we accept off `/story-deliver` sub-agents. */
35
+ /** Per-Story return statuses we accept off `/deliver` sub-agents. */
36
36
  export const VALID_STORY_STATUSES = new Set(['done', 'blocked', 'failed']);
37
37
 
38
38
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * plan-phase-cleanup.js — Post-phase temp-file cleanup for `/epic-plan`.
2
+ * plan-phase-cleanup.js — Post-phase temp-file cleanup for `/plan`.
3
3
  *
4
4
  * The spec and decompose phases write several Epic-scoped temp files under
5
5
  * the per-Epic tree (`temp/epic-<id>/planner-context.json`,
@@ -13,7 +13,7 @@
13
13
  * auto-fixes. The wrapper prints a human-readable blocker table
14
14
  * (`id · summary · fixCommand`) before returning. Code 2 is the
15
15
  * project-wide "preflight refused" reservation — see
16
- * .agents/workflows/epic-deliver.md for the rationale.
16
+ * .agents/workflows/helpers/deliver-epic.md for the rationale.
17
17
  *
18
18
  * Auto-fixes are logged via `logFixes` before the blocker check so the
19
19
  * operator sees the "we corrected X" line even when a separate blocker
@@ -12,9 +12,8 @@
12
12
 
13
13
  /**
14
14
  * Pure: project a full dispatch manifest into the `{ stories }` shape
15
- * `renderManifest` accepts. Returns the canonical, non-feature,
16
- * non-ungrouped story rows used by the Epic-level dispatch-manifest
17
- * comment.
15
+ * `renderManifest` accepts. Returns the canonical, non-ungrouped story
16
+ * rows used by the Epic-level dispatch-manifest comment.
18
17
  *
19
18
  * @param {object} manifest
20
19
  * @returns {{ storyId: number|string, wave: number, title: string }[]}
@@ -22,7 +21,7 @@
22
21
  export function projectStoriesFromManifest(manifest) {
23
22
  const storyManifest = manifest?.storyManifest ?? [];
24
23
  return storyManifest
25
- .filter((s) => s && s.type !== 'feature' && s.storyId !== '__ungrouped__')
24
+ .filter((s) => s && s.storyId !== '__ungrouped__')
26
25
  .map((s) => ({
27
26
  storyId: s.storyId,
28
27
  wave: s.earliestWave ?? -1,
@@ -71,7 +70,7 @@ export function renderManifest({ epicId, stories, generatedAt }) {
71
70
  `- **Stories:** ${list.length}`,
72
71
  `- **Generated:** ${generatedAt}`,
73
72
  '',
74
- 'Source of truth for the wave-completeness gate run at `/epic-deliver`.',
73
+ 'Source of truth for the wave-completeness gate run at `/deliver`.',
75
74
  '',
76
75
  '```json',
77
76
  JSON.stringify({ stories: list }, null, 2),
@@ -9,19 +9,19 @@
9
9
  * Extracted from `manifest-formatter.js` (Story #1849 Task #1869). The
10
10
  * shape projection used to be inlined in the formatter; pulling it out
11
11
  * isolates the spec → manifest projection from the Markdown renderer and
12
- * lets the per-feature / per-story guard cascade live behind a single
12
+ * lets the per-story guard cascade live behind a single
13
13
  * private predicate (`validateSpecShape`) so the orchestrator function's
14
14
  * CRAP score drops below 12.
15
15
  *
16
- * Story #3413 (3-tier cutover, final): the residual per-Task projection
16
+ * Story #3413 (2-tier cutover, final): the residual per-Task projection
17
17
  * (the Task projector, the per-Story Task array, and the Task-count
18
- * rollup) has been deleted. The walker (`projectFeatures`) now counts
18
+ * rollup) has been deleted. The walker (`projectStories`) counts
19
19
  * Stories directly, and `summary` carries Story-tier counts only
20
20
  * (`totalStories` / `doneStories` / `progressPercent`), matching the
21
21
  * canonical producer in `lib/orchestration/manifest-builder.js`.
22
22
  *
23
23
  * Story-level status surfaces on each `storyEntry.status` so downstream
24
- * renderers read the Story's own `agent::*` label directly — the 3-tier
24
+ * renderers read the Story's own `agent::*` label directly — the
25
25
  * "Stories are first-class lifecycle units" invariant.
26
26
  *
27
27
  * No fs / network access; pure transform. Caller supplies `state` from
@@ -38,8 +38,7 @@ import { AGENT_LABELS } from '../label-constants.js';
38
38
  * "is this thing iterable / object-shaped?" decisions.
39
39
  *
40
40
  * `level` describes which spec node we are validating:
41
- * - `'features'` → the spec-level `features` array
42
- * - `'stories'` → a feature's `stories` array
41
+ * - `'stories'` → the spec-level `stories` array
43
42
  * - `'story'` → a single Story object (must be a non-null object)
44
43
  *
45
44
  * Returns `true` when the node satisfies the shape contract for that
@@ -52,7 +51,6 @@ import { AGENT_LABELS } from '../label-constants.js';
52
51
  */
53
52
  function validateSpecShape(level, value) {
54
53
  switch (level) {
55
- case 'features':
56
54
  case 'stories':
57
55
  return Array.isArray(value);
58
56
  case 'story':
@@ -102,8 +100,8 @@ function buildResolvers(state) {
102
100
  * Private: project a single spec Story into a manifest Story entry. The
103
101
  * Story's status is resolved directly from the Story-level label
104
102
  * (`state.mapping[slug].lastObservedAgentState`) and surfaces on
105
- * `storyEntry.status` — under the 3-tier hierarchy Stories carry their
106
- * own lifecycle state and are leaves with no child Task tickets. Caller
103
+ * `storyEntry.status` — Stories carry their own lifecycle state and are
104
+ * leaves with no child tickets. Caller
107
105
  * filters non-object stories with `validateSpecShape('story', ...)`
108
106
  * before invoking.
109
107
  *
@@ -129,16 +127,17 @@ function projectStory(story, resolvers) {
129
127
  }
130
128
 
131
129
  /**
132
- * Private: walk every feature → story pair in a spec and collect the
133
- * per-story projections + Story-tier roll-up counters. Keeps the loop
134
- * machinery out of `buildManifestFromSpec` so the entry point reads as a
135
- * straight assembly of the result envelope.
130
+ * Private: walk every Story in a spec and collect the per-story
131
+ * projections + Story-tier roll-up counters. Keeps the loop machinery
132
+ * out of `buildManifestFromSpec` so the entry point reads as a straight
133
+ * assembly of the result envelope.
136
134
  *
137
- * Under the 3-tier hierarchy (Epic #3163) Stories are leaves, so the
138
- * rollup counts Stories directly: `totalStories` is every projected
139
- * Story and `doneStories` is the subset carrying `agent::done`.
135
+ * Under the 2-tier hierarchy (Story #4041) Stories are direct Epic
136
+ * children and leaves, so the rollup counts Stories directly:
137
+ * `totalStories` is every projected Story and `doneStories` is the
138
+ * subset carrying `agent::done`.
140
139
  *
141
- * @param {object[]} features
140
+ * @param {object[]} stories
142
141
  * @param {{ resolveId: Function, resolveStatus: Function }} resolvers
143
142
  * @returns {{
144
143
  * storyManifest: object[],
@@ -147,23 +146,18 @@ function projectStory(story, resolvers) {
147
146
  * waveSet: Set<number>,
148
147
  * }}
149
148
  */
150
- function projectFeatures(features, resolvers) {
149
+ function projectStories(stories, resolvers) {
151
150
  const storyManifest = [];
152
151
  let totalStories = 0;
153
152
  let doneStories = 0;
154
153
  const waveSet = new Set();
155
- for (const feature of features) {
156
- const stories = validateSpecShape('stories', feature?.stories)
157
- ? feature.stories
158
- : [];
159
- for (const story of stories) {
160
- if (!validateSpecShape('story', story)) continue;
161
- const { storyEntry, wave } = projectStory(story, resolvers);
162
- storyManifest.push(storyEntry);
163
- totalStories++;
164
- if (storyEntry.status === AGENT_LABELS.DONE) doneStories++;
165
- if (wave >= 0) waveSet.add(wave);
166
- }
154
+ for (const story of stories) {
155
+ if (!validateSpecShape('story', story)) continue;
156
+ const { storyEntry, wave } = projectStory(story, resolvers);
157
+ storyManifest.push(storyEntry);
158
+ totalStories++;
159
+ if (storyEntry.status === AGENT_LABELS.DONE) doneStories++;
160
+ if (wave >= 0) waveSet.add(wave);
167
161
  }
168
162
  return { storyManifest, totalStories, doneStories, waveSet };
169
163
  }
@@ -199,12 +193,12 @@ export function buildManifestFromSpec(spec, opts = {}) {
199
193
  spec?.epic && typeof spec.epic.id === 'number' ? spec.epic.id : null;
200
194
  const epicTitle =
201
195
  spec?.epic && typeof spec.epic.title === 'string' ? spec.epic.title : '';
202
- const features = validateSpecShape('features', spec?.features)
203
- ? spec.features
196
+ const stories = validateSpecShape('stories', spec?.stories)
197
+ ? spec.stories
204
198
  : [];
205
199
 
206
- const { storyManifest, totalStories, doneStories, waveSet } = projectFeatures(
207
- features,
200
+ const { storyManifest, totalStories, doneStories, waveSet } = projectStories(
201
+ stories,
208
202
  resolvers,
209
203
  );
210
204