baldart 4.89.2 → 4.92.1

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 (34) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/VERSION +1 -1
  3. package/framework/.claude/agents/api-perf-cost-auditor.md +9 -0
  4. package/framework/.claude/agents/code-reviewer.md +9 -0
  5. package/framework/.claude/agents/codebase-architect.md +6 -0
  6. package/framework/.claude/agents/coder.md +1 -1
  7. package/framework/.claude/agents/markup-fidelity-verifier.md +24 -1
  8. package/framework/.claude/agents/qa-sentinel.md +9 -0
  9. package/framework/.claude/agents/security-reviewer.md +9 -0
  10. package/framework/.claude/agents/ui-expert.md +49 -0
  11. package/framework/.claude/agents/visual-fidelity-verifier.md +16 -12
  12. package/framework/.claude/skills/e2e-review/CHANGELOG.md +59 -0
  13. package/framework/.claude/skills/e2e-review/SKILL.md +187 -26
  14. package/framework/.claude/skills/new/CHANGELOG.md +29 -0
  15. package/framework/.claude/skills/new/SKILL.md +44 -2
  16. package/framework/.claude/skills/new/references/completeness.md +1 -0
  17. package/framework/.claude/skills/new/references/implement.md +19 -3
  18. package/framework/.claude/skills/new/references/review-cycle.md +23 -1
  19. package/framework/.claude/skills/new/references/team-mode.md +4 -0
  20. package/framework/.claude/skills/new/scripts/verify-item.sh +54 -0
  21. package/framework/.claude/skills/new2/CHANGELOG.md +46 -0
  22. package/framework/.claude/skills/new2/SKILL.md +58 -10
  23. package/framework/.claude/skills/ui-implement/CHANGELOG.md +20 -0
  24. package/framework/.claude/skills/ui-implement/SKILL.md +37 -7
  25. package/framework/.claude/skills/ui-implement/references/integration.md +12 -3
  26. package/framework/.claude/skills/ui-implement/scripts/check-bindings.mjs +103 -0
  27. package/framework/.claude/workflows/new-card-review.js +19 -5
  28. package/framework/.claude/workflows/new-final-review.js +24 -9
  29. package/framework/.claude/workflows/new2.js +84 -5
  30. package/framework/agents/code-search-protocol.md +24 -0
  31. package/framework/agents/design-system-protocol.md +27 -0
  32. package/framework/docs/WORKFLOWS.md +9 -4
  33. package/framework/scripts/structural-compare.mjs +183 -0
  34. package/package.json +1 -1
@@ -2,6 +2,52 @@
2
2
 
