mandrel 2.30.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 (253) 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/ci-remediation.md +44 -1
  10. package/.agents/rules/git-conventions-reference.md +27 -27
  11. package/.agents/rules/git-conventions.md +4 -2
  12. package/.agents/rules/known-tooling-behavior.md +66 -30
  13. package/.agents/rules/testing-standards.md +35 -71
  14. package/.agents/runtime-deps.json +0 -1
  15. package/.agents/schemas/agentrc.schema.json +1939 -1400
  16. package/.agents/schemas/lifecycle/README.md +21 -14
  17. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  18. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  19. package/.agents/scripts/README.md +7 -29
  20. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +136 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  38. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  39. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  40. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  41. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  42. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  43. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  44. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  45. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  46. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  47. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  48. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  49. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  50. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  51. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  52. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  53. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  54. package/.agents/scripts/lib/child-exec.js +193 -0
  55. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  56. package/.agents/scripts/lib/cli-args.js +64 -0
  57. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  58. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  59. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  60. package/.agents/scripts/lib/config/commands.js +3 -5
  61. package/.agents/scripts/lib/config/explain.js +5 -7
  62. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  63. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  64. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  65. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  66. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  67. package/.agents/scripts/lib/config/gates/index.js +5 -2
  68. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  69. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  70. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  71. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  72. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  73. package/.agents/scripts/lib/config/github.js +0 -1
  74. package/.agents/scripts/lib/config/limits.js +3 -48
  75. package/.agents/scripts/lib/config/qa.js +105 -0
  76. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  77. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  78. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  79. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  80. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  81. package/.agents/scripts/lib/crap-utils.js +84 -520
  82. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  83. package/.agents/scripts/lib/degraded-mode.js +2 -2
  84. package/.agents/scripts/lib/doc-tiers.js +3 -3
  85. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  86. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  87. package/.agents/scripts/lib/fs-walk.js +52 -0
  88. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  89. package/.agents/scripts/lib/git-utils.js +16 -36
  90. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  91. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  92. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  93. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  94. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  95. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  96. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  97. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  98. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  99. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  100. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  101. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  102. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  103. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  104. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  106. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  107. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  108. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  109. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  110. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  111. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  112. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  113. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  124. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
  125. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  126. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  127. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  128. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  129. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  130. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  131. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  132. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  133. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  134. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  135. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  136. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  137. package/.agents/scripts/lib/signals/index.js +8 -6
  138. package/.agents/scripts/lib/signals/schema.js +20 -25
  139. package/.agents/scripts/lib/signals/write.js +8 -8
  140. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  141. package/.agents/scripts/lib/temp-retention.js +1 -1
  142. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  143. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  144. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  145. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  146. package/.agents/scripts/lib/worktree-manager.js +2 -3
  147. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  148. package/.agents/scripts/pr-watch-with-update.js +7 -5
  149. package/.agents/scripts/providers/github/cache.js +2 -2
  150. package/.agents/scripts/providers/github/comments.js +6 -28
  151. package/.agents/scripts/providers/github/compose.js +0 -15
  152. package/.agents/scripts/providers/github/errors.js +10 -27
  153. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  154. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  155. package/.agents/scripts/providers/github.js +4 -7
  156. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  157. package/.agents/scripts/resolve-stories.js +0 -2
  158. package/.agents/scripts/run-lint.js +61 -61
  159. package/.agents/scripts/run-test-profile.js +6 -6
  160. package/.agents/scripts/run-verify.js +48 -30
  161. package/.agents/scripts/single-story-close.js +20 -0
  162. package/.agents/scripts/single-story-init.js +12 -35
  163. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  165. package/.agents/skills/skills.index.json +2 -12
  166. package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
  167. package/.agents/workflows/audit-documentation.md +5 -6
  168. package/.agents/workflows/audit-to-stories.md +2 -2
  169. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  170. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  171. package/.agents/workflows/helpers/code-review.md +3 -8
  172. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  173. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  174. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  175. package/.agents/workflows/mandrel-update.md +10 -10
  176. package/.agents/workflows/qa-assist.md +15 -20
  177. package/.agents/workflows/qa-explore.md +9 -8
  178. package/README.md +1 -1
  179. package/docs/CHANGELOG.md +49 -0
  180. package/lib/migrations/index.js +2 -0
  181. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  182. package/package.json +12 -3
  183. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  184. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  185. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  186. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  187. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  188. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  189. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  190. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  191. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  192. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  193. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  194. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  195. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  196. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  197. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  198. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  199. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  200. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  201. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  202. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  203. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  204. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  205. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  206. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  207. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  208. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  209. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  210. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  211. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  212. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  213. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  214. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  215. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  216. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  217. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  218. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  219. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  220. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  221. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  222. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  223. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  224. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  225. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  226. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  227. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  228. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  229. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  230. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  231. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  232. package/.agents/scripts/lib/planning-corpus.js +0 -37
  233. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  234. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  235. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  236. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  237. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  238. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  239. package/.agents/scripts/lib/signals/read.js +0 -268
  240. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  241. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  242. package/.agents/scripts/lib/story-plan.js +0 -379
  243. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  244. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  245. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  246. package/.agents/scripts/lint-baseline.js +0 -507
  247. package/.agents/scripts/providers/github/prs.js +0 -103
  248. package/.agents/scripts/signals-view.js +0 -309
  249. package/.agents/scripts/story-plan.js +0 -370
  250. package/.agents/scripts/sync-branch-from-base.js +0 -149
  251. package/.agents/scripts/validate-docs-freshness.js +0 -314
  252. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  253. 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
- }