mandrel 2.31.0 → 2.32.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 (250) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +0 -1
  14. package/.agents/schemas/agentrc.schema.json +1939 -1400
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +7 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/bootstrap.js +28 -26
  21. package/.agents/scripts/check-baseline-drift.js +73 -13
  22. package/.agents/scripts/check-baseline-scope.js +362 -0
  23. package/.agents/scripts/check-dead-exports.js +9 -1
  24. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  25. package/.agents/scripts/check-knip-entries.js +136 -0
  26. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  27. package/.agents/scripts/check-schema-references.js +1 -1
  28. package/.agents/scripts/diagnose-friction.js +7 -4
  29. package/.agents/scripts/generate-config-docs.js +263 -171
  30. package/.agents/scripts/install-matrix-assert.js +0 -1
  31. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  32. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  33. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  34. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  35. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  37. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  38. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  39. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  40. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  41. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  42. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  43. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  44. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  45. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  46. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  47. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  48. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  49. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  50. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  51. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  52. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  53. package/.agents/scripts/lib/child-exec.js +193 -0
  54. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  55. package/.agents/scripts/lib/cli-args.js +64 -0
  56. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  57. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  58. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  59. package/.agents/scripts/lib/config/commands.js +3 -5
  60. package/.agents/scripts/lib/config/explain.js +5 -7
  61. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  62. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  63. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  64. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  65. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  66. package/.agents/scripts/lib/config/gates/index.js +5 -2
  67. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  68. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  69. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  70. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  71. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  72. package/.agents/scripts/lib/config/github.js +0 -1
  73. package/.agents/scripts/lib/config/limits.js +3 -48
  74. package/.agents/scripts/lib/config/qa.js +105 -0
  75. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  76. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  77. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  78. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  79. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  80. package/.agents/scripts/lib/crap-utils.js +84 -520
  81. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  82. package/.agents/scripts/lib/degraded-mode.js +2 -2
  83. package/.agents/scripts/lib/doc-tiers.js +3 -3
  84. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  85. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  86. package/.agents/scripts/lib/fs-walk.js +52 -0
  87. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  88. package/.agents/scripts/lib/git-utils.js +16 -36
  89. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  90. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  91. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  92. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  93. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  94. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  95. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  96. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  97. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  98. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  99. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  100. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  101. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  102. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  103. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  104. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  105. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  106. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  107. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  108. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  109. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  110. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  111. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  112. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  122. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  123. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  124. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  125. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  126. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  127. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  128. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  129. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  130. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  131. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  132. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  133. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  134. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  135. package/.agents/scripts/lib/signals/index.js +8 -6
  136. package/.agents/scripts/lib/signals/schema.js +20 -25
  137. package/.agents/scripts/lib/signals/write.js +8 -8
  138. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  139. package/.agents/scripts/lib/temp-retention.js +1 -1
  140. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  141. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  142. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  143. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  144. package/.agents/scripts/lib/worktree-manager.js +2 -3
  145. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  146. package/.agents/scripts/pr-watch-with-update.js +7 -5
  147. package/.agents/scripts/providers/github/cache.js +2 -2
  148. package/.agents/scripts/providers/github/comments.js +6 -28
  149. package/.agents/scripts/providers/github/compose.js +0 -15
  150. package/.agents/scripts/providers/github/errors.js +10 -27
  151. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  152. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  153. package/.agents/scripts/providers/github.js +4 -7
  154. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  155. package/.agents/scripts/resolve-stories.js +0 -2
  156. package/.agents/scripts/run-lint.js +61 -61
  157. package/.agents/scripts/run-test-profile.js +6 -6
  158. package/.agents/scripts/run-verify.js +48 -30
  159. package/.agents/scripts/single-story-close.js +20 -0
  160. package/.agents/scripts/single-story-init.js +12 -35
  161. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  162. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  163. package/.agents/skills/skills.index.json +1 -11
  164. package/.agents/workflows/audit-documentation.md +5 -6
  165. package/.agents/workflows/audit-to-stories.md +2 -2
  166. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  167. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  168. package/.agents/workflows/helpers/code-review.md +3 -8
  169. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  170. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  171. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  172. package/.agents/workflows/mandrel-update.md +10 -10
  173. package/.agents/workflows/qa-assist.md +15 -20
  174. package/.agents/workflows/qa-explore.md +9 -8
  175. package/README.md +1 -1
  176. package/docs/CHANGELOG.md +42 -0
  177. package/lib/migrations/index.js +2 -0
  178. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  179. package/package.json +12 -3
  180. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  181. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  182. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  183. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  184. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  185. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  186. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  187. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  188. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  189. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  190. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  191. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  192. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  193. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  194. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  195. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  196. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  197. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  198. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  199. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  200. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  201. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  202. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  203. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  204. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  205. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  206. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  207. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  208. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  209. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  210. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  211. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  212. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  213. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  214. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  215. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  216. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  217. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  218. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  219. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  220. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  221. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  222. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  223. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  224. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  225. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  226. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  227. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  228. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  229. package/.agents/scripts/lib/planning-corpus.js +0 -37
  230. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  231. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  232. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  233. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  234. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  235. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  236. package/.agents/scripts/lib/signals/read.js +0 -268
  237. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  238. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  239. package/.agents/scripts/lib/story-plan.js +0 -379
  240. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  241. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  242. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  243. package/.agents/scripts/lint-baseline.js +0 -507
  244. package/.agents/scripts/providers/github/prs.js +0 -103
  245. package/.agents/scripts/signals-view.js +0 -309
  246. package/.agents/scripts/story-plan.js +0 -370
  247. package/.agents/scripts/sync-branch-from-base.js +0 -149
  248. package/.agents/scripts/validate-docs-freshness.js +0 -314
  249. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  250. package/.agents/workflows/helpers/signals.md +0 -112