3
3
  Formato: [Keep a Changelog](https://keepachangelog.com/) · [SemVer](https://semver.org/).
4
4
 
5
+ ## 1.2.1 — 2026-07-02
6
+
7
+ Review avversariale post-release (3 lenti indipendenti, finding verificati):
8
+ - **Step 3.5 ramo AUTONOMOUS**: env-autonomous / origine delegata → mai una domanda;
9
+ verify green ⇒ prosegui, altrimenti `degraded` (deferral owner-gated di fine batch);
10
+ mai un apply command autonomo.
11
+ - **Origine delegata = autonoma ovunque** (Step 1.5): l'escape hatch 3b è SKIPPED anche
12
+ con TTY (niente ciclo /new→new2→/new a costo pieno inline).
13
+ - **Contatore resume DURABILE** (Step 5.3): `resume_attempt: N/3` appeso al batch
14
+ tracker prima di ogni re-invocazione (una compaction non resetta il bound) +
15
+ teardown del broker Codex prima di ogni uscita per stallo.
16
+ - `new2.js`: verifier strutturale `status:"skipped"` = UNDERRUN (mai PASS);
17
+ re-verify deterministica una-tantum dopo un fix strutturale risolto;
18
+ `bindingCheck` CONSUMATO (violazioni → resolve ui; assente → ledger, fail-open);
19
+ `${MAIN}` quotato dentro `$(git -C …)` (path con spazi).
20
+
21
+ ## 1.2.0 — 2026-07-02
22
+
23
+ - **Resume loop HARD-BOUNDED (Step 5.3)**: `resumeMaxAttempts = 3` con contatore
24
+ esplicito + stall detection (stesso set di card incomplete = stop immediato).
25
+ Un `degraded` persistente è un outage/blocker strutturale, non converge iterando
26
+ (~0–200k+ token a ciclo).
27
+
28
+ ## 1.1.1 — 2026-07-02
29
+
30
+ - Fix B4 (location parallela in `new2.js`): il preflight creava il tracker in
31
+ `/tmp/batch-tracker-…` (volatile — la causa dei 2/3 run con "tracker LOST");
32
+ ora la location DURABILE `$(git -C $MAIN rev-parse --git-common-dir)/baldart/
33
+ run/…` allineata a new/SKILL.md § Context Tracking. Anche il riferimento del
34
+ merge allo stash Phase-0 aggiornato.
35
+ - Cap adattivo del fan-out nei workflow review (`new-card-review.js` /
36
+ `new-final-review.js`): 5 → 2 al primo transient rate-limit (era fisso 3).
37
+
38
+ ## 1.1.0 — 2026-07-02
39
+
40
+ - **Promossa a motore di default di `/new -auto`** (delegation gate v4.90.0):
41
+ non più solo variante sperimentale A/B. Nuovo Step 1.5 (contratto della
42
+ delega: grammar residua, mai `-auto`/`-auto-ship`/`-inline`; batch delegati
43
+ sempre migration-free — trovare una migrazione dichiarata su run delegata =
44
+ HALT + handback a `/new -inline`, mai una domanda).
45
+ - `new2.js`: lane di fedeltà strutturale route-free per card con mockup
46
+ (markup-fidelity-verifier + obbligo Lane-Ledger: verifier assente →
47
+ `verification-coverage-gap`, card mai auto-DONE) + binding gate deterministico
48
+ nel brief di implement + campi cardGraph `designSrcDir`/`designHtml`/
49
+ `hasBindings` (T1.A).
50
+
5
51
  ## 1.0.0 — 2026-07-01
6
52
 
7
53
  - Baseline: versioning per-skill introdotto al framework v4.82.0.
@@ -1,15 +1,18 @@
1
1
  ---
2
2
  name: new2
3
3
  effort: high
4
- version: 1.0.0
4
+ version: 1.2.1
5
5
  description: >
6
- EXPERIMENTAL workflow-hosted variant of /new (A/B testing). Implements one or
6
+ Workflow-hosted batch engine for /new. Implements one or
7
7
  more backlog cards end-to-end by delegating the WHOLE batch to a background
8
8
  dynamic workflow — so subagent output never enters the main orchestrator
9
9
  context. The batch runs autonomously (zero AskUserQuestion during the run): every
10
10
  /new gate is replaced by a deterministic policy + a self-healing resolution pass;
11
11
  in interactive mode an optional post-batch escape hatch can hand the hard-case
12
- follow-ups to /new for the real human gate. Claude-only (needs the Workflow tool).
12
+ follow-ups to /new for the real human gate. Since v4.90.0 this is ALSO the
13
+ default engine of `/new -auto` (its delegation gate invokes this skill when the
14
+ Workflow tool + new2.js are present — measured driver: 53–59% orchestrator
15
+ turn-tax on inline -auto runs). Claude-only (needs the Workflow tool).
13
16
  Usage: /new2 CARD-IDS (same arg grammar as /new). Triggers on:
14
17
  /new2, "implementa le card con workflow", "new2".
15
18
  ---
@@ -71,6 +74,24 @@ Also verify the workflow script is linked: if `.claude/workflows/new2.js` is abs
71
74
  print the same guidance and HALT (the framework did not link it — Codex tree, or a
72
75
  pre-v4.16 install).
73
76
 
77
+ ### Step 1.5 — Called by `/new -auto` (delegation note, v4.90.0)
78
+
79
+ `/new`'s AUTONOMOUS OFF-CONTEXT DELEGATION GATE invokes this skill with the same
80
+ residual grammar (card IDs + `-full` + `-stats` + `effort=`) — never
81
+ `-auto`/`-auto-ship`/`-inline` (this skill IS autonomous by contract; the
82
+ `AUTO_SHIP` deploy boundary stays in `/new`'s classic path). **A delegated run is
83
+ AUTONOMOUS-ORIGIN**: treat it exactly like the env-signalled autonomous state
84
+ everywhere this skill branches on it — Step 3.5 resolves deterministically (its
85
+ AUTONOMOUS branch) and **Step 3b's escape hatch is SKIPPED** (the user asked for
86
+ `-auto`; a post-batch question — and a possible `/new` re-invocation at full
87
+ inline cost — would break that promise even with a TTY present). `/new` only
88
+ delegates **migration-free** batches (it probes `migration_plan.required`
89
+ itself), so Step 3.5's one interactive question cannot fire on a delegated run —
90
+ if you nonetheless find a declared, unapplied migration in a delegated batch,
91
+ HALT and hand back to `/new` classic (`"batch con migrazione dichiarata — riesegui
92
+ /new <cards> -auto -inline"`) instead of asking. Everything else is identical to a
93
+ direct `/new2` invocation.
94
+
74
95
  ### Step 2 — Parse the invocation (same grammar as `/new`)
75
96
 
76
97
  Parse `CARD-IDS` exactly as `/new` does — space-separated, hyphen-range
@@ -123,6 +144,14 @@ untouched), so the schema is live **before** the workflow starts. It mirrors `/n
123
144
  modalities (always include "Già applicata — prosegui" and "Abort"). This is the SAME class as the
124
145
  Step-2 "ONE pre-launch question" — pre-launch, not a mid-run gate; the zero-ask contract is about
125
146
  the *workflow*, which is untouched.
147
+ **AUTONOMOUS branch (v4.92.1 — env `BALDART_AUTONOMOUS` / `CI` / `GITHUB_ACTIONS`, or a
148
+ delegated `/new -auto` origin per Step 1.5): NEVER ask.** Resolve deterministically:
149
+ (a) `migration_plan.verify` exists → run it; green ⇒ treat as "Già applicata — prosegui",
150
+ red/absent ⇒ (b) set `migration = { status: 'degraded', reason: 'autonomous —
151
+ owner-gated apply deferred' }` and continue (the workflow's end-of-batch owner-gated
152
+ deferral handles it — a `db-migration-deploy` residual, exactly the `/new` Phase 0 1b
153
+ autonomous disposition). Never run an apply **command** modality autonomously (it is an
154
+ outward/irreversible action).
126
155
  6. **Execute the choice in `$MAIN`**:
