mandrel 2.59.0 → 2.60.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 (97) hide show
  1. package/.agents/README.md +11 -9
  2. package/.agents/agents/acceptance-critic.md +24 -43
  3. package/.agents/agents/story-worker.md +18 -19
  4. package/.agents/docs/SDLC.md +6 -6
  5. package/.agents/docs/agentrc-reference.json +1 -2
  6. package/.agents/docs/configuration.md +29 -46
  7. package/.agents/docs/quality-gates.md +8 -4
  8. package/.agents/docs/workflows.md +1 -1
  9. package/.agents/instructions.md +4 -5
  10. package/.agents/rules/ci-remediation.md +41 -8
  11. package/.agents/rules/known-tooling-behavior.md +65 -15
  12. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  13. package/.agents/schemas/agentrc.schema.json +6 -11
  14. package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
  15. package/.agents/scripts/README.md +11 -1
  16. package/.agents/scripts/acceptance-eval.js +25 -27
  17. package/.agents/scripts/ceremony-derive.js +15 -10
  18. package/.agents/scripts/check-context-budget.js +148 -228
  19. package/.agents/scripts/check-schema-references.js +5 -3
  20. package/.agents/scripts/check-workflow-citations.js +33 -147
  21. package/.agents/scripts/coverage-capture.js +7 -4
  22. package/.agents/scripts/deliver-light.js +41 -100
  23. package/.agents/scripts/deliver-run.js +631 -0
  24. package/.agents/scripts/file-ci-gap.js +59 -11
  25. package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
  26. package/.agents/scripts/lib/changed-files.js +30 -0
  27. package/.agents/scripts/lib/config/delivery-routing.js +5 -4
  28. package/.agents/scripts/lib/config/explain.js +1 -3
  29. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
  30. package/.agents/scripts/lib/config-resolver.js +1 -0
  31. package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
  32. package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
  33. package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
  34. package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
  35. package/.agents/scripts/lib/doc-tiers.js +4 -2
  36. package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
  37. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
  38. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  39. package/.agents/scripts/lib/gh-exec.js +160 -0
  40. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  41. package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
  42. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
  43. package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
  44. package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
  45. package/.agents/scripts/lib/orchestration/plan-context.js +13 -25
  46. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
  47. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +76 -95
  48. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +35 -18
  49. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
  50. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
  51. package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
  52. package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
  53. package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
  54. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
  55. package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
  56. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
  57. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
  58. package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
  59. package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
  60. package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
  61. package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
  62. package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
  63. package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
  64. package/.agents/scripts/lib/story-body/story-body.js +83 -29
  65. package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -15
  66. package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
  67. package/.agents/scripts/merge-baseline.js +4 -5
  68. package/.agents/scripts/plan-context.js +117 -28
  69. package/.agents/scripts/plan-persist.js +79 -28
  70. package/.agents/scripts/plan-run-epilogue.js +11 -8
  71. package/.agents/scripts/pr-watch-with-update.js +9 -2
  72. package/.agents/scripts/run-verify.js +13 -6
  73. package/.agents/scripts/single-story-init.js +7 -57
  74. package/.agents/scripts/stories-wave-tick.js +160 -26
  75. package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
  76. package/.agents/skills/skills.index.json +2 -2
  77. package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
  78. package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
  79. package/.agents/workflows/helpers/code-review.md +4 -2
  80. package/.agents/workflows/helpers/deliver-digest.md +31 -24
  81. package/.agents/workflows/helpers/deliver-light.md +92 -101
  82. package/.agents/workflows/helpers/deliver-reference.md +116 -100
  83. package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
  84. package/.agents/workflows/helpers/deliver-story.md +17 -18
  85. package/.agents/workflows/helpers/plan-reference.md +65 -54
  86. package/.agents/workflows/mandrel-deliver.md +47 -31
  87. package/.agents/workflows/mandrel-plan.md +22 -21
  88. package/.agents/workflows/mandrel-update.md +36 -21
  89. package/docs/CHANGELOG.md +35 -0
  90. package/lib/cli/update.js +376 -17
  91. package/lib/migrations/index.js +2 -0
  92. package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
  93. package/package.json +2 -1
  94. package/.agents/schemas/model-attribution.schema.json +0 -53
  95. package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
  96. package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
  97. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
@@ -107,6 +107,18 @@
107
107
  * deterministic config source (`delivery.deliverRunner.concurrencyCap`) and
