mandrel 2.24.0 → 2.26.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 (189) hide show
  1. package/.agents/agents/acceptance-critic.md +19 -21
  2. package/.agents/agents/auditor.md +16 -19
  3. package/.agents/agents/story-worker.md +41 -54
  4. package/.agents/audit-checklists/architecture.md +5 -1
  5. package/.agents/audit-checklists/baselines.md +21 -0
  6. package/.agents/audit-checklists/quality.md +3 -0
  7. package/.agents/docs/agentrc-reference.json +2 -1
  8. package/.agents/docs/configuration.md +2 -1
  9. package/.agents/docs/quality-gates.md +80 -18
  10. package/.agents/docs/workflows.md +4 -2
  11. package/.agents/instructions.md +1 -1
  12. package/.agents/rules/ci-remediation.md +68 -3
  13. package/.agents/schemas/agentrc.schema.json +6 -1
  14. package/.agents/schemas/audit-rules.json +15 -0
  15. package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
  16. package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
  17. package/.agents/schemas/baselines/crap.schema.json +13 -1
  18. package/.agents/schemas/crap-report.schema.json +37 -0
  19. package/.agents/schemas/model-attribution.schema.json +4 -0
  20. package/.agents/scripts/acceptance-eval.js +124 -15
  21. package/.agents/scripts/audit-baselines.js +136 -0
  22. package/.agents/scripts/audit-labels-bootstrap.js +16 -95
  23. package/.agents/scripts/audit-to-stories.js +33 -3
  24. package/.agents/scripts/check-arch-cycles.js +12 -93
  25. package/.agents/scripts/check-baseline-drift.js +16 -3
  26. package/.agents/scripts/check-baselines.js +19 -3
  27. package/.agents/scripts/check-context-budget.js +95 -14
  28. package/.agents/scripts/check-cyclomatic.js +214 -0
  29. package/.agents/scripts/check-schema-references.js +392 -0
  30. package/.agents/scripts/check-test-temp-hygiene.js +121 -1
  31. package/.agents/scripts/check-workflow-timeouts.js +291 -0
  32. package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
  33. package/.agents/scripts/deliver-light.js +3 -4
  34. package/.agents/scripts/deliver-recover.js +13 -0
  35. package/.agents/scripts/diagnose-friction.js +85 -19
  36. package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
  37. package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
  38. package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
  39. package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
  40. package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
  41. package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
  42. package/.agents/scripts/lib/audit-baselines/read.js +87 -0
  43. package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
  44. package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
  45. package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
  46. package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
  47. package/.agents/scripts/lib/audit-suite/index.js +0 -5
  48. package/.agents/scripts/lib/audit-suite/selector.js +9 -62
  49. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
  50. package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
  51. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
  52. package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
  53. package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
  54. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
  55. package/.agents/scripts/lib/baseline-loader.js +0 -0
  56. package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
  57. package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
  58. package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
  59. package/.agents/scripts/lib/baselines/git-base.js +0 -0
  60. package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
  61. package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
  62. package/.agents/scripts/lib/baselines/reader.js +10 -0
  63. package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
  64. package/.agents/scripts/lib/baselines/scope.js +39 -90
  65. package/.agents/scripts/lib/baselines/writer.js +16 -7
  66. package/.agents/scripts/lib/changed-files.js +8 -1
  67. package/.agents/scripts/lib/cli-args.js +115 -1
  68. package/.agents/scripts/lib/close-validation/runner.js +70 -25
  69. package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
  70. package/.agents/scripts/lib/crap-engine.js +124 -27
  71. package/.agents/scripts/lib/crap-method-identity.js +153 -0
  72. package/.agents/scripts/lib/crap-utils.js +86 -13
  73. package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
  74. package/.agents/scripts/lib/env-loader.js +46 -16
  75. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
  76. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
  77. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
  78. package/.agents/scripts/lib/findings/route-finding.js +123 -17
  79. package/.agents/scripts/lib/findings/severity.js +80 -2
  80. package/.agents/scripts/lib/git/cached-fetch.js +0 -0
  81. package/.agents/scripts/lib/git-utils.js +136 -80
  82. package/.agents/scripts/lib/import-graph.js +156 -0
  83. package/.agents/scripts/lib/label-constants.js +17 -0
  84. package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
  85. package/.agents/scripts/lib/observability/source-classifier.js +175 -2
  86. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
  87. package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
  88. package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
  89. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
  90. package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
  91. package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
  92. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
  93. package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
  94. package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
  95. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
  96. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
  97. package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
  98. package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
  99. package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
  100. package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
  101. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
  102. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
  103. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
  104. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
  105. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
  106. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
  107. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
  108. package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
  109. package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
  110. package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
  111. package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
  112. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
  118. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
  119. package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
  120. package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
  121. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
  122. package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
  123. package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
  124. package/.agents/scripts/lib/reserved-test-ids.js +77 -0
  125. package/.agents/scripts/lib/story-adjacency.js +3 -3
  126. package/.agents/scripts/lib/story-plan.js +137 -42
  127. package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
  128. package/.agents/scripts/lib/test-runner-contract.js +134 -0
  129. package/.agents/scripts/lib/test-tiers.js +11 -2
  130. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  131. package/.agents/scripts/lib/util/concurrent-map.js +17 -0
  132. package/.agents/scripts/lib/util/parse-id-list.js +103 -0
  133. package/.agents/scripts/lib/util/poll-loop.js +18 -2
  134. package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
  135. package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
  136. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
  137. package/.agents/scripts/lib/workers/crap-worker.js +8 -13
  138. package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
  139. package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
  140. package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
  141. package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
  142. package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
  143. package/.agents/scripts/lib/worktree-manager.js +14 -0
  144. package/.agents/scripts/plan-run-epilogue.js +17 -5
  145. package/.agents/scripts/pr-watch-with-update.js +568 -213
  146. package/.agents/scripts/providers/github/tickets.js +33 -10
  147. package/.agents/scripts/provision-git-hooks.js +85 -0
  148. package/.agents/scripts/quality-preview.js +137 -28
  149. package/.agents/scripts/resolve-stories.js +4 -1
  150. package/.agents/scripts/run-coverage.js +86 -35
  151. package/.agents/scripts/run-lint.js +20 -0
  152. package/.agents/scripts/run-tests.js +36 -36
  153. package/.agents/scripts/single-story-close.js +34 -2
  154. package/.agents/scripts/single-story-confirm-merge.js +22 -6
  155. package/.agents/scripts/single-story-init.js +7 -0
  156. package/.agents/scripts/stories-wave-tick.js +308 -47
  157. package/.agents/scripts/story-plan.js +65 -9
  158. package/.agents/scripts/update-coverage-baseline.js +34 -4
  159. package/.agents/scripts/update-crap-baseline.js +42 -4
  160. package/.agents/scripts/update-duplication-baseline.js +209 -83
  161. package/.agents/scripts/update-maintainability-baseline.js +31 -4
  162. package/.agents/scripts/validate-docs-freshness.js +1 -0
  163. package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
  165. package/.agents/skills/skills.index.json +2 -2
  166. package/.agents/templates/single-story-body.md +16 -8
  167. package/.agents/workflows/audit-architecture.md +69 -13
  168. package/.agents/workflows/audit-baselines.md +289 -0
  169. package/.agents/workflows/audit-navigability.md +5 -4
  170. package/.agents/workflows/audit-quality.md +26 -0
  171. package/.agents/workflows/audit-to-stories.md +30 -4
  172. package/.agents/workflows/deliver.md +92 -97
  173. package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
  174. package/.agents/workflows/helpers/audit-lens-core.md +25 -4
  175. package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
  176. package/.agents/workflows/helpers/deliver-digest.md +41 -21
  177. package/.agents/workflows/helpers/deliver-reference.md +95 -4
  178. package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
  179. package/.agents/workflows/helpers/deliver-story.md +104 -119
  180. package/.agents/workflows/helpers/plan-reference.md +49 -5
  181. package/.agents/workflows/memory-consolidate.md +116 -0
  182. package/.agents/workflows/plan.md +62 -75
  183. package/README.md +13 -6
  184. package/docs/CHANGELOG.md +93 -0
  185. package/lib/cli/update.js +14 -11
  186. package/lib/cli/version-check.js +9 -1
  187. package/package.json +9 -4
  188. package/.agents/schemas/friction-event.schema.json +0 -56
  189. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