127
156
  - **Schema-deploy-from-trunk-only guard** (when `stack.schema_deploy_from_trunk_only: true`): a
128
157
  **command** modality is a remote schema deploy → run it only from trunk. Check
@@ -239,11 +268,29 @@ returns when the batch is done. It returns:
239
268
  prints nothing, set the epic card `status: DONE` + `completed_date` + note
240
269
  `"epic-closure gate — all children DONE (post-run, new2 skill)"`, folded into the SAME
241
270
  reconciliation commit. If any child is still open, leave the epic untouched.
242
- 3. **Resume if degraded.** If `degraded` is true, re-invoke the workflow with
243
- `Workflow({ scriptPath, resumeFromRunId })` (same `args` + the new `ts`). The
244
- per-card **skip-completed** guard makes the resume idempotent — already-committed
245
- cards are skipped, only the incomplete/blocked ones run. Repeat until `degraded`
246
- is false (or the same cards stall twice surface to the user).
271
+ 3. **Resume if degraded — HARD-BOUNDED (v4.92.0).** If `degraded` is true, re-invoke
272
+ the workflow with `Workflow({ scriptPath, resumeFromRunId })` (same `args` + the
273
+ new `ts`). The per-card **skip-completed** guard makes the resume idempotent —
274
+ already-committed cards are skipped, only the incomplete/blocked ones run.
275
+ Bounds (BOTH enforced, count explicitly do not rely on remembering):
276
+ - **`resumeMaxAttempts = 3`** total re-invocations per batch. The counter is
277
+ DURABLE, not a working note: append `resume_attempt: N/3 | <ts> | incomplete:
278
+ <card list>` to the batch tracker (`$(git -C "$MAIN" rev-parse
279
+ --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md`) BEFORE each
280
+ re-invocation — a compaction/crash between resumes must not reset the count
281
+ (re-read the tracker's `resume_attempt` lines to recover it). At 3 still
282
+ `degraded` → STOP resuming: run the **Codex broker teardown first** (the same
283
+ idempotent two-line block as step 6 below — a stall exit must not leak the
284
+ broker), then surface `"batch stalled after 3 resumes — manual intervention
285
+ needed"` with the incomplete card list (their follow-ups are already on disk
286
+ from step 1 — nothing is lost by stopping).
287
+ - **Stall detection**: if a resume completes with the SAME incomplete card set
288
+ as the previous attempt (zero progress — compare against the tracker's last
289
+ `resume_attempt` line), stop immediately even before the cap — a second
290
+ identical pass will not converge either. Same teardown-then-surface exit.
291
+ An unbounded resume loop re-pays the whole batch-loop cost per cycle (~0–200k+
292
+ tokens each) with no new information; a persistent `degraded` is an outage or a
293
+ structural blocker, not something iteration fixes.
247
294
  3b. **Escape-hatch escalation for the hard cases (INTERACTIVE mode only — the `new2`
248
295
  "relaxation").** `new2` is autonomous *during the batch* — but a genuinely-blocked
249
296
  card (the workflow rolled it back / left it `IN_PROGRESS`, DoD not met) is exactly the
@@ -256,8 +303,9 @@ returns when the batch is done. It returns:
256
303
  the offer is purely additive over an already-safe ledger — declining (or a closed
257
304
  terminal) never drops a residual.
258
305
  - **Skip this step entirely in AUTONOMOUS mode** (env `BALDART_AUTONOMOUS` / `CI` /
259
- `GITHUB_ACTIONS` set, or no TTY) leave the cards `IN_PROGRESS` + their follow-ups,
260
- exactly as before. The escape hatch is interactive-only.
306
+ `GITHUB_ACTIONS` set, no TTY, **or a delegated `/new -auto` origin Step 1.5**) —
307
+ leave the cards `IN_PROGRESS` + their follow-ups, exactly as before. The escape
308
+ hatch is interactive-only, and a delegated run is autonomous even with a TTY.
261
309
  - **Eligible set** = the follow-ups whose residual `deferralClass` is **code-actionable**:
