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