@@ -29,52 +29,34 @@
29
29
  */
30
30
 
31
31
  import { spawnSync } from 'node:child_process';
32
- import os from 'node:os';
33
32
  import path from 'node:path';
34
33
  import { fileURLToPath } from 'node:url';
34
+ import { assertNoReservedIdStreams } from './check-test-temp-hygiene.js';
35
35
  import { cleanupRepoTestTempArtifacts } from './cleanup-repo-test-temp.js';
36
36
  import { runAsCli } from './lib/cli-utils.js';
37
37
  import { buildWebhookSafeTestEnv } from './lib/test-env.js';
38
+ import {
39
+ resolveTestConcurrency,
40
+ runTierPreflight,
41
+ TEST_CONCURRENCY_MAX,
42
+ TEST_CONCURRENCY_MIN,
43
+ TEST_RUNNER_FLAGS,
44
+ } from './lib/test-runner-contract.js';
38
45
  import { listTestFilesForTier, parseTierArgv } from './lib/test-tiers.js';
39
46
 
40
47
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
41
48
  const ROOT = path.resolve(__dirname, '..', '..');
42
49
 
43
- /** Minimum and maximum bounds for `--test-concurrency`. */
44
- export const TEST_CONCURRENCY_MIN = 1;
45
- export const TEST_CONCURRENCY_MAX = 16;
46
-
47
- /**
48
- * Resolve the `--test-concurrency` value for the current host.
49
- *
50
- * Uses `os.availableParallelism()` (Node ≥18.14 / ≥20.0) clamped to the
51
- * range `[TEST_CONCURRENCY_MIN, TEST_CONCURRENCY_MAX]`. The `parallelism`
52
- * parameter is injected in tests so the clamping logic is verifiable
53
- * without touching the OS.
54
- *
55
- * @param {number} [parallelism] - defaults to `os.availableParallelism()`
56
- * @returns {number}
57
- */
58
- export function resolveTestConcurrency(
59
- parallelism = os.availableParallelism(),
60
- ) {
61
- return Math.min(
62
- TEST_CONCURRENCY_MAX,
63
- Math.max(TEST_CONCURRENCY_MIN, parallelism),
64
- );
65
- }
66
-
67
- /**
68
- * Fixed `node --test` flags applied to every spawn (every chunk).
69
- * `--test-concurrency` is derived at startup from the host's available
70
- * parallelism so the value is appropriate for the machine running the suite
71
- * rather than being pinned to the historical constant of 8.
72
- */
73
- export const TEST_RUNNER_FLAGS = Object.freeze([
74
- '--experimental-test-module-mocks',
75
- '--test',
76
- `--test-concurrency=${resolveTestConcurrency()}`,
77
- ]);
50
+ // The runner contract (flag set, concurrency clamp, tier preflight) is
51
+ // declared once in `lib/test-runner-contract.js` so `run-coverage.js` shares
52
+ // it by construction. Re-exported here because this module is the runner's
53
+ // public face for its own unit tests.
54
+ export {
55
+ resolveTestConcurrency,
56
+ TEST_CONCURRENCY_MAX,
57
+ TEST_CONCURRENCY_MIN,
58
+ TEST_RUNNER_FLAGS,
59
+ };
78
60
 