@@ -1,379 +0,0 @@
1
- /**
2
- * story-plan.js — helpers for `/plan`.
3
- *
4
- * Pure functions used by `.agents/scripts/story-plan.js` (the CLI).
5
- * Kept side-effect-free so the CLI stays a thin orchestrator and these
6
- * helpers are easy to unit-test.
7
- *
8
- * Surfaces:
9
- * - DEFAULT_REFINE_THRESHOLD — seed length below which refinement
10
- * is auto-suggested.
11
- * - REQUIRED_SECTIONS — canonical section headings the body
12
- * must carry itself. Story #4874: the
13
- * `## Acceptance` / `## Verify` sections
14
- * are deliberately NOT among them —
15
- * those lists are the ticket's top-level
16
- * machine contract and persist
17
- * synthesizes their sections, exactly as
18
- * the story-author prompt instructs.
19
- * - rankDuplicateCandidates({ seed, openStories, maxResults })
20
- * — Jaccard-overlap ranking of open
21
- * Stories whose titles fuzzy-match
22
- * the seed.
23
- * - validateStoryBody(body, contract)
24
- * — schema-light shape check against the
25
- * canonical Story-body contract: `##
26
- * Goal` / `## Changes` present, no
27
- * `Epic:` reference, and an
28
- * acceptance/verify contract resolvable
29
- * from the top-level arrays or the body.
30
- * - buildContextEnvelope(opts) — assemble the context envelope the
31
- * host LLM consumes.
32
- */
33
-
34
- import { readFile } from 'node:fs/promises';
35
- import path from 'node:path';
36
- import { overlapScore, tokenize } from './duplicate-search.js';
37
- import { parse as parseStoryBody } from './story-body/story-body.js';
38
-
39
- export const DEFAULT_REFINE_THRESHOLD = 200;
40
- export const DEFAULT_DUPLICATE_MAX_RESULTS = 5;
41
- export const DEFAULT_DUPLICATE_MIN_SCORE = 0.15;
42
-
43
- /**
44
- * The sections the authored body must carry itself. Story #4874 reconciled
45
- * this list with the story-author prompt: the prompt tells the author to
46
- * write `acceptance[]` / `verify[]` **once** at the ticket's top level and
47
- * omit the matching body sections, so demanding those sections here made a
48
- * prompt-faithful body unpersistable and cost a re-author round.
49
- */
50
- export const REQUIRED_SECTIONS = ['Goal', 'Changes'];
51
-
52
- /** The contract lists persist synthesizes into the body from the top level. */
53
- const CONTRACT_FIELDS = /** @type {const} */ (['acceptance', 'verify']);
54
-
55
- const EPIC_REF_PATTERN = /^\s*Epic:\s*#\d+/m;
56
-
57
- /**
58
- * Rank open Stories by title-overlap with the seed. Reuses the same
59
- * tokenize + Jaccard primitives that `duplicate-search.js` exposes for
60
- * Epic-level dedupe — Stories are a different ticket type but the
61
- * scoring shape is the same.
62
- *
63
- * @param {{ seed: string, openStories: Array<{ id:number, title:string, url?:string, body?:string }>, maxResults?: number, minScore?: number }} opts
64
- * @returns {Array<{ id:number, title:string, url?:string, score:number }>}
65
- */
66
- export function rankDuplicateCandidates({
67
- seed,
68
- openStories,
69
- maxResults = DEFAULT_DUPLICATE_MAX_RESULTS,
70
- minScore = DEFAULT_DUPLICATE_MIN_SCORE,
71
- }) {
72
- if (!seed || typeof seed !== 'string') {
73
- throw new Error('rankDuplicateCandidates: seed must be a non-empty string');
74
- }
75
- if (!Array.isArray(openStories)) {
76
- throw new Error('rankDuplicateCandidates: openStories must be an array');
77
- }
78
- const seedTokens = tokenize(seed);
79
- if (seedTokens.size === 0) return [];
80
-
81
- const ranked = [];
82
- for (const story of openStories) {
83
- if (!story || typeof story.title !== 'string') continue;
84
- const corpus = `${story.title}\n${story.body ?? ''}`;
85
- const candTokens = tokenize(corpus);
86
- const score = overlapScore(seedTokens, candTokens);
87
- if (score >= minScore) {
88
- ranked.push({
89
- id: story.id,
90
- title: story.title,
91
- url: story.url,
92
- score: Number(score.toFixed(4)),
93
- });
94
- }
95
- }
96
- ranked.sort((a, b) => b.score - a.score);
97
- return ranked.slice(0, maxResults);
98
- }
99
-
100
- /**
101
- * Decide whether the seed warrants a Phase-2 idea-refinement pass.
102
- * The default rule: refine when the seed is shorter than
103
- * `DEFAULT_REFINE_THRESHOLD` chars. Operator overrides via
104
- * `--refine` / `--no-refine` short-circuit the heuristic.
105
- *
106
- * @param {{ seed:string, override?:'on'|'off'|null, threshold?:number }} opts
107
- * @returns {{ refine:boolean, reason:string }}
108
- */
109
- export function shouldRefine({
110
- seed,
111
- override = null,
112
- threshold = DEFAULT_REFINE_THRESHOLD,
113
- }) {
114
- if (override === 'on') return { refine: true, reason: 'operator-forced-on' };
115
- if (override === 'off')
116
- return { refine: false, reason: 'operator-forced-off' };
117
- if (typeof seed !== 'string' || seed.trim().length === 0) {
118
- return { refine: true, reason: 'empty-seed' };
119
- }
120
- if (seed.trim().length < threshold) {
121
- return {
122
- refine: true,
123
- reason: `seed-shorter-than-${threshold}-chars`,
124
- };
125
- }
126
- return { refine: false, reason: `seed-meets-${threshold}-char-threshold` };
127
- }
128
-
129
- /**
130
- * Collect the violations for one contract list (`acceptance` / `verify`).
131
- *
132
- * The list may be authored at the ticket's top level (the shape the
133
- * story-author prompt prescribes) or written into the body as a section —
134
- * either satisfies the contract, and the top level is preferred because
135
- * persist synthesizes the section from it. Only two shapes are violations:
136
- * the list is nowhere, or it is in both places and the two disagree (fail
137
- * closed rather than guess which is authoritative, mirroring
138
- * `plan-persist`'s `syncContractFieldFromTopLevel`).
139
- *
140
- * @param {'acceptance'|'verify'} field
141
- * @param {unknown} bodyList The parsed body's section entries.
142
- * @param {unknown} topLevel The ticket's top-level array.
143
- * @returns {string[]}
144
- */
145
- function collectContractErrors(field, bodyList, topLevel) {
146
- const inBody = (Array.isArray(bodyList) ? bodyList : []).map(String);
147
- const inTicket = (Array.isArray(topLevel) ? topLevel : []).map(String);
148
- if (inBody.length === 0 && inTicket.length === 0) {
149
- return [
150
- `${field} must list at least one entry — author it as the ticket's top-level ${field}[] array; persist synthesizes the "## ${field[0].toUpperCase()}${field.slice(1)}" section from it`,
151
- ];
152
- }
153
- if (
154
- inBody.length > 0 &&
155
- inTicket.length > 0 &&
156
- (inBody.length !== inTicket.length ||
157
- inBody.some((v, i) => v !== inTicket[i]))
158
- ) {
159
- return [
160
- `${field} disagrees between the body section and the top-level ${field}[] array — author it once, at the top level`,
161
- ];
162
- }
163
- return [];
164
- }
165
-
166
- /**
167
- * Schema-light validator for a standalone-Story body, stated against the
168
- * same authoring shape the story-author prompt prescribes (Story #4874).
169
- *
170
- * It parses the body with the canonical Story-body parser and asserts:
171
- * a non-empty `## Goal`, at least one `## Changes` entry, no leaking
172
- * `Epic: #N` reference (the standalone contract), and an acceptance +
173
- * verify contract resolvable from the ticket's top-level arrays **or** the
174
- * body's own sections. It deliberately does not demand the `## Acceptance`
175
- * / `## Verify` sections of the author — persist synthesizes them.
176
- *
177
- * @param {string} body
178
- * @param {{ acceptance?: string[], verify?: string[] }} [contract]
179
- * The ticket's top-level contract arrays, when the caller has them.
180
- * @returns {{ ok:boolean, errors:string[] }}
181
- */
182
- export function validateStoryBody(body, contract = {}) {
183
- if (typeof body !== 'string' || body.trim().length === 0) {
184
- return { ok: false, errors: ['body is empty'] };
185
- }
186
-
187
- let parsed;
188
- try {
189
- parsed = parseStoryBody(body).body;
190
- } catch (err) {
191
- return {
192
- ok: false,
193
- errors: [`body is not a parseable Story body: ${err.message}`],
194
- };
195
- }
196
-
197
- const errors = [];
198
- if (typeof parsed.goal !== 'string' || parsed.goal.trim() === '') {
199
- errors.push('missing required section: "## Goal"');
200
- }
201
- if (!Array.isArray(parsed.changes) || parsed.changes.length === 0) {
202
- errors.push('missing required section: "## Changes"');
203
- }
204
- if (EPIC_REF_PATTERN.test(body)) {
205
- errors.push(
206
- 'body contains an "Epic: #N" reference — standalone Stories must not link to an Epic',
207
- );
208
- }
209
- for (const field of CONTRACT_FIELDS) {
210
- errors.push(
211
- ...collectContractErrors(field, parsed[field], contract?.[field]),
212
- );
213
- }
214
-
215
- return { ok: errors.length === 0, errors };
216
- }
217
-
218
- /**
219
- * Render the canonical `## Acceptance` / `## Verify` sections into a body
220
- * that omits them, from the ticket's top-level arrays (Story #4874).
221
- *
222
- * This is the synthesis the story-author prompt promises: the author writes
223
- * each list once at top level, and the persisted GitHub issue still reads as
224
- * a complete executable document. A body that already carries a section is
225
- * returned untouched — `validateStoryBody` has already refused the case
226
- * where the two disagree.
227
- *
228
- * @param {string} body
229
- * @param {{ acceptance?: string[], verify?: string[] }} [contract]
230
- * @returns {string}
231
- */
232
- export function synthesizeContractSections(body, contract = {}) {
233
- let parsed;
234
- try {
235
- parsed = parseStoryBody(body).body;
236
- } catch {
237
- return body;
238
- }
239
- const blocks = [];
240
- if (
241
- (parsed.acceptance ?? []).length === 0 &&
242
- Array.isArray(contract.acceptance) &&
243
- contract.acceptance.length > 0
244
- ) {
245
- const items = contract.acceptance
246
- .map((a, i) => `- [ ] AC-${i + 1}: ${a}`)
247
- .join('\n');
248
- blocks.push(`## Acceptance\n${items}`);
249
- }
250
- if (
251
- (parsed.verify ?? []).length === 0 &&
252
- Array.isArray(contract.verify) &&
253
- contract.verify.length > 0
254
- ) {
255
- blocks.push(
256
- `## Verify\n${contract.verify.map((v) => `- ${v}`).join('\n')}`,
257
- );
258
- }
259
- if (blocks.length === 0) return body;
260
- return `${body.trimEnd()}\n\n${blocks.join('\n\n')}\n`;
261
- }
262
-
263
- /**
264
- * Load the body-template file from disk. Resolves relative to a project
265
- * root so callers (CLI, tests) can pin the lookup deterministically.
266
- *
267
- * @param {string} projectRoot
268
- * @returns {Promise<string>}
269
- */
270
- export async function loadBodyTemplate(projectRoot) {
271
- const tpl = path.join(
272
- projectRoot,
273
- '.agents',
274
- 'templates',
275
- 'single-story-body.md',
276
- );
277
- return readFile(tpl, 'utf8');
278
- }
279
-
280
- /**
281
- * Assemble the context envelope the host LLM consumes to author a draft
282
- * Story body. Pure — no I/O beyond what the caller passed in.
283
- *
284
- * @param {{
285
- * seed: string,
286
- * refine: { refine:boolean, reason:string },
287
- * persona: string,
288
- * bodyTemplate: string,
289
- * duplicateCandidates: Array<object>,
290
- * techStack?: string|null,
291
- * corpusContext?: { docsDigest: string|null, relevantSections: Array<object> }|null,
292
- * maxResults?: number,
293
- * }} opts
294
- */
295
- export function buildContextEnvelope({
296
- seed,
297
- refine,
298
- bodyTemplate,
299
- duplicateCandidates,
300
- techStack = null,
301
- corpusContext = null,
302
- maxResults = DEFAULT_DUPLICATE_MAX_RESULTS,
303
- }) {
304
- return {
305
- kind: 'story-plan-context',
306
- version: 1,
307
- seed,
308
- refine,
309
- bodyTemplate,
310
- requiredSections: [...REQUIRED_SECTIONS],
311
- duplicateCandidates: {
312
- maxResults,
313
- candidates: duplicateCandidates,
314
- },
315
- techStack,
316
- corpusContext,
317
- deliverContract: {
318
- workflow: '.agents/workflows/helpers/deliver-story.md',
319
- requiredLabels: ['type::story'],
320
- forbidden: ['Epic: #N references in the body'],
321
- },
322
- };
323
- }
324
-
325
- /**
326
- * Extract the "Tech Stack" `##` section from a markdown document.
327
- *
328
- * Tolerates a numbered / decorated heading (`## 1. Tech Stack`,
329
- * `## Tech Stack`, etc.) and a section that is the final `##` in the
330
- * file (the terminator matches the next `##` heading **or** end-of-file).
331
- * Returns the matched section text (re-headed to a clean `## Tech Stack`)
332
- * or `null` when no Tech Stack heading is present.
333
- *
334
- * @param {string} content
335
- * @returns {string|null}
336
- */
337
- function extractTechStackSection(content) {
338
- const match = content.match(
339
- /^##\s+(?:\d+[.)]\s+)?Tech Stack\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/m,
340
- );
341
- return match ? `## Tech Stack${match[1]}`.trim() : null;
342
- }
343
-
344
- /**
345
- * Resolve the project's Tech Stack inventory for the host LLM, in order:
346
- *
347
- * 1. A dedicated `docs/tech-stack.md` when present (the emerging
348
- * single-ownership convention — WHAT in tech-stack.md, HOW in
349
- * architecture.md, WHY in the ADRs). Its full body is returned.
350
- * 2. Otherwise, the `## Tech Stack` section of `docs/architecture.md`,
351
- * tolerating a numbered/decorated heading and a final-section
352
- * heading (no following `##` required).
353
- *
354
- * Returns `null` when neither source yields an inventory.
355
- *
356
- * @param {string} projectRoot
357
- * @returns {Promise<string|null>}
358
- */
359
- export async function readTechStackSummary(projectRoot) {
360
- const dedicatedPath = path.join(projectRoot, 'docs', 'tech-stack.md');
361
- try {
362
- const dedicated = await readFile(dedicatedPath, 'utf8');
363
- const trimmed = dedicated.trim();
364
- if (trimmed) {
365
- return trimmed;
366
- }
367
- } catch {
368
- // No dedicated tech-stack.md — fall through to architecture.md.
369
- }
370
-
371
- const archPath = path.join(projectRoot, 'docs', 'architecture.md');
372
- let content;
373
- try {
374
- content = await readFile(archPath, 'utf8');
375
- } catch {
376
- return null;
377
- }
378
- return extractTechStackSection(content);
379
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * lib/util/phase-timer-state.js — Cross-process persistence for phase-timer
3
- *
4
- * A Story Mode run spans two Node processes — `story-init` creates the
5
- * timer and opens the `implement` phase; the operator (or a headless
6
- * `claude -p` sub-agent) does the work; `story-close` reads the
7
- * timer back, appends `lint` / `test` / `close` / `api-sync` marks, and
8
- * emits the `phase-timings` structured comment.
9
- *
10
- * Snapshots land in the main-repo `.git/` (which every process in the story
11
- * lifecycle can see — init writes from the main checkout, close invokes with
12
- * `--cwd <main-repo>` per the worktree contract). `.git/` is deliberately
13
- * chosen over the worktree root so the file is never accidentally checked
14
- * into the story branch and never ships in a PR.
15
- */
16
-
17
- import fs from 'node:fs';
18
- import path from 'node:path';
19
-
20
- function filePath(mainCwd, storyId) {
21
- return path.join(mainCwd, '.git', `story-${storyId}-phase-timer.json`);
22
- }
23
-
24
- /**
25
- * Persist the current timer snapshot to disk. Writes atomically via a
26
- * temp-file rename so a crash mid-write cannot leave a truncated JSON file
27
- * that the close process would fail to parse.
28
- *
29
- * @param {{ snapshot: () => object }} timer
30
- * @param {{ mainCwd: string, storyId: number }} opts
31
- */
32
- export function savePhaseTimerState(timer, { mainCwd, storyId }) {
33
- const target = filePath(mainCwd, storyId);
34
- const tmp = `${target}.tmp`;
35
- fs.writeFileSync(tmp, JSON.stringify(timer.snapshot(), null, 2), 'utf8');
36
- fs.renameSync(tmp, target);
37
- }
38
-
39
- /**
40
- * Load a timer snapshot from disk if present. Returns `null` when the file
41
- * is missing or unparseable — both cases are treated as "no prior timer",
42
- * which lets close fall back to creating a fresh timer and emitting only
43
- * the phases it observes.
44
- *
45
- * @param {{ mainCwd: string, storyId: number }} opts
46
- * @returns {object | null}
47
- */
48
- export function loadPhaseTimerState({ mainCwd, storyId }) {
49
- const p = filePath(mainCwd, storyId);
50
- if (!fs.existsSync(p)) return null;
51
- try {
52
- return JSON.parse(fs.readFileSync(p, 'utf8'));
53
- } catch {
54
- return null;
55
- }
56
- }
57
-
58
- /**
59
- * Remove the on-disk snapshot once the close has consumed it. Missing file
60
- * is not an error — the close path may run without a prior init (e.g.
61
- * operator invoking close manually after an init failure).
62
- *
63
- * @param {{ mainCwd: string, storyId: number }} opts
64
- */
65
- export function clearPhaseTimerState({ mainCwd, storyId }) {
66
- const p = filePath(mainCwd, storyId);
67
- try {
68
- fs.unlinkSync(p);
69
- } catch (err) {
70
- if (err.code !== 'ENOENT') throw err;
71
- }
72
- }
@@ -1,163 +0,0 @@
1
- import { Logger } from '../Logger.js';
2
- /**
3
- * lib/util/phase-timer.js — Per-phase wall-clock timer for the Story lifecycle.
4
- *
5
- * A single Story Mode run spans two Node processes (`story-init` →
6
- * implement → `story-close`), so the timer supports snapshot/restore so
7
- * close can pick up where init left off. Phase names are drawn from a fixed
8
- * enum (tech spec #555 §Data Models) so the emitted `[phase-timing]` log
9
- * lines and the `phase-timings` structured comment share one schema.
10
- *
11
- * Semantics
12
- * ---------
13
- * - `mark(name)` closes the current open phase (if any) at `now()` and opens
14
- * a new phase at the same instant. The just-closed phase's
15
- * `elapsedMs = now() - openedAt` is appended to the ordered `phases` list
16
- * and a single `[phase-timing]` log line is emitted for it.
17
- * - `finish()` closes the current open phase at `now()` and returns the
18
- * full summary `{ storyId, totalMs, phases: [{ name, elapsedMs }, ...] }`.
19
- * Idempotent — subsequent calls return the cached summary.
20
- * - `snapshot()` returns a plain-object that `createPhaseTimer({ restore })`
21
- * can round-trip. Used by `story-init` to hand the timer state to
22
- * `story-close` via a small on-disk file in the main repo `.git/`.
23
- *
24
- * Invariants
25
- * ----------
26
- * - Unknown phase names throw. The enum is frozen at `ALLOWED_PHASE_NAMES`.
27
- * - `mark()` after `finish()` throws — caller bug, not runtime drift.
28
- * - `totalMs` is wall clock from `createdAt` to `finish()` time, not a sum
29
- * of `phases[].elapsedMs`. Gaps before the first `mark()` (or between
30
- * restore and the next mark) are therefore captured in `totalMs` even
31
- * though they are not attributable to a named phase.
32
- */
33
-
34
- export const ALLOWED_PHASE_NAMES = Object.freeze([
35
- 'worktree-create',
36
- 'bootstrap',
37
- 'install',
38
- 'implement',
39
- 'lint',
40
- 'test',
41
- 'close',
42
- 'api-sync',
43
- ]);
44
-
45
- const ALLOWED_SET = new Set(ALLOWED_PHASE_NAMES);
46
-
47
- /**
48
- * @typedef {typeof ALLOWED_PHASE_NAMES[number]} PhaseName
49
- * @typedef {{ name: PhaseName, elapsedMs: number }} PhaseEntry
50
- * @typedef {{
51
- * storyId: number,
52
- * totalMs: number,
53
- * phases: PhaseEntry[],
54
- * }} PhaseTimingSummary
55
- * @typedef {{
56
- * storyId: number,
57
- * createdAt: number,
58
- * phases: PhaseEntry[],
59
- * current: { name: PhaseName, openedAt: number } | null,
60
- * finished: PhaseTimingSummary | null,
61
- * }} PhaseTimerSnapshot
62
- */
63
-
64
- /**
65
- * Create a phase timer for a single Story lifecycle.
66
- *
67
- * @param {number} storyId - Numeric Story id; baked into log lines and summary.
68
- * @param {{
69
- * now?: () => number,
70
- * logger?: (line: string) => void,
71
- * restore?: PhaseTimerSnapshot,
72
- * }} [opts]
73
- * @returns {{
74
- * mark: (name: PhaseName) => void,
75
- * finish: () => PhaseTimingSummary,
76
- * snapshot: () => PhaseTimerSnapshot,
77
- * }}
78
- */
79
- export function createPhaseTimer(storyId, opts = {}) {
80
- const now = opts.now ?? Date.now;
81
- const logger = opts.logger ?? ((line) => Logger.info(line));
82
-
83
- let createdAt;
84
- let phases;
85
- let current;
86
- let finished;
87
- let timerStoryId;
88
-
89
- if (opts.restore) {
90
- const snap = opts.restore;
91
- timerStoryId = snap.storyId;
92
- createdAt = snap.createdAt;
93
- phases = Array.isArray(snap.phases)
94
- ? snap.phases.map((p) => ({ ...p }))
95
- : [];
96
- current = snap.current ? { ...snap.current } : null;
97
- finished = snap.finished
98
- ? {
99
- ...snap.finished,
100
- phases: snap.finished.phases.map((p) => ({ ...p })),
101
- }
102
- : null;
103
- } else {
104
- timerStoryId = storyId;
105
- createdAt = now();
106
- phases = [];
107
- current = null;
108
- finished = null;
109
- }
110
-
111
- function closeCurrent(endAt) {
112
- if (!current) return;
113
- const elapsedMs = Math.max(0, endAt - current.openedAt);
114
- const entry = { name: current.name, elapsedMs };
115
- phases.push(entry);
116
- logger(
117
- `[phase-timing] story=${timerStoryId} phase=${entry.name} elapsedMs=${elapsedMs}`,
118
- );
119
- current = null;
120
- }
121
-
122
- function mark(name) {
123
- if (finished) {
124
- throw new Error(
125
- `createPhaseTimer: cannot mark('${name}') after finish() on story #${timerStoryId}`,
126
- );
127
- }
128
- if (!ALLOWED_SET.has(name)) {
129
- throw new Error(
130
- `createPhaseTimer: unknown phase '${name}'. Allowed: ${ALLOWED_PHASE_NAMES.join(', ')}`,
131
- );
132
- }
133
- const t = now();
134
- closeCurrent(t);
135
- current = { name, openedAt: t };
136
- }
137
-
138
- function finish() {
139
- if (finished) return finished;
140
- const t = now();
141
- closeCurrent(t);
142
- finished = {
143
- storyId: timerStoryId,
144
- totalMs: Math.max(0, t - createdAt),
145
- phases: phases.map((p) => ({ ...p })),
146
- };
147
- return finished;
148
- }
149
-
150
- function snapshot() {
151
- return {
152
- storyId: timerStoryId,
153
- createdAt,
154
- phases: phases.map((p) => ({ ...p })),
155
- current: current ? { ...current } : null,
156
- finished: finished
157
- ? { ...finished, phases: finished.phases.map((p) => ({ ...p })) }
158
- : null,
159
- };
160
- }
161
-
162
- return { mark, finish, snapshot };
163
- }