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,370 +0,0 @@
1
- #!/usr/bin/env node
2
- /* node:coverage ignore file */
3
-
4
- /**
5
- * story-plan.js — QA helper for promote round-trips and `--emit-context`
6
- * envelope emission.
7
- *
8
- * Canonical operator planning is `plan-context.js` + `plan-persist.js` (see
9
- * `.agents/workflows/plan.md`). Use this CLI only for QA promote
10
- * round-trips (`--body` / `--dry-run`) or test harnesses that need the
11
- * standalone envelope shape.
12
- *
13
- * Operator planning:
14
- * node .agents/scripts/plan-context.js --seed "…" | --seed-file <path> | --tickets <ids>
15
- * node .agents/scripts/plan-persist.js --stories …
16
- */
17
-
18
- import { readFile } from 'node:fs/promises';
19
- import path from 'node:path';
20
- import { parseArgs } from 'node:util';
21
- import { runAsCli } from './lib/cli-utils.js';
22
- import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
23
- import { exec as ghExec } from './lib/gh-exec.js';
24
- import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
25
- import { TYPE_LABELS } from './lib/label-constants.js';
26
- import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
27
- import { buildCorpusContext } from './lib/planning-corpus.js';
28
- import { createProvider } from './lib/provider-factory.js';
29
- import {
30
- buildContextEnvelope,
31
- loadBodyTemplate,
32
- rankDuplicateCandidates,
33
- readTechStackSummary,
34
- shouldRefine,
35
- synthesizeContractSections,
36
- validateStoryBody,
37
- } from './lib/story-plan.js';
38
-
39
- const HELP = `\
40
- Usage:
41
- story-plan.js --emit-context (--seed "<seed>" | --seed-file <file>) \\
42
- [--refine | --no-refine] [--pretty]
43
-
44
- story-plan.js --body <file> [--acceptance <file>] [--verify <file>] \\
45
- [--dry-run]
46
-
47
- story-plan.js --help
48
-
49
- Modes:
50
- --emit-context Build the host-LLM authoring envelope and print it as
51
- JSON on stdout. Use this first; the host LLM authors a
52
- draft body using the envelope and the body template.
53
- --body <file> Persist a pre-authored body. Validates shape (## Goal
54
- and ## Changes present, no Epic: ref, an acceptance +
55
- verify contract) and calls \`gh issue create\` with
56
- type::story.
57
- --acceptance <file>
58
- JSON string[] of the Story's top-level acceptance
59
- criteria. Authored once here — NOT mirrored into the
60
- body by hand; the \`## Acceptance\` section is
61
- synthesized from it, matching the story-author prompt.
62
- --verify <file> JSON string[] of the Story's top-level verify entries,
63
- each ending in a (unit|contract|e2e|validate) tier.
64
- Synthesized into \`## Verify\` the same way.
65
- --dry-run With --body: print the body and the gh argv that would
66
- be invoked, then exit 0. No GitHub mutations.
67
-
68
- Options:
69
- --refine Force the idea-refinement hint on regardless of seed
70
- length. Default heuristic: refine when seed < 200
71
- chars.
72
- --no-refine Force the idea-refinement hint off.
73
- --pretty Pretty-print the JSON envelope.
74
- `;
75
-
76
- /**
77
- * Resolve the seed string from --seed or --seed-file. One of the two
78
- * must be present in --emit-context mode.
79
- */
80
- async function resolveSeed({ seed, seedFile }) {
81
- if (seed && seedFile) {
82
- throw new Error('Pass either --seed or --seed-file, not both.');
83
- }
84
- if (seed) return seed;
85
- if (seedFile) return (await readFile(seedFile, 'utf8')).trim();
86
- throw new Error(
87
- '--emit-context requires --seed "<seed>" or --seed-file <file>.',
88
- );
89
- }
90
-
91
- /**
92
- * Fetch open Stories via the ticketing provider. The github provider
93
- * exposes `listIssuesByLabel({ state, labels })`; other providers may
94
- * not, so the call is guarded.
95
- */
96
- async function fetchOpenStories(provider) {
97
- if (typeof provider.listIssuesByLabel !== 'function') return [];
98
- const issues = await provider.listIssuesByLabel({
99
- state: 'open',
100
- labels: TYPE_LABELS.STORY,
101
- });
102
- return issues.map((issue) => ({
103
- id: issue.number ?? issue.id,
104
- title: issue.title ?? '',
105
- body: issue.body ?? '',
106
- url: issue.html_url ?? issue.url ?? null,
107
- }));
108
- }
109
-
110
- /**
111
- * Render the draft body argv for `gh issue create`. Returns the argv
112
- * array; the caller decides whether to execute it (persist) or print
113
- * it (--dry-run).
114
- */
115
- function renderGhArgv({ title, bodyPath, labels, body = null }) {
116
- // `--body-file` streams the authored file untouched; an inline `--body` is
117
- // used only when persist synthesized sections into it (Story #4874), so the
118
- // file on disk is no longer what gets created.
119
- const source =
120
- typeof body === 'string' ? ['--body', body] : ['--body-file', bodyPath];
121
- const argv = ['issue', 'create', '--title', title, ...source];
122
- for (const label of labels) {
123
- argv.push('--label', label);
124
- }
125
- return argv;
126
- }
127
-
128
- /**
129
- * Extract the H1 title from a body. Falls back to a sensible default
130
- * when the body lacks one (host LLM should always emit `# <title>`).
131
- */
132
- export function extractTitle(body) {
133
- const m = body.match(/^#\s+(.+?)\s*$/m);
134
- return m ? m[1].trim() : 'Untitled standalone Story';
135
- }
136
-
137
- async function runEmitContext({
138
- values,
139
- provider,
140
- projectRoot,
141
- config,
142
- // Injectable stdout port so unit tests can capture the emitted envelope
143
- // without stubbing the process-global stream (mirrors the `runPersist`
144
- // pattern above — raw stdout writes corrupt the `node --test` runner's
145
- // structured report stream).
146
- write = (s) => process.stdout.write(s),
147
- }) {
148
- const seed = await resolveSeed({
149
- seed: values.seed,
150
- seedFile: values['seed-file'],
151
- });
152
- const override = values.refine ? 'on' : values['no-refine'] ? 'off' : null;
153
- const refine = shouldRefine({ seed, override });
154
-
155
- // Corpus lookup uses the raw (un-defaulted) docsContextFiles list, same
156
- // as the `/deliver` per-Epic digest builder: `config.project` fills in
157
- // the framework's default four-file set even when the operator
158
- // configured nothing, so a null-vs-configured distinction requires
159
- // reading `config.raw` directly.
160
- const docsContextFiles = config?.raw?.project?.docsContextFiles ?? [];
161
- // Resolve docsRoot against PROJECT_ROOT (not process.cwd()) so the
162
- // corpus digest reads the project's actual docs directory regardless
163
- // of the directory this CLI happens to be invoked from — matching the
164
- // sibling resolution pattern in
165
- // planning/authoring-context.js.
166
- const docsRoot = path.resolve(
167
- PROJECT_ROOT,
168
- config?.project?.paths?.docsRoot ?? 'docs',
169
- );
170
-
171
- const [bodyTemplate, openStories, techStack, corpusContext] =
172
- await Promise.all([
173
- loadBodyTemplate(projectRoot),
174
- fetchOpenStories(provider),
175
- readTechStackSummary(projectRoot),
176
- buildCorpusContext({ docsContextFiles, docsRoot }),
177
- ]);
178
-
179
- const duplicateCandidates = rankDuplicateCandidates({
180
- seed,
181
- openStories,
182
- });
183
-
184
- const envelope = buildContextEnvelope({
185
- seed,
186
- refine,
187
- bodyTemplate,
188
- duplicateCandidates,
189
- techStack,
190
- corpusContext,
191
- });
192
-
193
- const json = values.pretty
194
- ? JSON.stringify(envelope, null, 2)
195
- : JSON.stringify(envelope);
196
- write(`${json}\n`);
197
- }
198
-
199
- /**
200
- * Read an optional top-level contract array (`--acceptance` / `--verify`)
201
- * from a JSON file. Absent flag → an empty list, so the flags stay optional
202
- * for a body that already carries its own sections.
203
- *
204
- * @param {string|undefined} filePath
205
- * @param {'acceptance'|'verify'} label
206
- * @returns {Promise<string[]>}
207
- */
208
- async function readContractList(filePath, label) {
209
- if (!filePath) return [];
210
- let parsed;
211
- try {
212
- parsed = JSON.parse(await readFile(filePath, 'utf8'));
213
- } catch (err) {
214
- throw new Error(
215
- `Cannot read --${label} file "${filePath}": ${err.message}`,
216
- );
217
- }
218
- if (!Array.isArray(parsed) || parsed.some((v) => typeof v !== 'string')) {
219
- throw new Error(`--${label} file "${filePath}" must be a JSON string[].`);
220
- }
221
- return parsed;
222
- }
223
-
224
- async function runPersist({
225
- values,
226
- provider,
227
- dryRun,
228
- // Injectable stdout port so unit tests can capture/silence the summary
229
- // JSON without stubbing the process-global stream (raw stdout writes
230
- // corrupt the `node --test` runner's structured report stream).
231
- write = (s) => process.stdout.write(s),
232
- }) {
233
- const bodyPath = values.body;
234
- if (!bodyPath) {
235
- throw new Error('--body <file> is required in persist mode.');
236
- }
237
- const authored = await readFile(bodyPath, 'utf8');
238
- const contract = {
239
- acceptance: await readContractList(values.acceptance, 'acceptance'),
240
- verify: await readContractList(values.verify, 'verify'),
241
- };
242
- const validation = validateStoryBody(authored, contract);
243
- if (!validation.ok) {
244
- throw new Error(
245
- `Drafted body failed validation:\n - ${validation.errors.join('\n - ')}`,
246
- );
247
- }
248
-
249
- // Story #4874: the author writes acceptance[] / verify[] once, at the
250
- // ticket's top level, and persist renders their sections — never the
251
- // author by hand.
252
- const body = synthesizeContractSections(authored, contract);
253
- const title = extractTitle(body);
254
- const labels = [TYPE_LABELS.STORY];
255
- const argv = renderGhArgv({
256
- title,
257
- bodyPath,
258
- labels,
259
- body: body === authored ? null : body,
260
- });
261
-
262
- if (dryRun) {
263
- Logger.info('--- DRY RUN ---');
264
- Logger.info(`Title: ${title}`);
265
- Logger.info(`Labels: ${labels.join(', ')}`);
266
- Logger.info(`gh argv: gh ${argv.join(' ')}`);
267
- Logger.info('--- BODY ---');
268
- Logger.info(body);
269
- write(
270
- `${JSON.stringify({ dryRun: true, title, labels, argv }, null, 2)}\n`,
271
- );
272
- return;
273
- }
274
-
275
- // Persist via the provider when available so I/O stays inside the
276
- // injected ticketing surface. The GitHub provider's `createIssue`
277
- // also adds the new Story to the configured Projects V2 board via
278
- // the shared `addIssueToBoard` helper (Story #3822) — idempotent,
279
- // non-fatal, no-op when no project number is configured — so board
280
- // membership never depends on GitHub's "Auto-add to project"
281
- // built-in workflow. Fall back to `gh issue create` only when the
282
- // provider doesn't expose a createIssue analogue.
283
- let issueNumber;
284
- if (typeof provider.createIssue === 'function') {
285
- const created = await provider.createIssue({ title, body, labels });
286
- issueNumber = created.number ?? created.id;
287
- } else {
288
- // gh-exec returns { stdout, stderr, code }; parse the issue number
289
- // from the URL.
290
- const { stdout } = await ghExec({ args: argv });
291
- const m = stdout.match(/\/issues\/(\d+)/);
292
- if (!m) {
293
- throw new Error(
294
- `gh issue create did not return an issue URL; stdout was: ${stdout}`,
295
- );
296
- }
297
- issueNumber = Number(m[1]);
298
- }
299
-
300
- write(`${JSON.stringify({ issueNumber, title, labels }, null, 2)}\n`);
301
- Logger.info(`Next: /deliver ${issueNumber}`);
302
- }
303
-
304
- /* node:coverage ignore next */
305
- async function main() {
306
- const { values } = parseArgs({
307
- options: {
308
- 'emit-context': { type: 'boolean', default: false },
309
- seed: { type: 'string' },
310
- 'seed-file': { type: 'string' },
311
- body: { type: 'string' },
312
- acceptance: { type: 'string' },
313
- verify: { type: 'string' },
314
- refine: { type: 'boolean', default: false },
315
- 'no-refine': { type: 'boolean', default: false },
316
- pretty: { type: 'boolean', default: false },
317
- 'dry-run': { type: 'boolean', default: false },
318
- help: { type: 'boolean', default: false },
319
- },
320
- });
321
-
322
- if (values.help) {
323
- process.stdout.write(HELP);
324
- return;
325
- }
326
- if (values.refine && values['no-refine']) {
327
- throw new Error('Pass either --refine or --no-refine, not both.');
328
- }
329
-
330
- const config = resolveConfig();
331
- const provider = createProvider(config);
332
- const projectRoot = PROJECT_ROOT;
333
-
334
- if (values['emit-context']) {
335
- // Reserve stdout for the JSON envelope so a captured file is
336
- // unconditionally parseable by `JSON.parse`. Mirrors the contract
337
- // `plan-context.js` enforces for its emit mode.
338
- routeAllOutputToStderr();
339
- // Plan-metrics ledger (#4474 PR1): standalone plans have no Epic, so
340
- // `epicId: null` routes the stamp to the standalone stream
341
- // (`temp/standalone/plan-metrics.json`) — same pattern as friction.
342
- return recordPlanInvocation(
343
- { cli: 'story-plan', mode: 'emit-context', epicId: null, config },
344
- () => runEmitContext({ values, provider, projectRoot, config }),
345
- );
346
- }
347
-
348
- // Plan-metrics ledger (#4474 PR1): stamp entry/exit + mode.
349
- return recordPlanInvocation(
350
- { cli: 'story-plan', mode: 'persist', epicId: null, config },
351
- () =>
352
- runPersist({
353
- values,
354
- provider,
355
- dryRun: values['dry-run'],
356
- }),
357
- );
358
- }
359
-
360
- runAsCli(import.meta.url, main, { source: 'story-plan' });
361
-
362
- // Test surface — exported so unit tests can drive the helpers
363
- // without importing the CLI side.
364
- export {
365
- fetchOpenStories,
366
- renderGhArgv,
367
- resolveSeed,
368
- runEmitContext,
369
- runPersist,
370
- };
@@ -1,149 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * sync-branch-from-base.js — operator-facing CLI wrapper around
5
- * `lib/git/sync-from-base.js` (Story #2580).
6
- *
7
- * Use this from workflow markdown when the operator needs to sync a
8
- * working branch with `origin/<baseBranch>` before opening a PR — for
9
- * example as a pre-Phase-6 step in `/deliver` so the Epic→main PR
10
- * opens with the latest `main` commits already integrated.
11
- *
12
- * For `/single-story-deliver`, the sync runs in-process inside
13
- * `single-story-close.js`; this CLI is the operator surface for
14
- * workflows that don't have a built-in close pipeline.
15
- *
16
- * Usage:
17
- * node .agents/scripts/sync-branch-from-base.js \
18
- * --branch <branchName> --base <baseBranch> [--cwd <path>]
19
- *
20
- * Exit codes:
21
- * 0 — synced (`fast-forward`, `merge-commit`, or `noop-already-current`).
22
- * 1 — fetch/merge failed or conflict; error message on stderr names the
23
- * outcome kind and (for conflicts) the conflicting file list.
24
- *
25
- * The script does NOT post structured comments or flip ticket labels —
26
- * it is a pure git operation. Recovery surfaces are the caller's
27
- * responsibility (the workflow doc tells the operator what to do when
28
- * this exits non-zero).
29
- */
30
-
31
- import path from 'node:path';
32
- import { parseArgs } from 'node:util';
33
- import { runAsCli } from './lib/cli-utils.js';
34
- import { resolveConfig } from './lib/config-resolver.js';
35
- import { syncBranchFromBase } from './lib/git/sync-from-base.js';
36
- import { gitSpawn, gitSync } from './lib/git-utils.js';
37
- import { Logger } from './lib/Logger.js';
38
- import { emitTerseResult } from './lib/observability/terse-result.js';
39
- import { PROJECT_ROOT } from './lib/project-root.js';
40
-
41
- const progress = Logger.createProgress('sync-branch-from-base', {
42
- stderr: true,
43
- });
44
-
45
- /**
46
- * Core runner. Exported for tests so the CLI surface can be exercised
47
- * without spawning a subprocess.
48
- *
49
- * @param {object} [opts]
50
- * @param {string} [opts.branch] Branch name expected to be checked out.
51
- * @param {string} [opts.base] Base branch name on origin (e.g. `main`).
52
- * @param {string} [opts.cwd] Worktree or repo root. Defaults to PROJECT_ROOT.
53
- * @param {typeof syncBranchFromBase} [opts.injectedSync] Test-only seam.
54
- * @param {(cwd: string, ...args: string[]) => string} [opts.injectedGitSync]
55
- * Test-only seam for the branch-name guard.
56
- */
57
- export async function runSyncBranchFromBase(opts = {}) {
58
- const parsed = opts.branch ? opts : parseArgv(process.argv.slice(2));
59
- const branch = opts.branch ?? parsed.branch;
60
- const base = opts.base ?? parsed.base;
61
- if (!branch || !base) {
62
- throw new Error(
63
- 'Usage: node sync-branch-from-base.js --branch <branchName> --base <baseBranch> [--cwd <path>]',
64
- );
65
- }
66
- const cwd = path.resolve(opts.cwd ?? parsed.cwd ?? PROJECT_ROOT);
67
-
68
- // Guard: refuse to sync if the active branch doesn't match the
69
- // operator's intent. A mismatched checkout would merge `origin/<base>`
70
- // into the wrong branch — silently — and only surface as a confusing
71
- // diff later. Skipped when injectedGitSync is set so tests can run
72
- // without a real git repo.
73
- const gitSyncFn = opts.injectedGitSync ?? gitSync;
74
- let activeBranch = null;
75
- try {
76
- activeBranch = gitSyncFn(cwd, 'rev-parse', '--abbrev-ref', 'HEAD');
77
- } catch (err) {
78
- throw new Error(
79
- `[sync-branch-from-base] Could not resolve active branch in ${cwd}: ${err?.message ?? err}`,
80
- );
81
- }
82
- if (activeBranch !== branch) {
83
- throw new Error(
84
- `[sync-branch-from-base] Active branch is "${activeBranch}" but --branch is "${branch}". ` +
85
- `Check out ${branch} first, or pass --cwd to the correct worktree.`,
86
- );
87
- }
88
-
89
- progress('INIT', `Syncing ${branch} from origin/${base} in ${cwd}...`);
90
- const syncFn = opts.injectedSync ?? syncBranchFromBase;
91
- const result = await syncFn({
92
- cwd,
93
- baseBranch: base,
94
- log: (tag, msg) => progress(tag, msg),
95
- gitSpawn,
96
- });
97
-
98
- // Story #4685 — full detail to a temp log; emit a single summary line.
99
- // Story #4794 — resolve the config so the log honours `project.paths.tempRoot`
100
- // instead of the hardcoded `<cwd>/temp` this used to join. `resolveConfig`
101
- // degrades to the framework defaults when no `.agentrc.json` is present, so
102
- // a zero-config invocation needs no guard here.
103
- emitTerseResult({
104
- label: 'SYNC RESULT',
105
- result,
106
- scope: branch,
107
- config: resolveConfig({ cwd }),
108
- summary: { branch, base, synced: result.synced, kind: result.kind },
109
- });
110
-
111
- if (!result.synced) {
112
- const detail =
113
- result.kind === 'conflict'
114
- ? `: ${result.conflictFiles?.join(', ') ?? '(no file list)'}`
115
- : result.stderr
116
- ? `: ${result.stderr.slice(0, 200)}`
117
- : '';
118
- throw new Error(
119
- `[sync-branch-from-base] sync failed (${result.kind})${detail}`,
120
- );
121
- }
122
- progress('DONE', `✅ Synced ${branch} from origin/${base} (${result.kind}).`);
123
- return { success: true, result };
124
- }
125
-
126
- /**
127
- * Tiny argv parser scoped to this CLI's three flags. Exported for
128
- * tests.
129
- */
130
- export function parseArgv(argv) {
131
- const { values } = parseArgs({
132
- args: argv,
133
- options: {
134
- branch: { type: 'string' },
135
- base: { type: 'string' },
136
- cwd: { type: 'string' },
137
- },
138
- strict: false,
139
- });
140
- return {
141
- branch: values.branch ?? null,
142
- base: values.base ?? null,
143
- cwd: values.cwd ?? null,
144
- };
145
- }
146
-
147
- runAsCli(import.meta.url, runSyncBranchFromBase, {
148
- source: 'sync-branch-from-base',
149
- });