79
61
  /**
80
62
  * Per-spawn character budget for the joined *targets* portion of the argv.
@@ -165,8 +147,18 @@ export function runTestSuite({
165
147
  cleanup = cleanupRepoTestTempArtifacts,
166
148
  listTargets = listTestFilesForTier,
167
149
  maxTargetChars = resolveMaxTargetChars(),
150
+ fixtureStreamGuard = assertNoReservedIdStreams,
151
+ preflight = runTierPreflight,
168
152
  } = {}) {
169
153
  const { tier, rest } = parseTierArgv(argv);
154
+
155
+ // Preflight first, and abort on refusal — the tier's checks exist to stop
156
+ // the suite starting from a known-bad state, so running them after the
157
+ // spawn would be pointless. Nothing has been created yet, so there is
158
+ // nothing to clean up on this path.
159
+ const preflightStatus = preflight({ tier, repoRoot: cwd });
160
+ if (preflightStatus !== 0) return preflightStatus;
161
+
170
162
  const targets = listTargets(tier, cwd);
171
163
  const chunks = chunkTestTargets(targets, maxTargetChars);
172
164
 
@@ -192,6 +184,14 @@ export function runTestSuite({
192
184
 
193
185
  cleanup({ repoRoot: cwd });
194
186
 
187
+ // A test that spawns a real CLI at the repository root inherits the real
188
+ // state directory, so its fixture telemetry can land in the operator's live
189
+ // signals tree — where the retro graduator counts it as recurrence evidence
190
+ // and files a ticket citing a fixture Story id. Fail the run that caused it
191
+ // rather than discovering it from that ticket.
192
+ const polluted = fixtureStreamGuard({ cwd });
193
+ if (polluted !== 0 && status === 0) status = polluted;
194
+
195
195
  if (spawnError) {
196
196
  throw spawnError;
197
197
  }
@@ -43,6 +43,16 @@
43
43
  * [--skip-validation] [--skip-sync]
44
44
  * [--no-auto-merge]
45
45
  * [--wait-merge | --no-wait-merge]
46
+ * [--merge-watch-mode <sync|async>]
47
+ *
48
+ * `--merge-watch-mode` (Story #4949) overrides `delivery.mergeWatch.mode` for
49
+ * one invocation, on the same explicit-wins-over-config precedence
50
+ * `--max-wait-seconds` uses, and the two compose. It exists because run
51
+ * topology is invisible from inside close: a solo delivery is cheapest waiting
52
+ * in the foreground, while the Nth close of a wave pays that wait as
53
+ * serialized dead time. The config default therefore stays `sync` and the
54
+ * orchestrator — the only party that knows N — passes `async` per close. An
55
+ * unrecognized value fails during option parsing, before any phase runs.
46
56
  *
47
57
  * Close-and-land is the DEFAULT for every run (Story #4428 introduced it as
48
58
  * `--wait-merge`; `delivery.routing.closeAndLand` — default `true` — made it
@@ -85,11 +95,12 @@
85
95
  * @see .agents/schemas/story-deliver-terminal.schema.json
86
96
  */