108
108
  * one scheduling kernel with every `/mandrel-deliver` multi-Story invocation.
109
109
  *
110
+ * **The cap is clamped to 1 when worktree isolation resolves off** (Story
111
+ * #5357). Concurrent dispatch is safe only because each Story gets its own
112
+ * checkout; with isolation off every Story resolves to the same `workCwd`, so
113
+ * two workers contend for `HEAD` and one takes the branch from under the
114
+ * other — the #5339/#5340 web run, where both wave-1 workers were initialised
115
+ * into the same tree. The clamp reads `resolveWorktreeEnabled`'s result, so all
116
+ * three routes to off (`CLAUDE_CODE_REMOTE=true`, `AP_WORKTREE_ENABLED=false`,
117
+ * `delivery.worktreeIsolation.enabled: false`) are covered by construction. It
118
+ * is a safety floor rather than a preference, so it is the one case where an
119
+ * explicit `--concurrency` does NOT win — and `capPrecedence` names
120
+ * `worktree-clamp` as the winning source and carries the requested value.
121
+ *
110
122
  * Exit codes: 0 ok · 1 input error · 2 dependency cycle (`cycleError`) ·
111
123
  * 3 wedged (`wedged`) — ready is empty, nothing is in flight, and undone
112
124
  * Stories are waiting on blockers that are not done · 4 blocked (`blocked`) —
@@ -124,7 +136,13 @@ import { readFileSync } from 'node:fs';
124
136
  import { parseArgs } from 'node:util';
125
137
 
126
138
  import { runAsCli } from './lib/cli-utils.js';
127
- import { getPaths, getRunners, resolveConfig } from './lib/config-resolver.js';
139
+ import {
140
+ getPaths,
141
+ getRunners,
142
+ getWorktreeIsolation,
143
+ resolveConfig,
144
+ resolveWorktreeEnabled,
145
+ } from './lib/config-resolver.js';
128
146
  import { detectCycle } from './lib/Graph.js';
129
147
  import { Logger } from './lib/Logger.js';
130
148
  import { AGENT_LABELS } from './lib/label-constants.js';
@@ -206,6 +224,13 @@ Options:
206
224
  WINS over the configured value, and the envelope's
207
225
  capPrecedence records that it did — including when the
208
226
  request exceeds the configured cap.
227
+ ONE EXCEPTION: when worktree isolation resolves off
228
+ (CLAUDE_CODE_REMOTE=true, AP_WORKTREE_ENABLED=false, or
229
+ delivery.worktreeIsolation.enabled: false), the cap is
230
+ clamped to 1 and the clamp outranks this flag — every
231
+ Story would otherwise run in the same checkout and two
232
+ workers would contend for HEAD. capPrecedence reports
233
+ source "worktree-clamp" with the requested value.
209
234
  --done <csv> Comma-separated Story IDs already completed this run.
210
235
  Their dependents become eligible; they are never
211
236
  re-dispatched. Defaults to empty.
@@ -638,6 +663,15 @@ export function parseInFlight(raw) {
638
663
  return { value: num, error: null };
639
664
  }
640
665
 
666
+ /**
667
+ * The per-beat cap a run is clamped to when worktree isolation resolves off
668
+ * (Story #5357). `delivery.deliverRunner.concurrencyCap` is a throughput
669
+ * preference; this is a safety floor, so it outranks even an explicit
670
+ * `--concurrency`. Why the two are coupled, and why the clamp is never silent:
671
+ * ADR `20260917-5357` in `docs/decisions.md`.
672
+ */
673
+ export const WORKTREE_DISABLED_CONCURRENCY_CAP = 1;
674
+
641
675
  /**
642
676
  * Validate a raw `--concurrency` value into a positive integer.
643
677
  *
@@ -675,7 +709,9 @@ export function parseConcurrencyOverride(raw) {
675
709
  * @param {object} [opts.config] Pre-resolved config (injected by tests so
676
710
  * they never depend on a real `.agentrc`).
677
711
  * @param {number} [opts.override] Validated positive integer from
678
- * `--concurrency`; wins over config.
712
+ * `--concurrency`; wins over config, unless
713
+ * the worktree-isolation floor clamps it.
714
+ * @param {NodeJS.ProcessEnv} [opts.env] Environment (test injection).
679
715
  * @returns {number} The resolved positive-integer concurrency cap.
680
716
  */
681
717
  export function resolveConcurrencyCap(opts = {}) {
@@ -702,44 +738,115 @@ export function resolveConcurrencyCap(opts = {}) {
702
738
  * safety limit, and refusing a deliberate operator escalation would trade a
703
739
  * silent override for a silent stall.
704
740
  *
741
+ * **The one exception (Story #5357).** When worktree isolation resolves off,
742
+ * the cap is clamped to {@link WORKTREE_DISABLED_CONCURRENCY_CAP} and the
743
+ * clamp outranks the flag. That is not a reversal of the paragraph above: the
744
+ * configured cap is a preference and a flag may outrank a preference, but a
745
+ * shared checkout is not a preference at all. Dispatching two workers into one
746
+ * working tree corrupts the run whatever number either source asked for, so
747
+ * the floor wins — loudly, per {@link resolveWorktreeClamp}.
748
+ *
705
749
  * @param {object} [opts]
706
750
  * @param {string} [opts.cwd] Repo root for config resolution.
707
751
  * @param {object} [opts.config] Pre-resolved config (test injection).
708
752
  * @param {number} [opts.override] Validated positive integer from
709
753
  * `--concurrency`.
754
+ * @param {NodeJS.ProcessEnv} [opts.env] Environment the worktree-isolation
755
+ * rules read (test injection).
710
756
  * @returns {{
711
757
  * cap: number,
712
- * source: 'flag'|'config',
758
+ * source: 'flag'|'config'|'worktree-clamp',
713
759
  * configuredCap: number,
714
760
  * requestedCap: number|null,
715
761
  * exceedsConfigured: boolean,
716
762
  * note: string,
717
763
  * }}
718
764
  */
719
- export function resolveCapPrecedence({ cwd, config, override } = {}) {
765
+ export function resolveCapPrecedence({ cwd, config, override, env } = {}) {
720
766
  const resolved = config ?? resolveConfig({ cwd });
721
767
  const { deliverRunner } = getRunners(resolved);
722
768
  const configuredCap = deliverRunner.concurrencyCap;
723
- if (override == null) {
724
- return {
725
- cap: configuredCap,
726
- source: 'config',
727
- configuredCap,
728
- requestedCap: null,
729
- exceedsConfigured: false,
730
- note: `cap ${configuredCap} from delivery.deliverRunner.concurrencyCap (no --concurrency given)`,
731
- };
732
- }
733
- const exceedsConfigured = override > configuredCap;
769
+ const requested =
770
+ override == null
771
+ ? {
772
+ cap: configuredCap,
773
+ source: 'config',
774
+ configuredCap,
775
+ requestedCap: null,
776
+ exceedsConfigured: false,
777
+ note: `cap ${configuredCap} from delivery.deliverRunner.concurrencyCap (no --concurrency given)`,
778
+ }
779
+ : {
780
+ cap: override,
781
+ source: 'flag',
782
+ configuredCap,
783
+ requestedCap: override,
784
+ exceedsConfigured: override > configuredCap,
785
+ note:
786
+ override > configuredCap
787
+ ? `cap ${override} from --concurrency, which OVERRIDES and EXCEEDS the configured delivery.deliverRunner.concurrencyCap ${configuredCap} — this run is deliberately above the project default`
788
+ : `cap ${override} from --concurrency, which overrides the configured delivery.deliverRunner.concurrencyCap ${configuredCap}`,
789
+ };
790
+
791
+ return resolveWorktreeClamp({ requested, config: resolved, env });
792
+ }
793
+
794
+ /**
795
+ * Apply the worktree-isolation safety floor to an already-resolved precedence
796
+ * record (Story #5357).
797
+ *
798
+ * **Keyed off the resolved boolean, never one env var.** `CLAUDE_CODE_REMOTE`
799
+ * is only one of three routes to isolation-off — `AP_WORKTREE_ENABLED=false`
800
+ * and `delivery.worktreeIsolation.enabled: false` are the others and are
801
+ * exactly as unsafe — so this reads {@link resolveWorktreeEnabled}'s result and
802
+ * all three are covered by construction rather than by enumeration.
803
+ *
804
+ * **The config is normalised through `getWorktreeIsolation` first**, and that
805
+ * is load-bearing. `resolveWorktreeEnabled` reads
806
+ * `Boolean(config.delivery.worktreeIsolation.enabled)` raw, so a config that
807
+ * simply omits the block — every test-injected partial, and any consumer who
808
+ * never wrote one — reads `Boolean(undefined) === false` and would be clamped
809
+ * as though an operator had disabled isolation. `getWorktreeIsolation` applies
810
+ * the framework default (`enabled: true`) the same way `resolveConfig` does for
811
+ * a real `.agentrc`, so the clamp fires on a deliberate off, never on an
812
+ * absent key.
813
+ *
814
+ * The record is emitted whenever isolation is off, **including when the
815
+ * requested cap was already 1**: the point is that the run's sequentiality is
816
+ * always attributable to the reason that forced it, not left to coincide with
817
+ * a preference that happened to agree.
818
+ *
819
+ * @param {object} args
820
+ * @param {{cap: number, source: string, configuredCap: number, requestedCap: number|null, exceedsConfigured: boolean, note: string}} args.requested
821
+ * The precedence record the flag-vs-config rules produced.
822
+ * @param {object|null} [args.config] Resolved config.
823
+ * @param {NodeJS.ProcessEnv} [args.env] Environment (test injection).
824
+ * @returns {typeof args.requested} The record unchanged when isolation is on,
825
+ * or the clamped record naming `worktree-clamp` as the winning source.
826
+ */
827
+ export function resolveWorktreeClamp({ requested, config, env } = {}) {
828
+ const worktreeEnabled = resolveWorktreeEnabled(
829
+ {
830
+ config: { delivery: { worktreeIsolation: getWorktreeIsolation(config) } },
831
+ },
832
+ env ?? process.env,
833
+ );
834
+ if (worktreeEnabled) return requested;
835
+
836
+ const from =
837
+ requested.source === 'flag'
838
+ ? '--concurrency'
839
+ : 'delivery.deliverRunner.concurrencyCap';
734
840
  return {
735
- cap: override,
736
- source: 'flag',
737
- configuredCap,
738
- requestedCap: override,
739
- exceedsConfigured,
740
- note: exceedsConfigured
741
- ? `cap ${override} from --concurrency, which OVERRIDES and EXCEEDS the configured delivery.deliverRunner.concurrencyCap ${configuredCap} — this run is deliberately above the project default`
742
- : `cap ${override} from --concurrency, which overrides the configured delivery.deliverRunner.concurrencyCap ${configuredCap}`,
841
+ cap: WORKTREE_DISABLED_CONCURRENCY_CAP,
842
+ source: 'worktree-clamp',
843
+ configuredCap: requested.configuredCap,
844
+ requestedCap: requested.cap,
845
+ // The run is at 1, so it is not above the project default whatever was
846
+ // asked for — the escalation never took effect and must not be reported
847
+ // as though it had.
848
+ exceedsConfigured: false,
849
+ note: `cap clamped to ${WORKTREE_DISABLED_CONCURRENCY_CAP} because worktree isolation resolved OFF — requested ${requested.cap} from ${from}. Concurrent dispatch shares one checkout without per-Story worktrees, so two workers would contend for HEAD; this floor outranks an explicit --concurrency.`,
743
850
  };
744
851
  }
745
852
 
@@ -1000,6 +1107,8 @@ export function detectWedge({ nodes, doneIds, ready, inFlight }) {
1000
1107
  * @param {string|number} [args.inFlight] Raw --in-flight count.
1001
1108
  * @param {string} [args.cwd] Repo root for config resolution.
1002
1109
  * @param {object} [args.config] Pre-resolved config (test injection).
1110
+ * @param {NodeJS.ProcessEnv} [args.env] Environment the worktree-isolation
1111
+ * clamp reads (test injection).
1003
1112
  * @returns {{
1004
1113
  * envelope: {kind: string, ready: number[], totalStories: number, concurrencyCap: number, inFlight: number, cycleError: string|null},
1005
1114
  * exitCode: number
@@ -1013,6 +1122,7 @@ export function runStoriesWaveTick({
1013
1122
  inFlight,
1014
1123
  cwd,
1015
1124
  config,
1125
+ env,
1016
1126
  } = {}) {
1017
1127
  // Validate the --concurrency override before resolving config so an invalid
1018
1128
  // value fails fast with exit code 1 regardless of DAG validity.
@@ -1033,7 +1143,7 @@ export function runStoriesWaveTick({
1033
1143
  return inputErrorResult(doneError, null, inFlightValue);
1034
1144
  }
1035
1145
 
1036
- const capPrecedence = resolveCapPrecedence({ cwd, config, override });
1146
+ const capPrecedence = resolveCapPrecedence({ cwd, config, override, env });
1037
1147
  const concurrencyCap = capPrecedence.cap;
1038
1148
 
1039
1149
  let rawJson;
@@ -1099,6 +1209,10 @@ export function runStoriesWaveTick({
1099
1209
  * the run-end signal for `plan-run-epilogue.js`.
1100
1210
  * - `blocked` — ids carrying `agent::blocked` (Story #4601). Non-empty means
1101
1211
  * the loop must END, not poll: see `BLOCKED_EXIT_CODE`.
1212
+ * - `stalledDispatch` — `--dispatched` ids live state still reports as
1213
+ * `agent::ready`. They stay withheld; the field exists so a caller keeping
1214
+ * an append-only dispatched list can see an id that is pinned rather than
1215
+ * merely initializing (Story #5363).
1102
1216
  *
1103
1217
  * @param {object} args
1104
1218
  * @param {string} args.stories Raw `--stories` CSV of Story ids.
@@ -1107,9 +1221,14 @@ export function runStoriesWaveTick({
1107
1221
  * has spawned but may not yet have observed labelled.
1108
1222
  * @param {string} [args.cwd] Repo root for config resolution.
1109
1223
  * @param {object} [args.config] Pre-resolved config (test injection).
1224
+ * @param {NodeJS.ProcessEnv} [args.env] Environment the worktree-isolation
1225
+ * clamp reads (test injection).
1110
1226
  * @param {Function} [args.probe] Probe seam (test injection).
1111
1227
  * @param {Function} [args.context] Provider-context seam (test injection).
1112
- * @returns {Promise<{ envelope: object, exitCode: number }>}
1228
+ * @returns {Promise<{ envelope: object, exitCode: number, records: object[] }>}
1229
+ * `records` are the probed nodes (id, dependsOn, files, body, labels) — the
1230
+ * bodies this beat already fetched, handed to the caller beside the envelope
1231
+ * rather than serialized into it.
1113
1232
  */
1114
1233
  export async function runProbedStoriesWaveTick({
1115
1234
  stories,
@@ -1117,6 +1236,7 @@ export async function runProbedStoriesWaveTick({
1117
1236
  dispatched,
1118
1237
  cwd,
1119
1238
  config,
1239
+ env,
1120
1240
  probe = probeLiveState,
1121
1241
  context = createProbeContext,
1122
1242
  } = {}) {
@@ -1141,7 +1261,7 @@ export async function runProbedStoriesWaveTick({
1141
1261
  return inputErrorResult(dispatchedError);
1142
1262
  }
1143
1263
 
1144
- const capPrecedence = resolveCapPrecedence({ cwd, config, override });
1264
+ const capPrecedence = resolveCapPrecedence({ cwd, config, override, env });
1145
1265
  const concurrencyCap = capPrecedence.cap;
1146
1266
 
1147
1267
  let probed;
@@ -1171,6 +1291,7 @@ export async function runProbedStoriesWaveTick({
1171
1291
  doneIds,
1172
1292
  inFlight,
1173
1293
  blockedIds = [],
1294
+ stalledDispatch = [],
1174
1295
  foreignHeld = [],
1175
1296
  inFlightRecords = [],
1176
1297
  } = probed;
@@ -1198,12 +1319,25 @@ export async function runProbedStoriesWaveTick({
1198
1319
  epilogueDue,
1199
1320
  blocked: blockedIds,
1200
1321
  blockedReason: blockedReasonFor(blockedIds),
1322
+ // Ids the caller listed in `--dispatched` that live state still reports
1323
+ // as `agent::ready`. Withheld as in flight (that is the flag's whole
1324
+ // job) and named here, because the same reading covers a healthy init
1325
+ // window and a spawn that died before init — and only the caller, who
1326
+ // owns the dispatched list, can tell those apart (Story #5363).
1327
+ stalledDispatch,
1201
1328
  // Stories another operator's lease holds — withheld from dispatch this
1202
1329
  // beat (folded into in-flight) and surfaced so the run can report
1203
1330
  // "#<id> held by @<holder>" instead of dispatching into an init refusal.
1204
1331
  foreignHeld,
1205
1332
  foreignHeldReason: foreignHeldReasonFor(foreignHeld),
1206
1333
  },
1334
+ // The probed nodes, beside the envelope rather than inside it. The
1335
+ // envelope is a list of ids by design — a Story body is several KB and has
1336
+ // no business on stdout — but the probe has already fetched every body,
1337
+ // and the one caller that needs them (`deliver-run.js`, building each
1338
+ // ready Story's dispatch prompt from its declared `changes[]`) would
1339
+ // otherwise re-fetch the whole set to read what this beat already holds.
1340
+ records: nodes,
1207
1341
  // A blocked Story outranks the scheduler's own verdict — including a
1208
1342
  // wedge, whose named blockers are moot while a human owes a decision.
1209
1343
  // A cycle (2) does not yield: a self-referential DAG is a planning error
@@ -19,7 +19,6 @@ file is the roster, the per-kind command table, and the procedure.
19
19
  | `arch-cycles.json` | `npm run check:arch` |
20
20
  | `cyclomatic.json` | `npm run check:cyclomatic` |
21
21
  | `context-budget.json` | `npm run check:context-budget` |
22
- | `workflow-citations.json` | `npm run check:workflow-citations` |
23
22
  | `agents-loc.csv` | `npm run baseline:agents-loc` |
24
23
 
25
24
  A refresh is **not** a regression entry — it tells the ratchet that the new
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-09-14T11:57:26.498Z",
2
+ "generatedAt": "2026-09-17T13:37:08.250Z",
3
3
  "generator": "generate-skills-index.js@1",
4
4
  "skills": [
5
5
  {
@@ -87,7 +87,7 @@
87
87
  "tier": "stack",
88
88
  "category": "qa",
89
89
  "path": ".agents/skills/stack/qa/playwright/SKILL.md",
90
- "description": "Robust E2E browser testing with Playwright. Use when writing browser-driven tests — leverage auto-waiting (no `waitForTimeout`), prefer user-visible locators (`getByRole`, `getByText`, `getByLabel`) over CSS/XPath, reuse `storageState` for auth, and enable trace-on-first-retry for CI debugging.",
90
+ "description": "Robust E2E browser testing with Playwright. Use when writing browser-driven tests — leverage auto-waiting (no `waitForTimeout`), prefer user-visible locators (`getByRole`, `getByText`, `getByLabel`) over CSS/XPath, reuse `storageState` for auth, and enable trace-on-first-retry for CI debugging. Also carries the `data-testid` contract a UI change states in its acceptance criteria.",
91
91
  "policyCapsuleBullets": 8,
92
92
  "allowedTools": null,
93
93
  "vendor": "playwright"
@@ -5,6 +5,8 @@ description:
5
5
  tests — leverage auto-waiting (no `waitForTimeout`), prefer user-visible
6
6
  locators (`getByRole`, `getByText`, `getByLabel`) over CSS/XPath, reuse
7
7
  `storageState` for auth, and enable trace-on-first-retry for CI debugging.
8
+ Also carries the `data-testid` contract a UI change states in its acceptance
9
+ criteria.
8
10
  vendor: playwright
9
11
  ---
10
12
 
@@ -21,6 +23,30 @@ vendor: playwright
21
23
  - Use a unique data set per test run, or tear down state explicitly, to prevent cross-test contamination.
22
24
  - Never let Playwright own the lifetime of a dev server it did not start: boot the server out-of-band, point the suite at the running origin, and set `reuseExistingServer` so `webServer` only probes readiness.
23
25
 
26
+ ## The testid contract
27
+
28
+ A `data-testid` is a selector two artifacts share: the component that renders
29
+ it and the spec that queries it. Renaming one without the other is a silent
30
+ break — the suite still compiles, and the failure arrives as a missing element
31
+ at run time, in CI, attributed to whichever change happened to land next.
32
+
33
+ So a change touching UI (`*.tsx`, `*.astro`, `*.svelte`, `*.vue`, a components
34
+ folder) states which side of that contract it is on, in its own acceptance
35
+ criteria, as one of:
36
+
37
+ - **`data-testid invariance: <the testids that MUST be preserved>`** — the
38
+ change reshapes markup and renames nothing, and the list says what the
39
+ reshape may not touch.
40
+ - **`data-testid changes: <old> -> <new>, with the matching
41
+ tests/e2e/*.spec.ts selector updated`** — the rename is deliberate, and the
42
+ spec file that queries it is edited in the same change (or in one ordered
43
+ ahead of it, when the work is split).
44
+
45
+ Renaming a testid without the matching spec edit is forbidden either way. When
46
+ a change deliberately renames nothing, say so in its negative-scope prose too:
47
+ a preserved set stated only once reads as an omission the next author is free
48
+ to revise.
49
+
24
50
  ## Running a `webServer`-backed suite outside CI
25
51
 
26
52
  Playwright's `webServer` block **watches the process it spawned**. That