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,306 +0,0 @@
1
- /**
2
- * lib/mutation/stryker-runner.js — Invoke Stryker and parse the mutation
3
- * report (Story #1736, Task #1754).
4
- *
5
- * The runner is dependency-injection-friendly so unit tests never have to
6
- * spawn a real Stryker process. The default `spawnFn` is
7
- * `child_process.spawnSync`, the default `fsImpl` is `node:fs`, and the
8
- * default `clock` is `Date.now`.
9
- *
10
- * Output contract:
11
- *
12
- * { ok: true, mutationScore: <number>, byWorkspace: { "*": <number>, ... },
13
- * reportPath, durationMs }
14
- * { ok: false, skipped: true, reason: <string> }
15
- * { ok: false, skipped: false, error: <string>, durationMs }
16
- *
17
- * Stryker writes its JSON report by default to `reports/mutation/mutation.json`.
18
- * Consumers that relocate the report can pass `reportPath`. We never parse
19
- * the much larger per-mutant HTML report — only the JSON summary.
20
- *
21
- * `timeoutMs` is honoured via `spawnSync`'s `timeout` option; on TIMEOUT
22
- * we surface an `ok: false` result rather than throwing so the caller can
23
- * fold the failure into a single uniform "[mutation] failed" gate line.
24
- */
25
-
26
- import { spawnSync as defaultSpawnSync } from 'node:child_process';
27
- import fs from 'node:fs';
28
- import path from 'node:path';
29
-
30
- import { detectStrykerConfig as defaultDetectStrykerConfig } from './config-detector.js';
31
-
32
- /** Default per-gate timeout when none is supplied (15 minutes). */
33
- export const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
34
-
35
- /** Default Stryker JSON report path (mirrors the Stryker default). */
36
- export const DEFAULT_REPORT_PATH = 'reports/mutation/mutation.json';
37
-
38
- /**
39
- * @typedef {Object} RunResult
40
- * @property {boolean} ok
41
- * @property {boolean} [skipped]
42
- * @property {string} [reason]
43
- * @property {string} [error]
44
- * @property {number} [mutationScore]
45
- * @property {Record<string, number>} [byWorkspace]
46
- * @property {string} [reportPath]
47
- * @property {number} [durationMs]
48
- * @property {number} [exitCode]
49
- */
50
-
51
- /**
52
- * Run Stryker and return a normalised summary.
53
- *
54
- * @param {{
55
- * cwd?: string,
56
- * configPath?: string | null,
57
- * reportPath?: string,
58
- * timeoutMs?: number,
59
- * workspace?: string,
60
- * spawnFn?: typeof defaultSpawnSync,
61
- * fsImpl?: typeof fs,
62
- * clock?: () => number,
63
- * strykerCmd?: string,
64
- * strykerArgs?: string[],
65
- * detectFn?: typeof defaultDetectStrykerConfig,
66
- * skipDetect?: boolean,
67
- * }} [opts]
68
- * @returns {Promise<RunResult>}
69
- */
70
- export async function runStryker(opts = {}) {
71
- const cwd = opts.cwd ?? process.cwd();
72
- const reportPath = opts.reportPath ?? DEFAULT_REPORT_PATH;
73
- const timeoutMs = Number.isFinite(opts.timeoutMs)
74
- ? opts.timeoutMs
75
- : DEFAULT_TIMEOUT_MS;
76
- const workspace = opts.workspace ?? '*';
77
- const spawnFn = opts.spawnFn ?? defaultSpawnSync;
78
- const fsImpl = opts.fsImpl ?? fs;
79
- const clock = opts.clock ?? (() => Date.now());
80
- const detectFn = opts.detectFn ?? defaultDetectStrykerConfig;
81
-
82
- // Detection short-circuit: the runner is the single point of "no
83
- // Stryker config → skip" so callers can fold the same result into a
84
- // uniform "[mutation] skipped" gate line. `skipDetect` is the escape
85
- // hatch for tests and for callers that have already detected.
86
- let resolvedConfigPath = opts.configPath ?? null;
87
- if (!opts.skipDetect) {
88
- const detection = detectFn({
89
- cwd,
90
- configPath: resolvedConfigPath,
91
- fsImpl,
92
- });
93
- if (!detection.found) {
94
- return {
95
- ok: false,
96
- skipped: true,
97
- reason:
98
- detection.reason ??
99
- 'no Stryker config found. Run `npx stryker init` to enable.',
100
- };
101
- }
102
- resolvedConfigPath = detection.path;
103
- }
104
-
105
- // Default to `npx stryker run` — works whether Stryker is a local devDep
106
- // or a one-shot install. Consumers with a pinned executable can override.
107
- const cmd = opts.strykerCmd ?? 'npx';
108
- const args =
109
- opts.strykerArgs ??
110
- (resolvedConfigPath
111
- ? ['stryker', 'run', '--configFile', resolvedConfigPath]
112
- : ['stryker', 'run']);
113
-
114
- const startedAt = clock();
115
- const spawnResult = spawnFn(cmd, args, {
116
- cwd,
117
- encoding: 'utf-8',
118
- stdio: ['ignore', 'pipe', 'pipe'],
119
- timeout: timeoutMs,
120
- shell: process.platform === 'win32',
121
- });
122
- const durationMs = clock() - startedAt;
123
-
124
- // spawnSync sets `signal` to 'SIGTERM' when the timeout fires.
125
- if (
126
- spawnResult.signal === 'SIGTERM' ||
127
- spawnResult.error?.code === 'ETIMEDOUT'
128
- ) {
129
- return {
130
- ok: false,
131
- skipped: false,
132
- error: `Stryker timed out after ${timeoutMs}ms`,
133
- durationMs,
134
- exitCode: spawnResult.status ?? -1,
135
- };
136
- }
137
-
138
- if (spawnResult.error) {
139
- return {
140
- ok: false,
141
- skipped: false,
142
- error: `failed to invoke Stryker: ${spawnResult.error.message}`,
143
- durationMs,
144
- };
145
- }
146
-
147
- if (spawnResult.status !== 0) {
148
- return {
149
- ok: false,
150
- skipped: false,
151
- error: `Stryker exited with status ${spawnResult.status}: ${
152
- spawnResult.stderr?.trim() || spawnResult.stdout?.trim() || 'no output'
153
- }`,
154
- durationMs,
155
- exitCode: spawnResult.status,
156
- };
157
- }
158
-
159
- const absReportPath = path.isAbsolute(reportPath)
160
- ? reportPath
161
- : path.resolve(cwd, reportPath);
162
-
163
- if (!fsImpl.existsSync(absReportPath)) {
164
- return {
165
- ok: false,
166
- skipped: false,
167
- error: `Stryker report not found at ${reportPath} — expected JSON reporter to be enabled`,
168
- durationMs,
169
- };
170
- }
171
-
172
- let parsed;
173
- try {
174
- parsed = JSON.parse(fsImpl.readFileSync(absReportPath, 'utf8'));
175
- } catch (err) {
176
- return {
177
- ok: false,
178
- skipped: false,
179
- error: `failed to parse Stryker report at ${reportPath}: ${err instanceof Error ? err.message : String(err)}`,
180
- durationMs,
181
- };
182
- }
183
-
184
- const summary = summariseReport(parsed, { workspace });
185
- if (!summary.ok) {
186
- return {
187
- ok: false,
188
- skipped: false,
189
- error: summary.error,
190
- durationMs,
191
- };
192
- }
193
-
194
- return {
195
- ok: true,
196
- mutationScore: summary.mutationScore,
197
- byWorkspace: summary.byWorkspace,
198
- reportPath: absReportPath,
199
- durationMs,
200
- };
201
- }
202
-
203
- /**
204
- * Pure: parse a Stryker mutation-report payload and return the overall
205
- * mutation score plus per-workspace breakdown. Exposed for unit testing.
206
- *
207
- * The Stryker JSON report shape includes a top-level `files` map keyed by
208
- * source path, where each entry carries an array of `mutants` with a
209
- * `status` field. The "mutation score" is the percentage of killed +
210
- * covered+failed-by-test mutants over the total tested mutants.
211
- *
212
- * Status values that count toward the score numerator: `Killed`.
213
- * Status values that count toward the denominator: `Killed`, `Survived`,
214
- * `Timeout`, `CompileError`, `RuntimeError`, `NoCoverage`.
215
- *
216
- * Stryker also exposes a precomputed `schema.MutationScoreResult` /
217
- * `metrics.mutationScore` in its `mutation-testing-elements` reports;
218
- * when present, prefer it over a hand-roll so we never disagree with the
219
- * Stryker dashboard. The hand-roll is the fallback for older reports
220
- * that omit the metrics block.
221
- *
222
- * @param {unknown} report
223
- * @param {{ workspace?: string }} [opts]
224
- * @returns {{ ok: true, mutationScore: number, byWorkspace: Record<string, number> } | { ok: false, error: string }}
225
- */
226
- export function summariseReport(report, opts = {}) {
227
- const workspace = opts.workspace ?? '*';
228
-
229
- if (!report || typeof report !== 'object' || Array.isArray(report)) {
230
- return { ok: false, error: 'Stryker report must be a JSON object' };
231
- }
232
-
233
- const reportObj = /** @type {Record<string, unknown>} */ (report);
234
-
235
- // Prefer Stryker's precomputed metrics block when present.
236
- const metrics = /** @type {Record<string, unknown> | undefined} */ (
237
- reportObj.metrics
238
- );
239
- if (
240
- metrics &&
241
- typeof metrics === 'object' &&
242
- typeof metrics.mutationScore === 'number' &&
243
- Number.isFinite(metrics.mutationScore)
244
- ) {
245
- return {
246
- ok: true,
247
- mutationScore: metrics.mutationScore,
248
- byWorkspace: { [workspace]: metrics.mutationScore },
249
- };
250
- }
251
-
252
- const files = /** @type {Record<string, unknown> | undefined} */ (
253
- reportObj.files
254
- );
255
- if (!files || typeof files !== 'object') {
256
- return {
257
- ok: false,
258
- error: "Stryker report missing 'files' map",
259
- };
260
- }
261
-
262
- let killed = 0;
263
- let total = 0;
264
- for (const entry of Object.values(files)) {
265
- if (!entry || typeof entry !== 'object') continue;
266
- const mutants = /** @type {{ mutants?: Array<{ status?: string }> }} */ (
267
- entry
268
- ).mutants;
269
- if (!Array.isArray(mutants)) continue;
270
- for (const m of mutants) {
271
- const status = m?.status;
272
- if (typeof status !== 'string') continue;
273
- if (countsTowardDenominator(status)) total += 1;
274
- if (status === 'Killed') killed += 1;
275
- }
276
- }
277
-
278
- if (total === 0) {
279
- return {
280
- ok: false,
281
- error: 'Stryker report has no scored mutants',
282
- };
283
- }
284
-
285
- const score = (killed / total) * 100;
286
- return {
287
- ok: true,
288
- mutationScore: round2(score),
289
- byWorkspace: { [workspace]: round2(score) },
290
- };
291
- }
292
-
293
- function countsTowardDenominator(status) {
294
- return (
295
- status === 'Killed' ||
296
- status === 'Survived' ||
297
- status === 'Timeout' ||
298
- status === 'CompileError' ||
299
- status === 'RuntimeError' ||
300
- status === 'NoCoverage'
301
- );
302
- }
303
-
304
- function round2(n) {
305
- return Math.round(n * 100) / 100;
306
- }
@@ -1,160 +0,0 @@
1
- /**
2
- * lib/mutation/survivor-report.js — enumerate actionable surviving mutants
3
- * from a Stryker JSON report (Story #3428).
4
- *
5
- * The mutation gate (`stryker-runner.js` → `summariseReport`) collapses the
6
- * report into a single score. That score tells you *whether* the suite is
7
- * leaky, not *where*. This helper does the complementary job: it walks the
8
- * same `files[].mutants[].status` surface and enumerates the mutants that
9
- * the suite failed to kill — `Survived` (a mutation that the tests ran over
10
- * but never caught) and `NoCoverage` (a mutation in a line no test exercises
11
- * at all) — grouped per file so the survivor-kill loop has concrete targets.
12
- *
13
- * It is **pure**: no network, no child process, no filesystem reads. The
14
- * caller hands in an already-parsed report object (the same JSON that
15
- * `stryker-runner.js` reads from `reports/mutation/mutation.json`). This
16
- * keeps the helper trivially unit-testable with inline fixtures and lets
17
- * remediation tooling reuse a report it already loaded for scoring.
18
- *
19
- * Output contract:
20
- *
21
- * {
22
- * ok: true,
23
- * totals: { survived, noCoverage, actionable, files },
24
- * files: [
25
- * {
26
- * file: "src/a.js",
27
- * survived: [{ id, line, mutatorName, status, ... }],
28
- * noCoverage: [{ ... }],
29
- * count: <survived.length + noCoverage.length>,
30
- * },
31
- * ...
32
- * ],
33
- * }
34
- * { ok: false, error: <string> }
35
- *
36
- * Files with no actionable survivors are omitted from `files`. The `files`
37
- * array is sorted by descending actionable count (worst offenders first),
38
- * ties broken by file path for stable output.
39
- */
40
-
41
- /** Mutant statuses this helper treats as actionable survivors. */
42
- export const ACTIONABLE_STATUSES = Object.freeze(['Survived', 'NoCoverage']);
43
-
44
- /**
45
- * Fields copied verbatim from each Stryker mutant onto the enumerated
46
- * survivor record. We deliberately keep a fixed allowlist (rather than
47
- * spreading the raw mutant) so the output shape is stable and never leaks
48
- * unexpected report internals.
49
- */
50
- const MUTANT_FIELDS = Object.freeze([
51
- 'id',
52
- 'mutatorName',
53
- 'status',
54
- 'location',
55
- 'replacement',
56
- ]);
57
-
58
- /**
59
- * Pure: enumerate `Survived` and `NoCoverage` mutants per file from a
60
- * parsed Stryker mutation report.
61
- *
62
- * @param {unknown} report Parsed Stryker JSON report (an object with a
63
- * top-level `files` map). Passing the raw JSON string is **not**
64
- * supported — parse it first (mirrors `summariseReport`).
65
- * @returns {(
66
- * { ok: true, totals: { survived: number, noCoverage: number, actionable: number, files: number }, files: Array<{ file: string, survived: object[], noCoverage: object[], count: number }> }
67
- * | { ok: false, error: string }
68
- * )}
69
- */
70
- export function enumerateSurvivors(report) {
71
- if (!report || typeof report !== 'object' || Array.isArray(report)) {
72
- return { ok: false, error: 'Stryker report must be a JSON object' };
73
- }
74
-
75
- const files = /** @type {Record<string, unknown> | undefined} */ (
76
- /** @type {Record<string, unknown>} */ (report).files
77
- );
78
- if (!files || typeof files !== 'object' || Array.isArray(files)) {
79
- return { ok: false, error: "Stryker report missing 'files' map" };
80
- }
81
-
82
- const perFile = [];
83
- let survivedTotal = 0;
84
- let noCoverageTotal = 0;
85
-
86
- for (const [filePath, entry] of Object.entries(files)) {
87
- if (!entry || typeof entry !== 'object') continue;
88
- const mutants = /** @type {{ mutants?: unknown }} */ (entry).mutants;
89
- if (!Array.isArray(mutants)) continue;
90
-
91
- const survived = [];
92
- const noCoverage = [];
93
- for (const mutant of mutants) {
94
- if (!mutant || typeof mutant !== 'object') continue;
95
- const status = /** @type {{ status?: unknown }} */ (mutant).status;
96
- if (status === 'Survived') {
97
- survived.push(pickMutant(mutant));
98
- } else if (status === 'NoCoverage') {
99
- noCoverage.push(pickMutant(mutant));
100
- }
101
- }
102
-
103
- if (survived.length === 0 && noCoverage.length === 0) continue;
104
-
105
- survivedTotal += survived.length;
106
- noCoverageTotal += noCoverage.length;
107
- perFile.push({
108
- file: filePath,
109
- survived,
110
- noCoverage,
111
- count: survived.length + noCoverage.length,
112
- });
113
- }
114
-
115
- perFile.sort((a, b) => b.count - a.count || a.file.localeCompare(b.file));
116
-
117
- return {
118
- ok: true,
119
- totals: {
120
- survived: survivedTotal,
121
- noCoverage: noCoverageTotal,
122
- actionable: survivedTotal + noCoverageTotal,
123
- files: perFile.length,
124
- },
125
- files: perFile,
126
- };
127
- }
128
-
129
- /**
130
- * Copy the stable allowlist of fields off a raw Stryker mutant. The `line`
131
- * convenience field is derived from `location.start.line` when present so
132
- * callers can sort/print without re-walking the nested location shape.
133
- *
134
- * @param {Record<string, unknown>} mutant
135
- * @returns {Record<string, unknown>}
136
- */
137
- function pickMutant(mutant) {
138
- const out = {};
139
- for (const field of MUTANT_FIELDS) {
140
- if (mutant[field] !== undefined) out[field] = mutant[field];
141
- }
142
- const line = extractLine(mutant.location);
143
- if (line !== null) out.line = line;
144
- return out;
145
- }
146
-
147
- /**
148
- * Pull the 1-based start line out of a Stryker `location` object, or null
149
- * when the shape is missing or malformed.
150
- *
151
- * @param {unknown} location
152
- * @returns {number | null}
153
- */
154
- function extractLine(location) {
155
- if (!location || typeof location !== 'object') return null;
156
- const start = /** @type {{ start?: unknown }} */ (location).start;
157
- if (!start || typeof start !== 'object') return null;
158
- const line = /** @type {{ line?: unknown }} */ (start).line;
159
- return typeof line === 'number' && Number.isFinite(line) ? line : null;
160
- }
@@ -1,170 +0,0 @@
1
- /**
2
- * Active-Story env-var propagation (Epic #1030 Story #1043 / Task #1061).
3
- *
4
- * The PreToolUse / PostToolUse trace hook in
5
- * `lib/observability/tool-trace-hook.js` resolves the active Story from
6
- * `process.env.CC_STORY_ID`. This module is the single writer/clearer of
7
- * that var:
8
- *
9
- * - `setActiveStoryEnv({ storyId, workCwd })` is called from
10
- * `single-story-init.js` after the worktree is materialised. It sets the
11
- * var on the current `process.env` (so any child commands the
12
- * orchestrator spawns inherit it) and exports it to a sibling
13
- * `.env.local` inside the worktree. The harness re-spawns the
14
- * agent with that file's contents loaded, so the trace hook fires
15
- * with the right ids on the *next* tool call after init returns.
16
- *
17
- * - `clearActiveStoryEnv({ workCwd })` is called from
18
- * `story-close/post-merge-close.js` after the merge lands. It
19
- * deletes the env vars from `process.env` and removes the
20
- * `.env.local` file. Tooling invoked outside an active Story —
21
- * planning, dispatch, ad-hoc CLI — must NOT pollute random
22
- * `traces.ndjson` files; the hook's no-op contract relies on the
23
- * vars being absent at that point.
24
- *
25
- * Both functions are best-effort and never throw on fs failures. The
26
- * trace hook is itself best-effort, so a stale `.env.local` would at
27
- * worst cause one extra trace line to land in a stale `temp/.../`
28
- * directory — annoying but not a correctness bug. We log warnings via
29
- * the caller's logger when provided.
30
- */
31
-
32
- import nodeFs from 'node:fs';
33
- import nodePath from 'node:path';
34
-
35
- const ENV_LOCAL_BASENAME = '.env.local';
36
-
37
- /**
38
- * Names of the env vars we own. Keeping the list central makes the
39
- * round-trip (set on init, clear on close) trivially auditable —
40
- * grep `CC_STORY_ID` / `CC_EPIC_ID` to find every read site.
41
- *
42
- * `CC_STORY_ID` is the one var this module writes. `CC_EPIC_ID` is retained
43
- * on the CLEAR path only: v2 has no Epics and nothing here sets it, but
44
- * `tool-trace-hook.js`'s `resolveActiveStory` still reads it, so wiping it at
45
- * close keeps the trace path's "no stale context past close" contract honest
46
- * against a value that leaked in from outside (a pre-upgrade `.env.local` the
47
- * harness already loaded, or an operator's shell).
48
- *
49
- * `CC_SLICE_ID` / `CC_OPERATOR` were removed with the heartbeat substrate:
50
- * their only writer was the never-called `setActiveSliceEnv` and their only
51
- * reader the deleted `hook-heartbeat.js`.
52
- */
53
- export const ACTIVE_STORY_ENV_KEYS = ['CC_EPIC_ID', 'CC_STORY_ID'];
54
-
55
- /**
56
- * Render the `.env.local` body. One `KEY=value` line per var, LF
57
- * line endings (the harness's dotenv parser tolerates CRLF too but LF
58
- * keeps the file deterministic across Windows / macOS / Linux).
59
- *
60
- * Only `CC_STORY_ID` is emitted: v2 Stories are standalone, so there is no
61
- * parent Epic to key. The trace hook's no-op contract reads "var absent from
62
- * env" as the signal, so no `CC_EPIC_ID=` line is written at all — an empty
63
- * one would set the var to the empty string and change the contract.
64
- *
65
- * Exported for testing.
66
- *
67
- * @param {{ storyId: number }} input
68
- * @returns {string}
69
- */
70
- export function renderActiveStoryEnvFile({ storyId }) {
71
- return [
72
- '# Auto-managed by .agents/scripts/lib/observability/active-story-env.js',
73
- '# Re-generated on every story-init; deleted on story-close.',
74
- `CC_STORY_ID=${storyId}`,
75
- '',
76
- ].join('\n');
77
- }
78
-
79
- /**
80
- * Set `CC_STORY_ID` on the current process and (when `workCwd` is provided)
81
- * export it to `<workCwd>/.env.local`.
82
- *
83
- * Idempotent: re-running with the same id is a no-op on disk; with a
84
- * different id the `.env.local` is overwritten.
85
- *
86
- * v2 Stories are standalone — there is no parent Epic — so `CC_EPIC_ID` is
87
- * never set, and any pre-existing value is removed: the trace hook's
88
- * `resolveActiveStory` no-op contract is keyed on the var's absence, and a
89
- * stale epic id would key this Story's traces to a foreign Epic directory.
90
- *
91
- * @param {{ storyId: number, workCwd?: string,
92
- * env?: NodeJS.ProcessEnv, fs?: typeof nodeFs,
93
- * logger?: { warn?: (m: string) => void } }} args
94
- * @returns {{ envSet: boolean, fileWritten: boolean, filePath: string|null }}
95
- */
96
- export function setActiveStoryEnv({
97
- storyId,
98
- workCwd,
99
- env = process.env,
100
- fs = nodeFs,
101
- logger,
102
- } = {}) {
103
- if (!Number.isInteger(storyId) || storyId <= 0) {
104
- throw new Error(
105
- `[active-story-env] storyId must be a positive integer; got ${storyId}`,
106
- );
107
- }
108
-
109
- if ('CC_EPIC_ID' in env) delete env.CC_EPIC_ID;
110
- env.CC_STORY_ID = String(storyId);
111
-
112
- let fileWritten = false;
113
- let filePath = null;
114
- if (typeof workCwd === 'string' && workCwd.length > 0) {
115
- filePath = nodePath.join(workCwd, ENV_LOCAL_BASENAME);
116
- try {
117
- fs.writeFileSync(filePath, renderActiveStoryEnvFile({ storyId }), {
118
- encoding: 'utf8',
119
- });
120
- fileWritten = true;
121
- } catch (err) {
122
- logger?.warn?.(
123
- `[active-story-env] Failed to write ${filePath}: ${
124
- err instanceof Error ? err.message : String(err)
125
- }`,
126
- );
127
- }
128
- }
129
- return { envSet: true, fileWritten, filePath };
130
- }
131
-
132
- /**
133
- * Delete `CC_EPIC_ID` / `CC_STORY_ID` from the current process env and
134
- * remove `<workCwd>/.env.local` when present. Best-effort; a missing
135
- * file is not an error.
136
- *
137
- * @param {{ workCwd?: string, env?: NodeJS.ProcessEnv,
138
- * fs?: typeof nodeFs,
139
- * logger?: { warn?: (m: string) => void } }} args
140
- * @returns {{ envCleared: boolean, fileRemoved: boolean, filePath: string|null }}
141
- */
142
- export function clearActiveStoryEnv({
143
- workCwd,
144
- env = process.env,
145
- fs = nodeFs,
146
- logger,
147
- } = {}) {
148
- for (const k of ACTIVE_STORY_ENV_KEYS) {
149
- if (k in env) delete env[k];
150
- }
151
-
152
- let fileRemoved = false;
153
- let filePath = null;
154
- if (typeof workCwd === 'string' && workCwd.length > 0) {
155
- filePath = nodePath.join(workCwd, ENV_LOCAL_BASENAME);
156
- try {
157
- if (fs.existsSync(filePath)) {
158
- fs.unlinkSync(filePath);
159
- fileRemoved = true;
160
- }
161
- } catch (err) {
162
- logger?.warn?.(
163
- `[active-story-env] Failed to remove ${filePath}: ${
164
- err instanceof Error ? err.message : String(err)
165
- }`,
166
- );
167
- }
168
- }
169
- return { envCleared: true, fileRemoved, filePath };
170
- }