87
97
 
88
- import { parseSprintArgs } from './lib/cli-args.js';
98
+ import { parseSprintArgsTolerant } from './lib/cli-args.js';
89
99
  import { runAsCli } from './lib/cli-utils.js';
90
100
  import { formatCliError } from './lib/error-redactor.js';
91
101
  import { Logger } from './lib/Logger.js';
92
102
  import { emitTerminalFriction } from './lib/observability/runtime-friction.js';
103
+ import { resolveRunScopedConfig } from './lib/orchestration/run-scoped-config.js';
93
104
  import {
94
105
  failedTerminalFor,
95
106
  gatesForFailedPhase,
@@ -123,12 +134,17 @@ export const enableAutoMerge = enableAutoMergeWith;
123
134
  // `gatesForFailedPhase` now lives beside the envelope it feeds
124
135
  // (`single-story-close/failed-terminal.js`); it is re-exported here so the
125
136
  // CLI's public surface is unchanged by that move.
137
+ // `resolveRunScopedConfig` (Story #4891) is the run-scoped config pin the
138
+ // pipeline reads before its first phase; it is part of this CLI's surface for
139
+ // the same reason the sync helpers are — the runner reaches it only through a
140
+ // dynamic import, so this file is where it is statically visible.
126
141
  export {
127
142
  buildStoryReviewCrossRefBody,
128
143
  buildSyncFailureCommentBody,
129
144
  gatesForFailedPhase,
130
145
  handleSyncFailure,
131
146
  parsePrNumber,
147
+ resolveRunScopedConfig,
132
148
  runStoryScopeReview,
133
149
  };
134
150
 
@@ -145,13 +161,25 @@ export async function runSingleStoryClose(opts) {
145
161
  * status rather than from a thrown/not-thrown distinction, so `pending`
146
162
  * (resumable) is distinguishable from `blocked` (come look) without parsing
147
163
  * stdout.
164
+ *
165
+ * The catch parses argv through the **non-throwing** wrapper (Story #4959).
166
+ * It used to call `parseSprintArgs()` — re-invoking the very parser that had
167
+ * just thrown, since `parseMergeWatchMode` made argv parsing fallible. The
168
+ * second throw escaped the handler, so an unparseable argv produced a bare
169
+ * stack trace with no envelope and no friction signal, on the surface whose
170
+ * whole contract is that every invocation emits exactly one envelope. An
171
+ * error handler may not depend on an operation already known to fail.
172
+ *
173
+ * A parse rejection carries no `closePhase`, so the envelope reports `init` —
174
+ * accurate: the runner rejected the flag before any phase ran, and nothing
175
+ * was mutated.
148
176
  */
149
177
  async function main() {
150
178
  try {
151
179
  const outcome = await runSingleStoryClose();
152
180
  return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
153
181
  } catch (err) {
154
- const terminal = failedTerminalFor(err, parseSprintArgs());
182
+ const terminal = failedTerminalFor(err, parseSprintArgsTolerant().args);
155
183
  if (!terminal) throw err;
156
184
  // Mirror runAsCli's default error line (which this catch pre-empts) so the
157
185
  // human-facing failure text is unchanged, then emit the envelope.
@@ -185,6 +213,10 @@ runAsCli(import.meta.url, main, {
185
213
  ['--wait-merge', 'Force the in-close merge wait.'],
186
214
  ['--no-wait-merge', 'Return as soon as the PR is open; do not wait.'],
187
215
  ['--max-wait-seconds <n>', 'Per-invocation merge-wait bound.'],
216
+ [
217
+ '--merge-watch-mode <sync|async>',
218
+ 'Override delivery.mergeWatch.mode for this invocation only. `async` caps the merge wait to a short probe window and returns the resumable `pending` terminal instead of holding the foreground slot — pass it on every close of a multi-Story run. An invalid value exits non-zero before any phase runs.',
219
+ ],
188
220
  ['--no-evidence', 'Do not reuse or write gate evidence stamps.'],
189
221
  ['--dry-run', 'Report the plan; mutate nothing.'],
190
222
  ],
@@ -37,7 +37,7 @@
37
37
  */
38
38
 
39
39
  import { parseArgs } from 'node:util';
40
- import { parseSprintArgs } from './lib/cli-args.js';
40
+ import { parseSprintArgsTolerant } from './lib/cli-args.js';
41
41
  import { runAsCli } from './lib/cli-utils.js';
42
42
  import { resolveConfig } from './lib/config-resolver.js';
43
43
  import { formatCliError } from './lib/error-redactor.js';
@@ -66,6 +66,15 @@ const progress = Logger.createProgress('single-story-confirm-merge', {
66
66
  stderr: true,
67
67
  });
68
68
 
69
+ /**
70
+ * This CLI's own surface name — the `runAsCli` source, and the `emitter.tool`
71
+ * every friction record it emits carries. Single-homed so the two cannot
72
+ * drift: `emitTerminalFriction` defaults to the CLOSE CLI's name, so a record
73
+ * emitted from here without it is attributed to a CLI that never ran, and the
74
+ * retro roll-up sends the follow-up to the wrong surface.
75
+ */
76
+ const CLI_SOURCE = 'single-story-confirm-merge';
77
+
69
78
  /**
70
79
  * Default `gh` facade for this CLI, bound to the merge wait's spawn-level
71
80
  * timeout (Story #4710). This CLI is the resume surface async mode hands the
@@ -204,7 +213,7 @@ async function logConfirmResult(result, terminal, config) {
204
213
  },
205
214
  });
206
215
  emitTerminalEnvelope(terminal, { config });
207
- await emitTerminalFriction({ envelope: terminal, config });
216
+ await emitTerminalFriction({ envelope: terminal, tool: CLI_SOURCE, config });
208
217
  return { success: terminal.status !== 'failed', result, terminal };
209
218
  }
210
219
 
@@ -512,14 +521,21 @@ async function main() {
512
521
  const outcome = await runConfirmMerge();
513
522
  return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
514
523
  } catch (err) {
515
- const storyId = Number(parseSprintArgs().storyId);
524
+ // Non-throwing by construction (Story #4959): this used to call
525
+ // `parseSprintArgs()`, so a rejected `--merge-watch-mode` threw a second
526
+ // time here and escaped the handler — no envelope, no friction. Close
527
+ // carries the identical shape; both landing surfaces behave the same.
528
+ const { args, error: argvError } = parseSprintArgsTolerant();
529
+ const storyId = Number(args.storyId);
516
530
  // No story id → a usage error; there is nothing to report an envelope
517
531
  // about, so let runAsCli surface it as a plain fatal.
518
532
  if (!Number.isInteger(storyId) || storyId <= 0) throw err;
519
533
  const terminal = buildTerminalEnvelope({
520
534
  storyId,
521
535
  status: 'failed',
522
- phase: 'confirm-merge',
536
+ // An argv rejection happens before any phase runs, so it reports at
537
+ // `init` rather than claiming a confirm-merge attempt that never began.
538
+ phase: argvError ? 'init' : 'confirm-merge',
523
539
  failure: { reason: String(err?.message ?? err) },
524
540
  nextCommand: NEXT_COMMANDS.recover(storyId),
525
541
  elapsedSeconds: 0,
@@ -528,13 +544,13 @@ async function main() {
528
544
  `[single-story-confirm-merge] Fatal error: ${formatCliError(err)}`,
529
545
  );
530
546
  emitTerminalEnvelope(terminal);
531
- await emitTerminalFriction({ envelope: terminal });
547
+ await emitTerminalFriction({ envelope: terminal, tool: CLI_SOURCE });
532
548
  return exitCodeForTerminal(terminal);
533
549
  }
534
550
  }
535
551
 
536
552
  runAsCli(import.meta.url, main, {
537
- source: 'single-story-confirm-merge',
553
+ source: CLI_SOURCE,
538
554
  propagateExitCode: true,
539
555
  usage: {
540
556
  invocation:
@@ -61,6 +61,7 @@ import {
61
61
  planFastForward,
62
62
  } from './lib/orchestration/git-cleanup/phases/fast-forward.js';
63
63
  import { verifyRemote } from './lib/orchestration/remote-verifier.js';
64
+ import { pinRunScopedConfig } from './lib/orchestration/run-scoped-config.js';
64
65
  import {
65
66
  acquireStoryLease,
66
67
  releaseStoryLease,
@@ -735,6 +736,11 @@ export async function runSingleStoryInit({
735
736
  standalone: true,
736
737
  storyBranch,
737
738
  baseBranch,
739
+ // The write half of the run-scoped config pin. `baseBranch` above is the
740
+ // legacy field close still reads as a fallback; this block is the
741
+ // registry-driven form a second run-scoped key joins without a second
742
+ // mechanism (`lib/orchestration/run-scoped-config.js`).
743
+ runScopedConfig: pinRunScopedConfig(config),
738
744
  storyTitle: story.title,
739
745
  worktreeEnabled: runtime.worktreeEnabled,
740
746
  workCwd,
@@ -805,6 +811,7 @@ export function renderSingleStoryInitComment(result) {
805
811
  standalone: true,
806
812
  storyBranch: result.storyBranch,
807
813
  baseBranch: result.baseBranch,
814
+ runScopedConfig: result.runScopedConfig,
808
815
  worktreeEnabled: result.worktreeEnabled,
809
816
  workCwd: result.workCwd,
810
817
  worktreeCreated: result.worktreeCreated,