262
310
  `unresolved`, `out-of-ownership`, `scope-expansion`. EXCLUDE `owner-gated` /
263
311
  `not-a-code-defect` / `policy-deferred-ac` (external infra steps — `/new` cannot perform
@@ -2,6 +2,26 @@
2
2
 
3
3
  Formato: [Keep a Changelog](https://keepachangelog.com/) · [SemVer](https://semver.org/).
4
4
 
5
+ ## 1.1.1 — 2026-07-02
6
+
7
+ - Return Contract (SKILL.md): documentati i campi `bindings` + `lane_coverage`/`lanes`
8
+ del JSON programmatico (lo schema resta SSOT in references/integration.md).
9
+
10
+ ## 1.1.0 — 2026-07-02
11
+
12
+ - **Binding-compliance gate (Step 5b)**: nuovo script deterministico
13
+ `scripts/check-bindings.mjs` — ogni `component_bindings` entry `reuse`/
14
+ `reuse-variant` deve comparire (import o uso JSX) nei file toccati dalla build;
15
+ violazione = `binding-not-honored` (Critical, 1 retry a `ui-expert`, poi
16
+ `blocked`). Chiude il buco "la mappa è autoritativa ma nessuno verifica che la
17
+ build l'abbia onorata". Fail-open sugli errori interni dello script.
18
+ - **Build-with-eyes nel briefing (Step 4)**: il brief di `ui-expert` ora porta
19
+ l'obbligo del self-check in-build (`ui-expert.md` § "Build-with-eyes": check
20
+ strutturale deterministico via `structural-compare.mjs` + check visivo con
21
+ screenshot Playwright quando catturabile, max 2 iterazioni, degrado onesto su
22
+ runtime non-multimodale) — costruisci-bene a monte invece di
23
+ cattura-e-ripara a valle.
24
+
5
25
  ## 1.0.0 — 2026-07-01
6
26
 
7
27
  - Skill introdotta (framework v4.82.0). Estrae da `/new` il playbook mockup→code
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ui-implement
3
3
  effort: medium
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  description: >
6
6
  Implement an ALREADY-APPROVED mockup into the codebase end-to-end and
7
7
  self-verify its fidelity. Takes a mockup in any channel — a Claude Design
@@ -123,11 +123,15 @@ Each step is a delegation. Read the cited SSOT; do not paraphrase it.
123
123
  4. **Implement — spawn `ui-expert`.** Build via the `ui-expert` agent with a
124
124
  briefing that carries: the chosen build workflow (Step 1), the mockup path(s),
125
125
  the registry-first cascade declaration requirement, the design-source
126
- declaration requirement (when code-src exists), the `component_bindings`, and
127
- the i18n / states-taxonomy / token-cascade constraints. All of these are
128
- `ui-expert.md`'s own contracts the briefing points at them; it does not
129
- re-teach them. (Codex / no-subagents the skill performs the build inline
130
- following the same `ui-expert.md` contracts.)
126
+ declaration requirement (when code-src exists), the `component_bindings`, the
127
+ **build-with-eyes self-check obligation** (`ui-expert.md` § "Build-with-eyes"
128
+ bounded structural + visual self-check BEFORE returning; the completion
129
+ report must carry its one-line outcome), and the i18n / states-taxonomy /
130
+ token-cascade constraints. All of these are `ui-expert.md`'s own contracts
131
+ the briefing points at them; it does not re-teach them. (Codex /
132
+ no-subagents → the skill performs the build inline following the same
133
+ `ui-expert.md` contracts; a non-multimodal runtime degrades the visual
134
+ self-check to a logged skip.)
131
135
 
132
136
  5. **Ground the design-source declaration** *(only when the card has
133
137
  `links.design_src` / `mockups/_src/` exists)*. After the agent's first
@@ -137,6 +141,27 @@ Each step is a delegation. Read the cited SSOT; do not paraphrase it.
137
141
  re-spawn. Log `design-source: grounded` | `rejected (<reason>)`. (This is the
138
142
  Step 7b grounding lifted out of `/new`.)
139
143
 
144
+ 5b. **Binding-compliance gate (deterministic — only when the card carries
145
+ `component_bindings`).** The bindings are authoritative (Step 3), but until
146
+ now nothing verified the built code actually HONORED them. Run:
147
+
148
+ ```bash
149
+ node .claude/skills/ui-implement/scripts/check-bindings.mjs \
150
+ --bindings-json '<the card's component_bindings serialized to JSON>' \
151
+ --files <the build's touched implementation files>
152
+ ```
153
+
154
+ One JSON line on stdout; exit `3` = violations. Every `reuse`/`reuse-variant`
155
+ binding whose component appears in NO touched file (import or JSX usage) is a
156
+ **`binding-not-honored`** finding (Critical, UI domain): re-spawn `ui-expert`
157
+ ONCE with the violation list ("the binding map is authoritative — use
158
+ `<Component>` for region `<region>`, do not re-invent it"); still violated →
159
+ the card is `blocked` (this is a fidelity-contract breach, not a taste issue).
160
+ `action: new` entries are informational here — their governance belongs to the
161
+ Post-Intervention Coherence Check (Step 6). Script errors fail OPEN (`skipped`,
162
+ exit 0) — the gate is a safety net, never a tribunal. Log
163
+ `bindings: ok (N honored)` | `violations (<components>)` | `skipped`.
164
+
140
165
  6. **Post-Intervention Coherence Check (BLOCKING).** Reconcile the design-system
141
166
  sources in the SAME change — CITE `design-system-protocol.md`
142
167
  § "Post-Intervention Coherence Check": INDEX (`DS_INDEX_DRIFT`), component spec
@@ -208,7 +233,12 @@ older install), `/new` falls back to an inline **coarse** build brief + a direct
208
233
 
209
234
  **Mode:** COMPACT (default). Your final message is bounded — verdict
210
235
  (`passed | blocked | overridden | skipped | error`) + the chosen build workflow +
211
- key findings as `path:line` + `Report: <path>`. Persist the long form (the
236
+ key findings as `path:line` + `Report: <path>`. The programmatic JSON ALSO carries
237
+ (since 1.1.1): `bindings` (the Step 5b `check-bindings.mjs` one-line result, when
238
+ the card has `component_bindings`) and `lane_coverage` + `lanes` (forwarded from
239
+ the internal `/e2e-review` report) — full schema:
240
+ [references/integration.md](references/integration.md) § Return contract (the
241
+ SSOT; do not restate it here). Persist the long form (the
212
242
  `/e2e-review` report + tracker), return the short form. FULL narrative only when the
213
243
  user invoked you directly. Schema + persist-then-summarize rule:
214
244
  `framework/agents/return-contract-protocol.md`.
@@ -34,6 +34,9 @@ only the input source and the return shape differ.
34
34
  "findings": [ /* gating findings, canonical severity taxonomy */ ],
35
35
  "ds_drift_codes": [ "DS_INDEX_DRIFT" | "DS_COMPONENT_STALE" | "DS_TOKENS_DRIFT" ],
36
36
  "design_system_coherence": { /* INDEX / components / tokens / mirror reconciled */ },
37
+ "bindings": { /* Step 5b check-bindings.mjs one-line JSON (skill ≥1.1.0); absent when the card has no component_bindings */ },
38
+ "lane_coverage": "complete" | "incomplete",
39
+ "lanes": { /* forwarded from the internal /e2e-review report (skill ≥1.1.0, e2e-review ≥1.1.0) */ },
37
40
  "report_path": ".baldart/e2e-review/<id>/report.json"
38
41
  }
39
42
  ```
@@ -85,8 +88,14 @@ same opt-in contract as `/prd` → `/ds-handoff`.
85
88
  ## `new2` (workflow) note
86
89
 
87
90
  `new2.js` hosts the batch in a background workflow and reads `implement.md` /
88
- `review-cycle.md` for the per-card semantics. In this release `new2` keeps its
89
- **inline** implement+verify path (the fallback prose stays valid), so no `new2.js`
90
- change is required. Full `new2` parity — delegating the UI card's implement+verify
91
+ `review-cycle.md` for the per-card semantics. `new2` keeps its **inline**
92
+ implement path (the fallback prose stays valid), with two T1.A parallels wired in
93
+ directly (v4.90.0): the mockup card's implement brief carries the
94
+ **binding-compliance gate** (same `check-bindings.mjs` script as Step 5b), and a
95
+ **route-free structural fidelity lane** (`markup-fidelity-verifier`) runs after
96
+ implement for every card with a code-form mockup — its Lane-Ledger obligation
97
+ (`design-system-protocol.md` § 6): verifier unavailable → `verification-coverage-gap`
98
+ residual, the card cannot auto-DONE. Browser lanes (visual/quality) remain
99
+ `/new`-path only. Full `new2` parity — delegating the UI card's implement+verify
91
100
  off-context via `workflow()` — is a follow-up, to be taken after the context-economy
92
101
  measurement (`SKILL.md` scope note + the plan's measure-first gate).
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ // check-bindings.mjs — deterministic `binding-not-honored` gate (T1.A, framework v4.90.0).
3
+ //
4
+ // A card's `component_bindings` (card-schema.md § component_bindings) is AUTHORITATIVE
5
+ // over the mockup: each `action: reuse | reuse-variant` entry promises that the bound
6
+ // registry component is actually USED in the implementation. Until now nothing verified
7
+ // that promise after the build — a ui-expert that ignored a binding and re-invented the
8
+ // primitive sailed through (the verifier trio checks fidelity-to-mockup, not
9
+ // binding-compliance). This script closes that gap without a model call.
10
+ //
11
+ // Contract:
12
+ // node check-bindings.mjs --bindings-json '<json array>' --files <f1> <f2> ...
13
+ // node check-bindings.mjs --bindings-file <path.json> --files <f1> <f2> ...
14
+ //
15
+ // - `--bindings-json` is the card's `component_bindings` serialized to JSON by the
16
+ // caller (the orchestrator/agent already has the card in context; YAML parsing is
17
+ // deliberately NOT re-implemented here — the deterministic part is the code check).
18
+ // - `--files` are the implementation files the card touched (the build's scopeFiles).
19
+ // - Prints ONE JSON line to stdout:
20
+ // { status: "ok" | "violations" | "skipped",
21
+ // checked, honored, violations:[{component, region, action}],
22
+ // results:[{component, region, action, status, matched_in:[...]}] }
23
+ // - Exit codes: 0 = ok/skipped (nothing to gate), 3 = violations (gate fires).
24
+ // Internal errors NEVER crash the caller: fail-open → {status:"skipped"} exit 0
25
+ // (same philosophy as the framework-edit-gate: a safety net, not a tribunal).
26
+ //
27
+ // Matching rule (deterministic, conservative — prefer false-negative over false-block):
28
+ // an entry with action reuse/reuse-variant is HONORED when the component identifier
29
+ // appears in ≥1 provided file as an import specifier OR a JSX opening tag
30
+ // (`<Component` / `<NS.Component`). `action: new` entries are reported as
31
+ // informational (`new-component`) and never count as violations here — the
32
+ // Post-Intervention Coherence Check owns their governance.
33
+
34
+ import fs from 'node:fs';
35
+
36
+ function emit(obj, code) {
37
+ process.stdout.write(JSON.stringify(obj) + '\n');
38
+ process.exit(code);
39
+ }
40
+
41
+ function parseArgs(argv) {
42
+ const a = { bindingsJson: null, bindingsFile: null, files: [] };
43
+ for (let i = 2; i < argv.length; i++) {
44
+ if (argv[i] === '--bindings-json') a.bindingsJson = argv[++i];
45
+ else if (argv[i] === '--bindings-file') a.bindingsFile = argv[++i];
46
+ else if (argv[i] === '--files') {
47
+ while (i + 1 < argv.length && !argv[i + 1].startsWith('--')) a.files.push(argv[++i]);
48
+ }
49
+ }
50
+ return a;
51
+ }
52
+
53
+ function main() {
54
+ const args = parseArgs(process.argv);
55
+ let raw = args.bindingsJson;
56
+ if (!raw && args.bindingsFile) {
57
+ if (!fs.existsSync(args.bindingsFile)) emit({ status: 'skipped', reason: 'bindings file not found: ' + args.bindingsFile }, 0);
58
+ raw = fs.readFileSync(args.bindingsFile, 'utf8');
59
+ }
60
+ if (!raw) emit({ status: 'skipped', reason: 'no --bindings-json / --bindings-file' }, 0);
61
+
62
+ let bindings;
63
+ try { bindings = JSON.parse(raw); } catch (e) { emit({ status: 'skipped', reason: 'bindings JSON unparsable: ' + e.message }, 0); }
64
+ if (!Array.isArray(bindings) || bindings.length === 0) emit({ status: 'ok', checked: 0, honored: 0, violations: [], results: [], note: 'no bindings to check' }, 0);
65
+
66
+ const sources = [];
67
+ for (const f of args.files) {
68
+ try {
69
+ if (fs.existsSync(f) && fs.statSync(f).isFile()) sources.push({ path: f, text: fs.readFileSync(f, 'utf8') });
70
+ } catch { /* unreadable file → treated as absent, fail-open per-file */ }
71
+ }
72
+ if (sources.length === 0) emit({ status: 'skipped', reason: 'no readable implementation files passed via --files' }, 0);
73
+
74
+ const results = [];
75
+ for (const b of bindings) {
76
+ const component = String(b.component || b.name || '').trim();
77
+ const action = String(b.action || 'reuse').trim();
78
+ const region = String(b.region || '').trim();
79
+ if (!component) { results.push({ component: '(missing)', region, action, status: 'skipped-malformed', matched_in: [] }); continue; }
80
+ if (action === 'new') { results.push({ component, region, action, status: 'new-component', matched_in: [] }); continue; }
81
+
82
+ // Import specifier or JSX opening tag, word-bounded. Conservative: any hit counts.
83
+ const esc = component.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
84
+ const importRe = new RegExp(`import[^;]*\\b${esc}\\b[^;]*from`, 'm');
85
+ const jsxRe = new RegExp(`<(?:[A-Za-z_$][\\w$]*\\.)?${esc}[\\s/>]`);
86
+ const bareRe = new RegExp(`\\b${esc}\\b`);
87
+ const matched = sources.filter((s) => importRe.test(s.text) || jsxRe.test(s.text) || (/\.(css|scss)$/.test(s.path) && bareRe.test(s.text)));
88
+ results.push({
89
+ component, region, action,
90
+ status: matched.length ? 'honored' : 'not-found',
91
+ matched_in: matched.map((s) => s.path),
92
+ });
93
+ }
94
+
95
+ const violations = results.filter((r) => r.status === 'not-found').map(({ component, region, action }) => ({ component, region, action }));
96
+ const honored = results.filter((r) => r.status === 'honored').length;
97
+ emit(
98
+ { status: violations.length ? 'violations' : 'ok', checked: results.length, honored, violations, results },
99
+ violations.length ? 3 : 0
100
+ );
101
+ }
102
+
103
+ try { main(); } catch (e) { emit({ status: 'skipped', reason: 'internal error: ' + (e && e.message) }, 0); }
@@ -46,18 +46,28 @@ const protocolRef = '.claude/skills/new/references/review-cycle.md'
46
46
  // (kills agents mid-work). Cap concurrent agents at 3 — a rolling worker pool (NOT chunked barriers,
47
47
  // which would block fast finders behind the slow background Codex poll). Mirrors the team-mode coder
48
48
  // cap so review + implementation obey the same ceiling.
49
- const MAX_PARALLEL = 3
50
- async function parallelCapped(thunks, cap) {
49
+ // ADAPTIVE since v4.91.0 (mirrors new-final-review.js): start at 5; on the FIRST rate-limit
50
+ // transient anywhere in the run drop to 2 (below the old fixed 3 — when the shared org limit
51
+ // is hot, narrower is what keeps reviewers alive; when it is not, the fixed cap was pure
52
+ // queue-wait tax). Surplus workers retire at their next pickup.
53
+ const CAP = { value: 5 }
54
+ const MAX_PARALLEL = CAP.value // kept for arg-compat at call sites; capNow() is authoritative
55
+ const capNow = () => CAP.value
56
+ function noteRateLimitPressure(src) {
57
+ if (CAP.value > 2) { CAP.value = 2; log(`parallelCapped: rate-limit transient (${src}) → cap 5→2 for the rest of the run`) }
58
+ }
59
+ async function parallelCapped(thunks, _cap) {
51
60
  if (!Array.isArray(thunks) || !thunks.length) return []
52
61
  const results = new Array(thunks.length)
53
62
  let next = 0
54
- const worker = async () => {
63
+ const worker = async (idx) => {
55
64
  while (next < thunks.length) {
65
+ if (idx >= capNow()) return // adaptive shrink
56
66
  const i = next++
57
67
  try { results[i] = await thunks[i]() } catch (_) { results[i] = null }
58
68
  }
59
69
  }
60
- await Promise.all(Array.from({ length: Math.min(cap, thunks.length) }, worker))
70
+ await Promise.all(Array.from({ length: Math.min(capNow(), thunks.length) }, (_, idx) => worker(idx)))
61
71
  return results
62
72
  }
63
73
 
@@ -79,7 +89,11 @@ async function agentSafe(prompt, opts, maxAttempts) {
79
89
  try {
80
90
  const o = i === 0 ? opts : Object.assign({}, opts, { label: `${(opts && opts.label) || 'agent'}~retry${i}` })
81
91
  return await agent(prompt, o)
82
- } catch (e) { lastErr = e; if (!isTransient(e)) throw e } // permanent error → surface
92
+ } catch (e) {
93
+ lastErr = e
94
+ if (!isTransient(e)) throw e // permanent error → surface
95
+ noteRateLimitPressure((opts && opts.label) || 'agent') // v4.91.0 — shrink the pool for the rest of the run
96
+ }
83
97
  }
84
98
  const err = new Error('transient-exhausted: ' + String((lastErr && lastErr.message) || lastErr))
85
99
  err.transientExhausted = true
@@ -53,22 +53,33 @@ const editableFiles = Array.isArray(a.editableFiles) ? a.editableFiles.filter(Bo
53
53
  const tcCmds = (cfg.toolchain && cfg.toolchain.commands) || {}
54
54
  const tc = (key, fallback) => (tcCmds[key] && String(tcCmds[key]).trim()) || fallback
55
55
 
56
- // Concurrency cap (v4.53.6 API rate-limit guardrail; mirrors new-card-review.js + the team-mode
57
- // coder cap). The org-level rate limit is SHARED across every terminal/session; a wide agent fan-out
58
- // saturates it and the server throttles. Cap concurrent agents at 3 via a rolling worker pool (NOT
59
- // chunked barriers those would block fast finders behind the slow background Codex poll).
60
- const MAX_PARALLEL = 3
61
- async function parallelCapped(thunks, cap) {
56
+ // Concurrency cap (v4.53.6 guardrail, ADAPTIVE since v4.91.0 mirrors new-card-review.js + the
57
+ // team-mode coder cap). The org-level rate limit is SHARED across every terminal/session; a wide
58
+ // agent fan-out saturates it and the server throttles (the v4.66.0 transient-kill class). The old
59
+ // fixed cap of 3 protected against that but taxed EVERY run's wall-clock, including the majority
60
+ // with no rate pressure (measured: review fan-out ≈ 50 min on a small batch, mostly queue wait).
61
+ // Adaptive policy: start at 5; on the FIRST rate-limit transient seen anywhere in the run, drop to
62
+ // 2 for the remainder (BELOW the old cap — when the org limit is hot, narrower is what saves the
63
+ // reviewers). Rolling worker pool (NOT chunked barriers — those would block fast finders behind
64
+ // the slow background Codex poll); workers above the current cap retire at their next pickup.
65
+ const CAP = { value: 5 }
66
+ const MAX_PARALLEL = CAP.value // kept for arg-compat at call sites; capNow() is authoritative
67
+ const capNow = () => CAP.value
68
+ function noteRateLimitPressure(src) {
69
+ if (CAP.value > 2) { CAP.value = 2; log(`parallelCapped: rate-limit transient (${src}) → cap 5→2 for the rest of the run`) }
70
+ }
71
+ async function parallelCapped(thunks, _cap) {
62
72
  if (!Array.isArray(thunks) || !thunks.length) return []
63
73
  const results = new Array(thunks.length)
64
74
  let next = 0
65
- const worker = async () => {
75
+ const worker = async (idx) => {
66
76
  while (next < thunks.length) {
77
+ if (idx >= capNow()) return // adaptive shrink: retire surplus workers at pickup time
67
78
  const i = next++
68
79
  try { results[i] = await thunks[i]() } catch (_) { results[i] = null }
69
80
  }
70
81
  }
71
- await Promise.all(Array.from({ length: Math.min(cap, thunks.length) }, worker))
82
+ await Promise.all(Array.from({ length: Math.min(capNow(), thunks.length) }, (_, idx) => worker(idx)))
72
83
  return results
73
84
  }
74
85
 
@@ -91,7 +102,11 @@ async function agentSafe(prompt, opts, maxAttempts) {
91
102
  try {
92
103
  const o = i === 0 ? opts : Object.assign({}, opts, { label: `${(opts && opts.label) || 'agent'}~retry${i}` })
93
104
  return await agent(prompt, o)
94
- } catch (e) { lastErr = e; if (!isTransient(e)) throw e } // permanent error → surface
105
+ } catch (e) {
106
+ lastErr = e
107
+ if (!isTransient(e)) throw e // permanent error → surface
108
+ noteRateLimitPressure((opts && opts.label) || 'agent') // v4.91.0 — shrink the pool for the rest of the run
109
+ }
95
110
  }
96
111
  const err = new Error('transient-exhausted: ' + String((lastErr && lastErr.message) || lastErr))
97
112
  err.transientExhausted = true