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,411 +0,0 @@
1
- #!/usr/bin/env node
2
- /* node:coverage ignore file */
3
-
4
- /**
5
- * check-lifecycle-doc-drift.js — enforce that the listener-model table
6
- * in `docs/LIFECYCLE.md` stays in lockstep with the per-listener
7
- * `this.events = Object.freeze([...])` subscription arrays under
8
- * `.agents/scripts/lib/orchestration/lifecycle/listeners/`.
9
- *
10
- * Why this exists (Tech Spec F5, Epic #2880): the LIFECYCLE.md
11
- * listener-model table is hand-authored prose that documents the
12
- * lifecycle-bus contract. When a listener rebinds (e.g.
13
- * `epic.merge.armed` → `epic.merge.confirmed`) the table drifts
14
- * silently because nothing reads the code against the doc. This check
15
- * closes that gap by failing CI on any per-listener subscription that
16
- * exists in code but not in the doc — or vice versa.
17
- *
18
- * Scope:
19
- * - Listener files scanned: every `*.js` under
20
- * `.agents/scripts/lib/orchestration/lifecycle/listeners/` except
21
- * `index.js` (the factory).
22
- * - Subscription source: each listener exposes its subscription list
23
- * via a `this.events = Object.freeze([...])` assignment containing
24
- * a literal array of single-quoted event names. Listeners that do
25
- * not match this pattern — wildcard observers (`bus.on('*', ...)`)
26
- * and dynamic-event listeners that resolve subscriptions through
27
- * identifiers (`Object.keys(...)`, imported constants) — are
28
- * treated as **wildcard** for the purpose of this check. The doc
29
- * row for a wildcard listener must carry `` `*` `` in its
30
- * subscribes-to column.
31
- * - Doc parsed: `docs/LIFECYCLE.md` § "4. Listener model" — every
32
- * row in the listener table is keyed by the PascalCase listener
33
- * name in its first column and the set of backticked event tokens
34
- * in its second column.
35
- *
36
- * Drift detection:
37
- * - `code-only`: a literal event in `this.events` that does not
38
- * appear in the matching doc row's events column.
39
- * - `doc-only`: a backticked event token in the doc row that does
40
- * not appear in the listener's `this.events`.
41
- * - `missing-row`: a listener file with no matching PascalCase row
42
- * in the doc table.
43
- * - `unknown-row`: a doc row whose listener name does not map to any
44
- * file under listeners/. Listeners that ship outside the listeners/
45
- * directory (LedgerWriter, TraceLogger) are exempted by allow-list.
46
- *
47
- * Exit codes:
48
- * 0 — clean.
49
- * 1 — at least one drift; offending listener + diff printed to stderr.
50
- *
51
- * Wiring:
52
- * This script ships under the `baselines` CI check (see
53
- * `.agentrc.json` → `github.branchProtection.requiredChecks`).
54
- *
55
- * Public test seam: `checkLifecycleDocDrift({ listenersDir, docPath })`
56
- * runs the scanner against an arbitrary listener tree + doc path. The
57
- * pure helpers `extractCodeEvents(src)`, `parseListenerTable(md)`, and
58
- * `diffListenerEvents({ code, doc })` are exported for unit-level
59
- * reuse (the contract test under tests/contract/lifecycle drives them
60
- * directly).
61
- */
62
-
63
- import { readdirSync, readFileSync } from 'node:fs';
64
- import path from 'node:path';
65
- import { fileURLToPath } from 'node:url';
66
-
67
- import { runAsCli } from './lib/cli-utils.js';
68
-
69
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
70
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
71
-
72
- const DEFAULT_LISTENERS_DIR = path.join(
73
- REPO_ROOT,
74
- '.agents',
75
- 'scripts',
76
- 'lib',
77
- 'orchestration',
78
- 'lifecycle',
79
- 'listeners',
80
- );
81
- const DEFAULT_DOC_PATH = path.join(REPO_ROOT, 'docs', 'LIFECYCLE.md');
82
-
83
- /**
84
- * Listener classes documented in the LIFECYCLE.md listener table that
85
- * are intentionally NOT shipped as files under listeners/ — they live
86
- * in sibling modules (`ledger-writer.js`, `trace-logger.js`) but
87
- * register against the bus on the same wildcard contract. They are
88
- * exempt from the `unknown-row` check.
89
- */
90
- const EXEMPT_DOC_ROWS = Object.freeze(new Set(['LedgerWriter', 'TraceLogger']));
91
-
92
- /**
93
- * Convert a kebab-case basename to PascalCase, e.g.
94
- * `acceptance-reconciler` → `AcceptanceReconciler`.
95
- *
96
- * @param {string} basename
97
- * @returns {string}
98
- */
99
- export function kebabToPascal(basename) {
100
- return basename
101
- .split('-')
102
- .map((seg) => (seg.length ? seg[0].toUpperCase() + seg.slice(1) : seg))
103
- .join('');
104
- }
105
-
106
- /**
107
- * Extract the array of literal event names from a listener file's
108
- * source. Looks for the canonical `this.events = Object.freeze([...])`
109
- * pattern; the array body may span multiple lines. Returns:
110
- * - `{ kind: 'literals', events: [...] }` when every array entry is
111
- * a single- or double-quoted string literal.
112
- * - `{ kind: 'wildcard' }` when no pattern matches OR the array body
113
- * contains identifiers / function calls (i.e. the subscription set
114
- * is dynamic at runtime; the doc row must use `*`).
115
- *
116
- * @param {string} src
117
- * @returns {{ kind: 'literals', events: string[] } | { kind: 'wildcard' }}
118
- */
119
- export function extractCodeEvents(src) {
120
- // Greedy across newlines, non-greedy on the array body. The
121
- // surrounding `Object.freeze([` … `])` anchors the match tightly so
122
- // we never swallow unrelated array literals.
123
- const re = /this\.events\s*=\s*Object\.freeze\(\s*\[([\s\S]*?)\]\s*\)/;
124
- const m = re.exec(src);
125
- if (!m) return { kind: 'wildcard' };
126
- const body = m[1];
127
- // Build a constant table for top-level `const|export const NAME =
128
- // '<literal>';` assignments so subscriptions that reference an event
129
- // name constant (e.g. `Object.freeze([INTERVENTION_RECORDED_EVENT])`)
130
- // resolve to a literal here instead of falling through to wildcard.
131
- const constants = collectStringConstants(src);
132
- // Strip comments inside the array body so they don't confuse the
133
- // literal-vs-identifier split.
134
- const bodyClean = body
135
- .replace(/\/\/.*$/gm, '')
136
- .replace(/\/\*[\s\S]*?\*\//g, '');
137
- const literals = [];
138
- let isDynamic = false;
139
- for (const rawEntry of bodyClean.split(',')) {
140
- const entry = rawEntry.trim();
141
- if (entry.length === 0) continue;
142
- const strLit = /^'([^']*)'$|^"([^"]*)"$/.exec(entry);
143
- if (strLit) {
144
- literals.push(strLit[1] ?? strLit[2]);
145
- continue;
146
- }
147
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(entry) && constants.has(entry)) {
148
- literals.push(constants.get(entry));
149
- continue;
150
- }
151
- // Anything else (function call, spread, unknown identifier) means
152
- // the array is dynamic at runtime; treat as wildcard.
153
- isDynamic = true;
154
- break;
155
- }
156
- if (isDynamic) return { kind: 'wildcard' };
157
- if (literals.length === 0) return { kind: 'wildcard' };
158
- return { kind: 'literals', events: literals };
159
- }
160
-
161
- /**
162
- * Collect top-level string constants of the shape
163
- * `const NAME = '<literal>';` or `export const NAME = '<literal>';`
164
- * so the array-body parser can resolve identifier-form event names.
165
- *
166
- * @param {string} src
167
- * @returns {Map<string, string>}
168
- */
169
- export function collectStringConstants(src) {
170
- const out = new Map();
171
- const re =
172
- /^\s*(?:export\s+)?const\s+([A-Z_][A-Z0-9_]*)\s*=\s*(?:'([^']*)'|"([^"]*)")\s*;/gm;
173
- let m = re.exec(src);
174
- while (m !== null) {
175
- out.set(m[1], m[2] ?? m[3]);
176
- m = re.exec(src);
177
- }
178
- return out;
179
- }
180
-
181
- /**
182
- * Parse the LIFECYCLE.md listener-model table. Returns a Map from
183
- * PascalCase listener name → { events: Set<string>, hasWildcard:
184
- * boolean }. The table is located by looking for a markdown table
185
- * whose header includes "Listener" and "Subscribes to".
186
- *
187
- * @param {string} md
188
- * @returns {Map<string, { events: Set<string>, hasWildcard: boolean }>}
189
- */
190
- export function parseListenerTable(md) {
191
- const out = new Map();
192
- const lines = md.split(/\r?\n/);
193
- // Find the header row that names "Listener" and "Subscribes to".
194
- let inTable = false;
195
- for (let i = 0; i < lines.length; i += 1) {
196
- const line = lines[i];
197
- if (!inTable) {
198
- if (line.includes('| Listener') && line.includes('Subscribes to')) {
199
- // Header row found; skip the separator on the next line and
200
- // start parsing data rows.
201
- inTable = true;
202
- i += 1; // skip separator
203
- }
204
- continue;
205
- }
206
- // Data row or end-of-table.
207
- if (!line.startsWith('|')) break;
208
- const cells = line.split('|').map((c) => c.trim());
209
- // cells[0] is empty before leading '|'; cells[1] is the listener,
210
- // cells[2] is the subscribes-to column.
211
- if (cells.length < 3) continue;
212
- const nameCell = cells[1];
213
- const subsCell = cells[2];
214
- const nameMatch = /`([A-Za-z][A-Za-z0-9_]*)`/.exec(nameCell);
215
- if (!nameMatch) continue;
216
- const name = nameMatch[1];
217
- const events = new Set();
218
- let hasWildcard = false;
219
- const tokenRe = /`([^`]+)`/g;
220
- let tm = tokenRe.exec(subsCell);
221
- while (tm !== null) {
222
- const tok = tm[1];
223
- if (tok === '*') hasWildcard = true;
224
- // Event tokens look like `<word>.<word>(.<word>)*`. Anything
225
- // else (e.g. `epic-*`, prose tokens) is ignored — wildcard rows
226
- // already cover those cases.
227
- else if (/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/i.test(tok)) {
228
- events.add(tok);
229
- }
230
- tm = tokenRe.exec(subsCell);
231
- }
232
- out.set(name, { events, hasWildcard });
233
- }
234
- return out;
235
- }
236
-
237
- /**
238
- * Diff a code-side event extraction against a doc-side row.
239
- *
240
- * @param {object} input
241
- * @param {{ kind: 'literals', events: string[] } | { kind: 'wildcard' }} input.code
242
- * @param {{ events: Set<string>, hasWildcard: boolean } | undefined} input.doc
243
- * @returns {{ codeOnly: string[], docOnly: string[], wildcardMismatch: boolean }}
244
- */
245
- export function diffListenerEvents({ code, doc }) {
246
- const codeOnly = [];
247
- const docOnly = [];
248
- let wildcardMismatch = false;
249
- if (!doc) {
250
- // Caller surfaces this as `missing-row`; nothing to diff here.
251
- if (code.kind === 'literals')
252
- return { codeOnly: code.events.slice(), docOnly, wildcardMismatch };
253
- return { codeOnly, docOnly, wildcardMismatch };
254
- }
255
- if (code.kind === 'wildcard') {
256
- if (!doc.hasWildcard) wildcardMismatch = true;
257
- return { codeOnly, docOnly, wildcardMismatch };
258
- }
259
- // Literal code-side; compare both directions.
260
- const codeSet = new Set(code.events);
261
- for (const ev of code.events) {
262
- if (!doc.events.has(ev)) codeOnly.push(ev);
263
- }
264
- for (const ev of doc.events) {
265
- if (!codeSet.has(ev)) docOnly.push(ev);
266
- }
267
- return { codeOnly, docOnly, wildcardMismatch };
268
- }
269
-
270
- /**
271
- * Walk the listeners directory and return [{ pascalName, filePath,
272
- * code }] for every `.js` file (excluding `index.js`).
273
- *
274
- * @param {string} listenersDir
275
- * @param {{ read?: typeof readFileSync, readDir?: typeof readdirSync }} [opts]
276
- * @returns {Array<{ pascalName: string, filePath: string, code: ReturnType<typeof extractCodeEvents> }>}
277
- */
278
- export function loadCodeListeners(
279
- listenersDir,
280
- { read = readFileSync, readDir = readdirSync } = {},
281
- ) {
282
- const out = [];
283
- const entries = readDir(listenersDir, { withFileTypes: true });
284
- for (const ent of entries) {
285
- if (!ent.isFile()) continue;
286
- if (!ent.name.endsWith('.js')) continue;
287
- if (ent.name === 'index.js') continue;
288
- const basename = ent.name.replace(/\.js$/, '');
289
- const pascalName = kebabToPascal(basename);
290
- const filePath = path.join(listenersDir, ent.name);
291
- const src = read(filePath, 'utf8');
292
- const code = extractCodeEvents(src);
293
- out.push({ pascalName, filePath, code });
294
- }
295
- return out;
296
- }
297
-
298
- /**
299
- * Run the drift check end-to-end. Returns an array of findings; each
300
- * finding carries a `kind` discriminator and the offending listener
301
- * name + details.
302
- *
303
- * @param {object} [opts]
304
- * @param {string} [opts.listenersDir] override the default listeners/
305
- * path (used by tests + fixture injection).
306
- * @param {string} [opts.docPath] override the default LIFECYCLE.md
307
- * path.
308
- * @param {typeof readFileSync} [opts.read]
309
- * @param {typeof readdirSync} [opts.readDir]
310
- * @returns {Array<{ kind: string, listener: string, detail?: string, codeOnly?: string[], docOnly?: string[] }>}
311
- */
312
- export function checkLifecycleDocDrift({
313
- listenersDir = DEFAULT_LISTENERS_DIR,
314
- docPath = DEFAULT_DOC_PATH,
315
- read = readFileSync,
316
- readDir = readdirSync,
317
- } = {}) {
318
- const findings = [];
319
- const md = read(docPath, 'utf8');
320
- const docRows = parseListenerTable(md);
321
- const codeListeners = loadCodeListeners(listenersDir, { read, readDir });
322
- const codePascalNames = new Set(codeListeners.map((c) => c.pascalName));
323
-
324
- for (const { pascalName, code } of codeListeners) {
325
- const doc = docRows.get(pascalName);
326
- if (!doc) {
327
- findings.push({
328
- kind: 'missing-row',
329
- listener: pascalName,
330
- detail: `listener file present in code but no matching row in LIFECYCLE.md listener table`,
331
- });
332
- continue;
333
- }
334
- const diff = diffListenerEvents({ code, doc });
335
- if (diff.wildcardMismatch) {
336
- findings.push({
337
- kind: 'wildcard-mismatch',
338
- listener: pascalName,
339
- detail: `listener registers a wildcard or dynamic subscription in code but the doc row carries no \`*\` token`,
340
- });
341
- }
342
- if (diff.codeOnly.length > 0 || diff.docOnly.length > 0) {
343
- findings.push({
344
- kind: 'event-drift',
345
- listener: pascalName,
346
- codeOnly: diff.codeOnly,
347
- docOnly: diff.docOnly,
348
- });
349
- }
350
- }
351
-
352
- for (const [docName] of docRows) {
353
- if (codePascalNames.has(docName)) continue;
354
- if (EXEMPT_DOC_ROWS.has(docName)) continue;
355
- findings.push({
356
- kind: 'unknown-row',
357
- listener: docName,
358
- detail: `LIFECYCLE.md lists this listener but no matching file exists under listeners/`,
359
- });
360
- }
361
-
362
- return findings;
363
- }
364
-
365
- /**
366
- * Format a single finding as a human-readable diff line.
367
- *
368
- * @param {{ kind: string, listener: string, detail?: string, codeOnly?: string[], docOnly?: string[] }} f
369
- * @returns {string}
370
- */
371
- export function formatFinding(f) {
372
- if (f.kind === 'event-drift') {
373
- const parts = [];
374
- if (f.codeOnly && f.codeOnly.length > 0) {
375
- parts.push(`code-only: [${f.codeOnly.join(', ')}]`);
376
- }
377
- if (f.docOnly && f.docOnly.length > 0) {
378
- parts.push(`doc-only: [${f.docOnly.join(', ')}]`);
379
- }
380
- return `[lifecycle-doc-drift][event-drift] ${f.listener}\n ${parts.join(' | ')}`;
381
- }
382
- return `[lifecycle-doc-drift][${f.kind}] ${f.listener}\n ${f.detail ?? ''}`;
383
- }
384
-
385
- async function main() {
386
- const findings = checkLifecycleDocDrift();
387
- if (findings.length === 0) {
388
- process.stdout.write(
389
- '[lifecycle-doc-drift] clean: listener subscriptions match docs/LIFECYCLE.md listener-model table.\n',
390
- );
391
- return 0;
392
- }
393
- for (const f of findings) {
394
- process.stderr.write(`${formatFinding(f)}\n`);
395
- }
396
- return 1;
397
- }
398
-
399
- await runAsCli(import.meta.url, main, {
400
- source: 'check-lifecycle-doc-drift',
401
- propagateExitCode: true,
402
- usage: {
403
- invocation: 'node .agents/scripts/check-lifecycle-doc-drift.js',
404
- summary:
405
- "Fail when the listener-model table in docs/LIFECYCLE.md drifts from the lifecycle listeners' declared subscriptions.",
406
- flags: [],
407
- notes: [
408
- 'Exit codes:\n 0 doc and code agree\n 1 drift in either direction',
409
- ],
410
- },
411
- });
@@ -1,64 +0,0 @@
1
- /**
2
- * lib/audit-suite/cli.js — Argv parsing + help text for run-audit-suite.
3
- *
4
- * Extracted from `.agents/scripts/run-audit-suite.js` (Story #963, Epic #946).
5
- * No IO, no provider call. The entry-point composes these helpers with the
6
- * runner from `./runner.js`.
7
- */
8
-
9
- import { parseArgs } from 'node:util';
10
-
11
- export const HELP = `Usage: node .agents/scripts/run-audit-suite.js \\
12
- --audits <comma-list> [--ticket <id>] [--base-branch main] \\
13
- [--substitution key=value]... [--run-id <id>]
14
-
15
- Flags:
16
- --audits Comma-separated audit workflow names (required).
17
- --ticket Ticket id used for the {{ticketId}} substitution (optional).
18
- --base-branch Value used for the {{baseBranch}} substitution (default: main).
19
- --substitution Repeatable key=value substitution (e.g. --substitution alphaKey=val).
20
- Allowed keys are the built-ins (auditOutputDir, ticketId, baseBranch,
21
- changedFiles) plus any substitutionKeys declared on the requested audits.
22
- --run-id Optional artifact prefix. When set, full prompt bodies are written
23
- to <auditOutputDir>/audit-<run-id>-<audit>.md (defaults to
24
- temp/audits/) so downstream agents can read them.
25
- --help Show this message.
26
- `;
27
-
28
- /**
29
- * Pure: split a comma-separated `--audits` value into a clean workflow list.
30
- * Empty/whitespace tokens are dropped; nullish input yields an empty array.
31
- *
32
- * @param {string|null|undefined} commaList
33
- * @returns {string[]}
34
- */
35
- export function parseAuditList(commaList) {
36
- return String(commaList ?? '')
37
- .split(',')
38
- .map((a) => a.trim())
39
- .filter(Boolean);
40
- }
41
-
42
- /**
43
- * Parse the run-audit-suite CLI argv into a flat `values` object.
44
- * `strict: false` keeps the parser permissive so callers can pass extra
45
- * positional/unknown flags without aborting the run.
46
- *
47
- * @param {string[]} argv
48
- * @returns {Record<string, unknown>}
49
- */
50
- export function parseArgv(argv) {
51
- const { values } = parseArgs({
52
- args: argv,
53
- options: {
54
- audits: { type: 'string' },
55
- ticket: { type: 'string' },
56
- 'base-branch': { type: 'string' },
57
- substitution: { type: 'string', multiple: true },
58
- 'run-id': { type: 'string' },
59
- help: { type: 'boolean' },
60
- },
61
- strict: false,
62
- });
63
- return values;
64
- }
@@ -1,202 +0,0 @@
1
- /**
2
- * bootstrap/baselines-layout-migration — Story #1401 (Epic #1386),
3
- * re-targeted by Story #1467 (Epic #1179).
4
- *
5
- * Idempotent helper that brings a project's per-Epic ratchet snapshots into
6
- * the `temp/epic/<id>/baselines/` namespace. Three legacy shapes are
7
- * recognised and migrated:
8
- *
9
- * 1. Loose per-Epic snapshots at the baselines root
10
- * (`baselines/epic-<id>-{maintainability,crap}.json`).
11
- * 2. The flat prototype `baselines/snapshots/<id>/` tree.
12
- * 3. The committed `baselines/epic/<id>/` subdirectory shape that the
13
- * original Story #1396 introduced (now superseded — committed snapshots
14
- * accumulated obsolete entries forever because nothing pruned them).
15
- *
16
- * All three shapes are relocated under `<repoRoot>/temp/epic/<id>/baselines/`,
17
- * where they inherit the existing per-epic temp-tree cleanup contract:
18
- * `/deliver` reaps `temp/epic/<id>/` on merge, so the ratchet snapshots
19
- * are ephemeral scratch state — never committed, no manual prune.
20
- *
21
- * The main-tracked `baselines/{maintainability,crap}.json` files are NOT
22
- * touched — they remain at the root as the `main`-baseline contract
23
- * specifies.
24
- *
25
- * Pruning committed leftovers
26
- * ---------------------------
27
- * When the legacy `baselines/epic/<id>/` subdirectory shape is detected, the
28
- * helper invokes `git rm -r --quiet baselines/epic/<id>` (with
29
- * `--ignore-unmatch` for the untracked case) so the now-empty committed tree
30
- * is removed in the same operation. Callers commit the resulting working-tree
31
- * delta; on a clean repo (no committed `baselines/epic/`) the helper is a
32
- * filesystem-only no-op.
33
- *
34
- * The helper reports the per-Epic outcome so the workflow can summarise
35
- * exactly which snapshots moved and which were already in the target
36
- * shape. Re-running on an already-migrated tree produces zero mutations.
37
- *
38
- * @module bootstrap/baselines-layout-migration
39
- */
40
-
41
- import { spawnSync as defaultSpawnSync } from 'node:child_process';
42
- import fs from 'node:fs';
43
- import path from 'node:path';
44
-
45
- const BASELINE_FILE_RE = /^(maintainability|crap)\.json$/;
46
- const LOOSE_FILE_RE = /^epic-(\d+)-(maintainability|crap)\.json$/;
47
-
48
- /**
49
- * Move a single snapshot file to its temp-namespace target. When the target
50
- * already holds a canonical copy, the source is discarded instead of
51
- * overwriting.
52
- */
53
- function moveOneSnapshot({ from, to, label, moves }) {
54
- if (fs.existsSync(to)) {
55
- fs.rmSync(from);
56
- moves.push({ from, to, action: 'discarded-superseded' });
57
- return;
58
- }
59
- fs.mkdirSync(path.dirname(to), { recursive: true });
60
- fs.renameSync(from, to);
61
- moves.push({ from, to, action: label });
62
- }
63
-
64
- /**
65
- * Drop a directory if it exists on disk and is empty.
66
- */
67
- function dropEmptyDir(absDir) {
68
- if (fs.existsSync(absDir) && fs.readdirSync(absDir).length === 0) {
69
- fs.rmdirSync(absDir);
70
- }
71
- }
72
-
73
- /**
74
- * Walk a per-Epic source directory (`<srcRoot>/<id>/*.json`) and migrate
75
- * each baseline file into `<tempEpicRoot>/<id>/baselines/`. Returns the
76
- * Epic dirs that were touched so the caller can run per-Epic cleanup
77
- * (e.g. `git rm` for the committed shape).
78
- */
79
- function migrateEpicDir({ srcRoot, tempEpicRoot, label, moves }) {
80
- const touched = [];
81
- if (!fs.existsSync(srcRoot) || !fs.statSync(srcRoot).isDirectory()) {
82
- return touched;
83
- }
84
- for (const epicEnt of fs.readdirSync(srcRoot, { withFileTypes: true })) {
85
- if (!epicEnt.isDirectory() || !/^\d+$/.test(epicEnt.name)) continue;
86
- const epicId = epicEnt.name;
87
- const fromDir = path.join(srcRoot, epicId);
88
- const toDir = path.join(tempEpicRoot, epicId, 'baselines');
89
- for (const fileEnt of fs.readdirSync(fromDir, { withFileTypes: true })) {
90
- if (!fileEnt.isFile() || !BASELINE_FILE_RE.test(fileEnt.name)) continue;
91
- moveOneSnapshot({
92
- from: path.join(fromDir, fileEnt.name),
93
- to: path.join(toDir, fileEnt.name),
94
- label,
95
- moves,
96
- });
97
- }
98
- dropEmptyDir(fromDir);
99
- touched.push({ epicId, fromDir });
100
- }
101
- return touched;
102
- }
103
-
104
- /**
105
- * Migrate shape 1: loose per-Epic snapshots at the root of `baselines/`.
106
- */
107
- function migrateLooseShape({ baselinesDir, tempEpicRoot, moves }) {
108
- for (const ent of fs.readdirSync(baselinesDir, { withFileTypes: true })) {
109
- if (!ent.isFile()) continue;
110
- const m = ent.name.match(LOOSE_FILE_RE);
111
- if (!m) continue;
112
- const [, epicId, gate] = m;
113
- moveOneSnapshot({
114
- from: path.join(baselinesDir, ent.name),
115
- to: path.join(tempEpicRoot, epicId, 'baselines', `${gate}.json`),
116
- label: 'relocated-loose',
117
- moves,
118
- });
119
- }
120
- }
121
-
122
- /**
123
- * Stage the prune of a now-empty committed `baselines/epic/<id>/` via
124
- * `git rm -r --quiet --ignore-unmatch`. `--ignore-unmatch` keeps the call
125
- * safe when the path is not tracked (fresh-clone case).
126
- */
127
- function pruneCommittedEpic({ fromDir, repoRoot, spawnSync, prunedDirs }) {
128
- const epicRelPath = path
129
- .relative(repoRoot, fromDir)
130
- .split(path.sep)
131
- .join('/');
132
- const rm = spawnSync(
133
- 'git',
134
- ['rm', '-r', '--quiet', '--ignore-unmatch', '--', epicRelPath],
135
- { cwd: repoRoot, encoding: 'utf-8', stdio: 'pipe', shell: false },
136
- );
137
- prunedDirs.push({ path: epicRelPath, gitStatus: rm.status ?? null });
138
- }
139
-
140
- /**
141
- * Detect and migrate any legacy per-Epic snapshot under `baselinesDir` into
142
- * the `temp/epic/<id>/baselines/` shape under `repoRoot`. Returns one of
143
- * three actions per discovered snapshot.
144
- *
145
- * @param {object} args
146
- * @param {string} args.baselinesDir - Absolute path to the project's
147
- * `baselines/` directory (the legacy source-of-snapshots locator).
148
- * @param {string} [args.repoRoot] - Absolute path to the project root.
149
- * Defaults to `path.dirname(baselinesDir)` so existing callers that only
150
- * pass `baselinesDir` continue to work for repos where `baselines/` sits
151
- * directly under the repo root (the canonical layout).
152
- * @param {typeof defaultSpawnSync} [args.spawnSync] - Injected for tests.
153
- */
154
- export function migrateBaselinesLayout(args) {
155
- const baselinesDir = args.baselinesDir;
156
- const repoRoot = args.repoRoot ?? path.dirname(baselinesDir);
157
- const spawnSync = args.spawnSync ?? defaultSpawnSync;
158
- const moves = [];
159
- const prunedDirs = [];
160
-
161
- if (!fs.existsSync(baselinesDir)) {
162
- return { action: 'no-baselines-dir', moves, prunedDirs };
163
- }
164
-
165
- const tempEpicRoot = path.join(repoRoot, 'temp', 'epic');
166
-
167
- // Shape 1: loose per-Epic snapshots at the root.
168
- migrateLooseShape({ baselinesDir, tempEpicRoot, moves });
169
-
170
- // Shape 2: prototype `baselines/snapshots/<id>/` tree.
171
- const protoRoot = path.join(baselinesDir, 'snapshots');
172
- migrateEpicDir({
173
- srcRoot: protoRoot,
174
- tempEpicRoot,
175
- label: 'relocated-prototype',
176
- moves,
177
- });
178
- dropEmptyDir(protoRoot);
179
-
180
- // Shape 3: committed `baselines/epic/<id>/` subdirectory layout (the
181
- // shape Story #1396 introduced; superseded by the temp-namespace
182
- // contract in Story #1467). Move snapshots OUT to temp, prune each
183
- // committed per-Epic dir via `git rm`, then drop the parent.
184
- const committedEpicRoot = path.join(baselinesDir, 'epic');
185
- const touched = migrateEpicDir({
186
- srcRoot: committedEpicRoot,
187
- tempEpicRoot,
188
- label: 'relocated-committed',
189
- moves,
190
- });
191
- for (const { fromDir } of touched) {
192
- pruneCommittedEpic({ fromDir, repoRoot, spawnSync, prunedDirs });
193
- }
194
- dropEmptyDir(committedEpicRoot);
195
-
196
- return {
197
- action:
198
- moves.length > 0 || prunedDirs.length > 0 ? 'migrated' : 'no-change',
199
- moves,
200
- prunedDirs,
201
- };
202
- }