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,212 +0,0 @@
1
- /**
2
- * bootstrap/ci-workflow-template — Story #1401 (Epic #1386)
3
- *
4
- * Renders the stabilized-quality-gates CI workflow YAML that
5
- * `/agents-bootstrap-github` writes (or refreshes) on a downstream project's
6
- * `.github/workflows/ci.yml`. Two design rules drive the shape:
7
- *
8
- * 1. **Lean on the diff-scoped default.** Story #1394 flipped both gate
9
- * CLIs (`check-maintainability.js`, `check-crap.js`) to default
10
- * `--changed-since=main`. The template no longer passes `--changed-since`
11
- * explicitly on the per-PR job — the default already produces the right
12
- * diff scope and an explicit re-statement risks drifting from the
13
- * framework default at the next bump. The push-to-main job still passes
14
- * `--full-scope` so a self-diff doesn't degrade to "no files in diff".
15
- *
16
- * 2. **Pin the baseline read to the PR's base branch via `BASELINE_REF`.**
17
- * Inside `/deliver` the close-validation chain sets the same env var
18
- * (`close-validation/gates.js` → `BASELINE_REF: origin/<baseBranch>`);
19
- * on CI the equivalent surface is the PR's base branch. This is an
20
- * ENV VAR, not a CLI flag: `check-baselines.js` resolves it through
21
- * `resolveDispatchScope`, and the `--epic-ref` flag this template used
22
- * to pass never existed on it — it was left over from the separate
23
- * `check-crap.js` / `check-maintainability.js` CLIs that were unified
24
- * away, and it named the ref `epic/<base>`, a branch the Story-only
25
- * model never creates. When `BASELINE_REF` is empty (push-to-base,
26
- * where there is no base to compare against) both gates fall through to
27
- * the working-tree baseline, so the expansion is always safe.
28
- *
29
- * The renderer is a pure string template so the integration test can assert
30
- * on its exact shape without instantiating a YAML parser.
31
- *
32
- * @module bootstrap/ci-workflow-template
33
- */
34
-
35
- import fs from 'node:fs';
36
- import path from 'node:path';
37
-
38
- /**
39
- * @typedef {object} CiTemplateOptions
40
- * @property {string} [nodeVersion='22'] - Node major to install via setup-node.
41
- * @property {string} [baseRef='main'] - Default base ref the gate CLIs diff
42
- * against. Surfaces as the workflow's `BASE_REF` env so the template stays
43
- * tunable per consumer.
44
- * @property {boolean} [includeCrap=true] - Whether to render the CRAP step.
45
- * Some downstream projects keep CRAP off until they have coverage wired.
46
- */
47
-
48
- /**
49
- * Render the CI workflow YAML body. The output is deterministic and safe to
50
- * write byte-for-byte over an existing template — the bootstrap caller is
51
- * responsible for diff-aware merging when an operator has hand-edited
52
- * `ci.yml`.
53
- *
54
- * @param {CiTemplateOptions} [opts]
55
- * @returns {string}
56
- */
57
- export function renderCiWorkflow(opts = {}) {
58
- const nodeVersion = opts.nodeVersion ?? '22';
59
- const baseRef = opts.baseRef ?? 'main';
60
- const includeCrap = opts.includeCrap !== false;
61
-
62
- const crapBlock = includeCrap
63
- ? `
64
- - name: CRAP Check
65
- # Diff-scoped via the framework default (Story #1394 flipped
66
- # --changed-since to '${baseRef}'). On PRs we let the default win;
67
- # on push-to-main we pass --full-scope so a self-diff doesn't
68
- # collapse to an empty file set. BASELINE_REF makes the gate read
69
- # the baseline at the base branch tip via
70
- # baseline-loader.readBaselineAtRef.
71
- env:
72
- BASELINE_REF: \${{ env.BASELINE_REF }}
73
- run: |
74
- mkdir -p temp
75
- if [[ "\${{ github.event_name }}" == "pull_request" ]]; then
76
- npm run crap:check -- --json temp/crap-report.json
77
- else
78
- npm run crap:check -- --full-scope --json temp/crap-report.json
79
- fi
80
-
81
- - name: Upload CRAP Report
82
- uses: actions/upload-artifact@v4
83
- if: always()
84
- with:
85
- name: crap-report
86
- path: temp/crap-report.json
87
- `
88
- : '';
89
-
90
- return `# Generated by agents-bootstrap-github (Epic #1386 / Story #1401).
91
- # Re-run /agents-bootstrap-github to refresh this file. Hand edits are
92
- # preserved by the bootstrap helper when it detects operator changes —
93
- # inspect the diff before accepting an overwrite.
94
- name: CI
95
-
96
- on:
97
- push:
98
- branches: [${baseRef}]
99
- pull_request:
100
- branches: [${baseRef}]
101
- types: [opened, synchronize, reopened]
102
- workflow_dispatch:
103
-
104
- concurrency:
105
- group: \${{ github.workflow }}-\${{ github.ref }}
106
- cancel-in-progress: true
107
-
108
- jobs:
109
- validate:
110
- name: Validate and Test
111
- runs-on: ubuntu-latest
112
- permissions:
113
- contents: read
114
- env:
115
- # Story #1120: on a PR the gate CLIs read the canonical baseline at
116
- # the BASE branch tip via --baseline-ref (git show), not from the
117
- # PR head's working tree — a branch that edited its own baseline
118
- # must not be allowed to grade itself against the edit.
119
- # Empty on push-to-${baseRef} (there is no base to compare against),
120
- # where both gates fall through to the working-tree baseline, so the
121
- # env-var expansion is always safe.
122
- BASELINE_REF: \${{ github.event_name == 'pull_request' && format('origin/{0}', github.event.pull_request.base.ref) || '' }}
123
- BASE_REF: ${baseRef}
124
- steps:
125
- - name: Checkout Code
126
- uses: actions/checkout@v4
127
- with:
128
- fetch-depth: 0
129
-
130
- - name: Setup Node.js
131
- uses: actions/setup-node@v4
132
- with:
133
- node-version: '${nodeVersion}'
134
- cache: 'npm'
135
-
136
- - name: Install Dependencies
137
- run: npm ci --ignore-scripts
138
-
139
- - name: Lint and Format
140
- run: |
141
- npm run lint
142
- npm run format:check
143
-
144
- - name: Maintainability Check
145
- # Story #1394 (Epic #1386): the gate CLI defaults --changed-since to
146
- # the project's BASE_REF, so PRs no longer pass --changed-since
147
- # explicitly. push-to-${baseRef} runs with --full-scope so the
148
- # diff-scoped default doesn't degrade to a self-diff (= empty file
149
- # set). BASELINE_REF pins the baseline read to the base branch tip.
150
- env:
151
- BASELINE_REF: \${{ env.BASELINE_REF }}
152
- run: |
153
- if [[ "\${{ github.event_name }}" == "pull_request" ]]; then
154
- npm run maintainability:check
155
- else
156
- npm run maintainability:check -- --full-scope
157
- fi
158
-
159
- - name: Run Tests
160
- run: npm test
161
- ${crapBlock}`;
162
- }
163
-
164
- /**
165
- * Default workflow filename emitted by the bootstrap. Surfaced as a constant
166
- * so the integration test can assert the bootstrap writes to the canonical
167
- * path without re-deriving it.
168
- */
169
- export const CI_WORKFLOW_RELATIVE_PATH = '.github/workflows/ci.yml';
170
-
171
- /**
172
- * Render the stabilized-quality-gates CI workflow template into a project
173
- * checkout. Idempotent on the byte level: when `.github/workflows/ci.yml`
174
- * already matches the rendered template, no write occurs and the action is
175
- * `unchanged`. When the file is absent the action is `created`. When the
176
- * file exists with operator-authored differences the helper preserves it
177
- * and returns `custom-workflow-skip` along with the rendered body so the
178
- * bootstrap caller (or `/mandrel-update`) can offer a side-by-side diff.
179
- *
180
- * Network-free; safe to invoke under tests with a tmp `projectRoot`.
181
- *
182
- * Moved from `agents-bootstrap-github.js` to this module so it lives
183
- * alongside `renderCiWorkflow` and `CI_WORKFLOW_RELATIVE_PATH`
184
- * (Story #4048 B2 — dead-code deletion from the bootstrap entry point).
185
- *
186
- * @param {object} args
187
- * @param {string} args.projectRoot - Repo root (must contain or accept
188
- * `.github/workflows/`).
189
- * @param {object} [args.template] - Forwarded to `renderCiWorkflow`.
190
- * @param {boolean} [args.write=true] - When `false`, the helper computes
191
- * the would-be action without touching disk. Used by the
192
- * bootstrap CLI's dry-run mode.
193
- * @returns {{ action: 'created'|'unchanged'|'custom-workflow-skip',
194
- * path: string, rendered: string }}
195
- */
196
- export function ensureCiWorkflow(args) {
197
- const projectRoot = args.projectRoot;
198
- const rendered = renderCiWorkflow(args.template);
199
- const target = path.join(projectRoot, CI_WORKFLOW_RELATIVE_PATH);
200
- if (!fs.existsSync(target)) {
201
- if (args.write !== false) {
202
- fs.mkdirSync(path.dirname(target), { recursive: true });
203
- fs.writeFileSync(target, rendered, 'utf8');
204
- }
205
- return { action: 'created', path: target, rendered };
206
- }
207
- const existing = fs.readFileSync(target, 'utf8');
208
- if (existing === rendered) {
209
- return { action: 'unchanged', path: target, rendered };
210
- }
211
- return { action: 'custom-workflow-skip', path: target, rendered };
212
- }
@@ -1,104 +0,0 @@
1
- /**
2
- * baseline-drift-main-checkout — refuse-and-print check.
3
- *
4
- * Detects the close-validation-main-drift failure mode where `story-close`
5
- * (or any close-validation step) runs against the **main checkout** while a
6
- * corresponding worktree exists. The active branch in the main checkout is
7
- * usually drifted from the Story branch by stale formatting / MI-baseline
8
- * commits, and the close validation then refuses to merge.
9
- *
10
- * Pre-existing epic-branch format drift blocks every story close when the
11
- * validator hits the main checkout.
12
- *
13
- * Surface: `story-close` preflight and the `/diagnose` CLI. Surfaced as a
14
- * blocker — if the validator is running against the wrong tree, none of the
15
- * downstream gates are reliable.
16
- *
17
- * AutoCorrect is `refuse-and-print`. The fix is for the *operator* to `cd`
18
- * into the worktree; an auto-fix would mutate the process's cwd, which is
19
- * a footgun no other check assumes.
20
- *
21
- * Detection logic:
22
- * 1. If headRef is not a story branch (`story-<n>`), the check does not
23
- * apply — only story-close runs in this scope, and only against a
24
- * story branch. Returns null.
25
- * 2. If a worktree at `.worktrees/<headRef>/` does NOT exist, the check
26
- * cannot say anything — the operator may legitimately be running in
27
- * single-tree mode. Returns null.
28
- * 3. If the active cwd is the worktree path, all good. Returns null.
29
- * 4. Otherwise the validator is running against a different tree (the
30
- * main checkout) while the worktree exists. Returns a blocker with
31
- * the `cd <worktree>` recipe.
32
- */
33
-
34
- import path from 'node:path';
35
- import { isStoryBranch as isFlatStoryBranch } from '../git-utils.js';
36
-
37
- const ID = 'baseline-drift-main-checkout';
38
-
39
- export default {
40
- id: ID,
41
- severity: 'blocker',
42
- scope: ['story-close', 'diagnose'],
43
- autoCorrect: 'refuse-and-print',
44
- detect(state) {
45
- const headRef = state?.git?.headRef ?? null;
46
- if (!headRef || !isStoryBranch(headRef)) return null;
47
- const worktreePaths = state?.fs?.worktreePaths ?? [];
48
- if (!Array.isArray(worktreePaths) || worktreePaths.length === 0) {
49
- return null;
50
- }
51
- const expected = worktreePaths.find((p) => path.basename(p) === headRef);
52
- if (!expected) return null; // worktree for this branch doesn't exist
53
- const cwd = state?.cwd ?? '';
54
- if (samePath(cwd, expected)) return null; // already in the worktree
55
- return {
56
- id: ID,
57
- severity: 'blocker',
58
- scope: state?.scope ?? '',
59
- summary: `Close-validation running outside worktree (cwd: ${cwd}, expected: ${expected})`,
60
- detail: [
61
- `The active branch is ${headRef} and a worktree exists at ${expected},`,
62
- `but the current working directory is ${cwd}.`,
63
- '',
64
- 'Close-validation must run inside the Story worktree — the main checkout',
65
- 'may carry stale formatting or MI-baseline drift that will fail validation',
66
- 'even when the Story branch is clean.',
67
- ].join('\n'),
68
- fixCommand: `cd "${expected}"`,
69
- autoCorrectable: false,
70
- };
71
- },
72
- };
73
-
74
- /**
75
- * Story branches use a flat `story-<n>` shape; the older
76
- * `story/epic-<id>/<n>` layout no longer applies. Accept both for forward
77
- * compatibility.
78
- *
79
- * @param {string} ref
80
- * @returns {boolean}
81
- */
82
- function isStoryBranch(ref) {
83
- return isFlatStoryBranch(ref) || /^story\/epic-\d+\/\d+$/.test(ref);
84
- }
85
-
86
- /**
87
- * Compare two paths normalized for the platform. Windows is case-insensitive
88
- * and uses backslashes; POSIX is case-sensitive. We normalize both sides
89
- * with `path.resolve` so the comparison is stable regardless of trailing
90
- * separators or `./` prefixes.
91
- *
92
- * @param {string} a
93
- * @param {string} b
94
- * @returns {boolean}
95
- */
96
- function samePath(a, b) {
97
- if (!a || !b) return false;
98
- const na = path.resolve(a);
99
- const nb = path.resolve(b);
100
- if (process.platform === 'win32') {
101
- return na.toLowerCase() === nb.toLowerCase();
102
- }
103
- return na === nb;
104
- }
@@ -1,106 +0,0 @@
1
- /**
2
- * push-hook-parity — refuse-and-print check.
3
- *
4
- * Detects when the pre-push gates (biome check, MI gate) would fail at
5
- * `git push` time, BEFORE merge commits land. Biome check and the MI
6
- * gate fire at push time (not lint time), so an apparently-clean commit
7
- * cascade can still get stuck at the actual push step.
8
- *
9
- * Surface: `/diagnose` CLI. The gates execute against HEAD in dry-run
10
- * mode (no working-tree mutation) and report pass/fail. This check is
11
- * intentionally scoped to `diagnose` rather than `story-close` because:
12
- *
13
- * - story-close already runs the full validation chain (typecheck,
14
- * lint, test, format, MI, coverage, crap) against the worktree. The
15
- * gates are part of that chain. Running them twice in preflight is
16
- * wasteful.
17
- * - /diagnose is the surface where the operator wants a fast pre-push
18
- * verdict ("would this commit pass the push hook?"). That's exactly
19
- * what this check answers.
20
- *
21
- * The gate runs live in `state.js`'s `gates.*` projection so they are
22
- * memoized by `(scope, cwd)` and the heavy spawn cost is paid at most
23
- * once per assembly. Tests inject a fake `gates` probe to drive the
24
- * specific pass/fail scenarios without spawning real binaries.
25
- *
26
- * AutoCorrect is `refuse-and-print`. The fix is "run the gate locally
27
- * and address its findings" — there is no safe auto-fix because we
28
- * cannot mutate the working tree to satisfy MI baseline regressions
29
- * without making domain decisions about which file to touch.
30
- */
31
-
32
- const ID = 'push-hook-parity';
33
-
34
- export default {
35
- id: ID,
36
- severity: 'blocker',
37
- scope: ['diagnose'],
38
- autoCorrect: 'refuse-and-print',
39
- detect(state) {
40
- const gates = state?.gates ?? {};
41
- const biome = gates.biome;
42
- const miGate = gates.miGate;
43
- // If neither gate ran (state.js was assembled for a scope without
44
- // gates.* keys), the check has nothing to say. Returning null avoids
45
- // a false positive when the consumer happens to share state across
46
- // scopes.
47
- if (!biome && !miGate) return null;
48
- const failed = [];
49
- if (biome && !biome.ok)
50
- failed.push({ name: 'biome', output: biome.output });
51
- if (miGate && !miGate.ok)
52
- failed.push({ name: 'miGate', output: miGate.output });
53
- if (failed.length === 0) return null;
54
- const failedNames = failed.map((f) => f.name).join(' + ');
55
- const detailLines = failed.map(
56
- (f) => ` - ${f.name}:\n${indent(f.output ?? '(no output)')}`,
57
- );
58
- return {
59
- id: ID,
60
- severity: 'blocker',
61
- scope: state?.scope ?? '',
62
- summary: `pre-push gate(s) would fail at push time: ${failedNames}`,
63
- detail: [
64
- 'These gates run on `git push`, not on `git commit`, so the working',
65
- 'tree looks clean but the push will be rejected. Resolve before merging:',
66
- '',
67
- ...detailLines,
68
- ].join('\n'),
69
- fixCommand: buildFixCommand(failed.map((f) => f.name)),
70
- autoCorrectable: false,
71
- };
72
- },
73
- };
74
-
75
- /**
76
- * Build the literal command(s) the operator should run locally to see the
77
- * same failure the push hook would surface. Each gate has its own canonical
78
- * invocation; chaining with `&&` lets the operator copy-paste the whole
79
- * recipe as one line.
80
- *
81
- * @param {string[]} gateNames
82
- * @returns {string}
83
- */
84
- function buildFixCommand(gateNames) {
85
- const recipes = {
86
- biome: 'npx biome check .',
87
- miGate: 'npm run check:maintainability',
88
- };
89
- return gateNames
90
- .map((g) => recipes[g] ?? `# unknown gate: ${g}`)
91
- .join(' && ');
92
- }
93
-
94
- /**
95
- * Indent every line of `text` by two spaces so multi-line gate output
96
- * renders cleanly inside the bullet list in `detail`.
97
- *
98
- * @param {string} text
99
- * @returns {string}
100
- */
101
- function indent(text) {
102
- return String(text)
103
- .split('\n')
104
- .map((line) => ` ${line}`)
105
- .join('\n');
106
- }
@@ -1,92 +0,0 @@
1
- /**
2
- * windows-coverage-noise-floor — refuse-and-print warning check.
3
- *
4
- * Detects coverage-gate deltas that are within the Windows / Node-22
5
- * noise floor — i.e., tiny fractional branch-coverage swings that vary
6
- * run-to-run and would chase the gate to a different file if the
7
- * operator ratcheted from a flapping CI artifact. The canonical mistake
8
- * is to fetch a failing run's coverage delta and bump the baseline by
9
- * that delta, which simply moves the noise floor to another file rather
10
- * than stabilising the gate.
11
- *
12
- * Scope: 'story-close', 'retro'. The check is `warning` severity (not
13
- * `blocker`) because flapping is informational — the operator decides
14
- * whether to ratchet, skip, or investigate. Coverage-blocking belongs
15
- * to a separate baseline-refresh gate.
16
- *
17
- * The check is `refuse-and-print`. A `fix()` here would mean either
18
- * suppressing the flap (which silently lowers signal) or ratcheting
19
- * (which is the very mistake the check exists to prevent). The
20
- * fixCommand advises against ratcheting from a flapping artifact.
21
- *
22
- * Input shape — the check reads `state.coverage`:
23
- * {
24
- * branchDelta: number, // signed fractional delta (e.g. 0.0015 = +0.15%)
25
- * noiseFloor?: number, // optional override; default 0.0025
26
- * file?: string, // optional file the delta is attributed to
27
- * }
28
- *
29
- * Returns null when `state.coverage` is absent (the assembler does not
30
- * always populate coverage; absent input is a no-op rather than a
31
- * failure).
32
- */
33
-
34
- const DEFAULT_NOISE_FLOOR = 0.0025; // 0.25%
35
-
36
- const FIX_COMMAND = [
37
- '# Do NOT ratchet the coverage baseline from a flapping CI artifact.',
38
- '# A delta within the Windows / Node-22 noise floor (< 0.25% by default)',
39
- '# is run-to-run jitter, not real coverage loss. Ratcheting in response',
40
- '# moves the noise floor to a different file rather than fixing it.',
41
- '#',
42
- '# Instead: re-run the gate. If the delta persists across multiple runs',
43
- '# in the same file, investigate the specific test that flipped — most',
44
- '# often a Date.now() / Math.random() leak into the branch decision.',
45
- '#',
46
- '# If the delta is real but small, capture a fresh baseline-refresh',
47
- '# commit deliberately, separate from the failing PR.',
48
- ].join('\n');
49
-
50
- export default {
51
- id: 'windows-coverage-noise-floor',
52
- severity: 'warning',
53
- scope: ['story-close', 'retro'],
54
- autoCorrect: 'refuse-and-print',
55
-
56
- detect(state) {
57
- const cov = state?.coverage;
58
- if (!cov || typeof cov !== 'object') return null;
59
- if (typeof cov.branchDelta !== 'number' || Number.isNaN(cov.branchDelta)) {
60
- return null;
61
- }
62
- const noiseFloor =
63
- typeof cov.noiseFloor === 'number' && cov.noiseFloor > 0
64
- ? cov.noiseFloor
65
- : DEFAULT_NOISE_FLOOR;
66
- const absDelta = Math.abs(cov.branchDelta);
67
- // Within (or equal to) the floor → flapping → warning.
68
- // Above the floor → real signal → return null and let the actual
69
- // coverage gate decide.
70
- if (absDelta > noiseFloor) {
71
- return null;
72
- }
73
- const deltaPct = (cov.branchDelta * 100).toFixed(3);
74
- const floorPct = (noiseFloor * 100).toFixed(3);
75
- const fileNote = cov.file ? ` (file: ${cov.file})` : '';
76
- return {
77
- id: 'windows-coverage-noise-floor',
78
- severity: 'warning',
79
- scope: state?.scope ?? 'story-close',
80
- summary: `Branch-coverage delta ${deltaPct}% is within the Windows noise floor (${floorPct}%) — likely flap, do not ratchet${fileNote}`,
81
- detail: [
82
- `branchDelta: ${cov.branchDelta}`,
83
- `noiseFloor: ${noiseFloor}`,
84
- cov.file ? `attributedFile: ${cov.file}` : null,
85
- ]
86
- .filter(Boolean)
87
- .join('\n'),
88
- fixCommand: FIX_COMMAND,
89
- autoCorrectable: false,
90
- };
91
- },
92
- };
@@ -1,81 +0,0 @@
1
- /**
2
- * worktree-bootstrap-env — refuse-and-print check.
3
- *
4
- * Detects active worktrees that are missing the `.env` or `.mcp.json`
5
- * bootstrap files. These files are intentionally untracked in git, so a
6
- * freshly-created worktree under `.worktrees/<story>/` does not get them
7
- * unless the orchestration explicitly copies them. When they are missing,
8
- * tests that read environment variables silently take a different code
9
- * path.
10
- *
11
- * Surface: the `/diagnose` CLI. Surfaced as a
12
- * warning rather than a blocker — the missing file may be a deliberate
13
- * choice (e.g. a worktree that doesn't need MCP), but the operator should
14
- * see the divergence from the main checkout.
15
- *
16
- * AutoCorrect is `refuse-and-print`. Copying `.env` between trees is
17
- * defensible at the orchestration layer (`worktree.bootstrap` step) but
18
- * not as a self-healing fix — the contents are project-specific and may
19
- * include secrets the check explicitly never reads.
20
- *
21
- * Privacy: this check reads only the bootstrap *status* projection
22
- * (`fs.worktreeBootstrapStatus`) which records presence per worktree, not
23
- * file contents. The Finding's `detail` never exposes any env-var value.
24
- */
25
-
26
- const ID = 'worktree-bootstrap-env';
27
-
28
- export default {
29
- id: ID,
30
- severity: 'warning',
31
- scope: ['diagnose'],
32
- autoCorrect: 'refuse-and-print',
33
- detect(state) {
34
- const status = state?.fs?.worktreeBootstrapStatus ?? {};
35
- const entries = Object.entries(status);
36
- if (entries.length === 0) return null;
37
- const missing = entries
38
- .map(([wtPath, files]) => {
39
- const missingFiles = [];
40
- if (!files.dotEnv) missingFiles.push('.env');
41
- if (!files.dotMcp) missingFiles.push('.mcp.json');
42
- return { wtPath, missingFiles };
43
- })
44
- .filter((entry) => entry.missingFiles.length > 0);
45
- if (missing.length === 0) return null;
46
- const detailLines = missing.map(
47
- (m) => ` - ${m.wtPath}: missing ${m.missingFiles.join(', ')}`,
48
- );
49
- return {
50
- id: ID,
51
- severity: 'warning',
52
- scope: state?.scope ?? '',
53
- summary: `${missing.length} worktree(s) missing bootstrap files (.env / .mcp.json)`,
54
- detail: [
55
- 'Worktrees with missing bootstrap files:',
56
- ...detailLines,
57
- '',
58
- 'Tests that read these vars will silently degrade in the affected worktrees.',
59
- ].join('\n'),
60
- fixCommand: buildFixCommand(missing),
61
- autoCorrectable: false,
62
- };
63
- },
64
- };
65
-
66
- /**
67
- * Build a copy recipe the operator can run from the main checkout. Each
68
- * missing file gets its own `cp` line targeting the affected worktree.
69
- *
70
- * @param {Array<{wtPath: string, missingFiles: string[]}>} missing
71
- * @returns {string}
72
- */
73
- function buildFixCommand(missing) {
74
- const lines = [];
75
- for (const { wtPath, missingFiles } of missing) {
76
- for (const file of missingFiles) {
77
- lines.push(`cp "${file}" "${wtPath}/${file}"`);
78
- }
79
- }
80
- return lines.join(' && ');
81
- }
@@ -1,55 +0,0 @@
1
- /**
2
- * worktree-residue-biome — refuse-and-print check.
3
- *
4
- * Detects partial-reap residue under `.worktrees/` where a previous Story's
5
- * tree was deleted incompletely and left a nested `biome.json` behind. The
6
- * root `npm run lint` walks into the orphan tree, hits the nested config,
7
- * and fails. The close-time biome block is the canonical symptom.
8
- *
9
- * Surface: `story-close` preflight, the `/diagnose` CLI, and retro.
10
- * Surfaced as a blocker so the operator clears the residue
11
- * with `rm -rf` before close re-runs the validation chain.
12
- *
13
- * AutoCorrect is `refuse-and-print`. Recursive deletion under `.worktrees/`
14
- * is technically blessed by the README, but the residue may include
15
- * partially-merged work the operator wants to inspect first. We print the
16
- * exact `rm -rf` recipe rather than running it.
17
- */
18
-
19
- const ID = 'worktree-residue-biome';
20
-
21
- export default {
22
- id: ID,
23
- severity: 'blocker',
24
- scope: ['story-close', 'diagnose', 'retro'],
25
- autoCorrect: 'refuse-and-print',
26
- detect(state) {
27
- const orphans = state?.fs?.worktreeBiomeOrphans ?? [];
28
- if (!Array.isArray(orphans) || orphans.length === 0) return null;
29
- const lines = orphans.map((p) => ` - ${p}`).join('\n');
30
- return {
31
- id: ID,
32
- severity: 'blocker',
33
- scope: state?.scope ?? '',
34
- summary: `${orphans.length} orphan worktree(s) with nested biome.json — root biome lint will fail`,
35
- detail: `Nested biome.json files found under .worktrees/ subdirectories:\n${lines}\n\nThese block root \`npm run lint\` because biome walks into them and resolves the nested config.`,
36
- fixCommand: buildFixCommand(orphans),
37
- autoCorrectable: false,
38
- };
39
- },
40
- };
41
-
42
- /**
43
- * Build the `rm -rf` command(s) the operator should run to clear the
44
- * residue. Multiple orphans get a single chained command so it can be
45
- * copy-pasted as one line.
46
- *
47
- * @param {string[]} orphans
48
- * @returns {string}
49
- */
50
- function buildFixCommand(orphans) {
51
- if (orphans.length === 0) return '';
52
- // Each path is already an absolute orphan worktree directory; deleting
53
- // the whole directory is the canonical reap recipe.
54
- return orphans.map((p) => `rm -rf "${p}"`).join(' && ');
55
- }