baldart 4.92.0 → 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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to BALDART will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.92.1] - 2026-07-02
9
+
10
+ **Review avversariale post-release del motore new2 (coerenza interna).** Tre reviewer indipendenti a contesto fresco (dataflow / edge-case / contratti) sull'onda v4.90–4.92; ogni finding ri-verificato contro il codice prima di agire: **11 confermati e fixati, 9 refutati con evidenza empirica** (tra i refutati: la "race" del worker-pool adattivo — simulata: 0 buchi; `verify-item.sh` su bash 3.2 reale — funziona; `parseInt` con CRLF; un CAP attribuito a `new2.js` che non esiste).
11
+
12
+ **PATCH** — hardening senza cambi di superficie. **Nessuna nuova chiave config**. **Codex parity: portable/N/A** (prosa + workflow JS Claude-only).
13
+
14
+ ### Fixed
15
+
16
+ - **`new2.js`**: verifier strutturale che si auto-dichiara `status:"skipped"` = UNDERRUN (mai PASS — era l'ultimo canale di under-run mascherato); **re-verify deterministica** una-tantum dopo un fix strutturale risolto (il self-report del fixer non è la conferma del verifier); **`bindingCheck` consumato** (violazioni sopravvissute al pass dell'agent → resolve ui; report assente → ledger fail-open, mai silente); `${MAIN}` quotato dentro `$(git -C …)` (path con spazi).
17
+ - **`/e2e-review` 1.2.1**: stringa lane canonica `skipped:render_unavailable_structural_covered` scritta esplicitamente da Phase 3.8 (l'assertion di Phase 5 valida l'enum chiuso); chiave di convergenza del self-heal estesa con `viewport_role`.
18
+ - **`new2` 1.2.1**: Step 3.5 ramo AUTONOMOUS (CI/`BALDART_AUTONOMOUS`/origine delegata → mai una domanda, mai un apply command autonomo); **origine delegata = autonoma ovunque** (escape hatch 3b SKIPPED anche con TTY — chiuso il rischio di ciclo `/new`→`new2`→`/new` a costo pieno); contatore resume **durabile nel batch tracker** (una compaction non resetta il bound) + teardown broker Codex su ogni uscita per stallo.
19
+ - **`structural-compare.mjs`**: arbitrary value Tailwind con funzioni (`grid-cols-[repeat(2,_minmax(0,1fr))]`) contati con la `countTracks` parens-aware (era `split('_')` → 3 invece di 2, falso segnale di divergenza).
20
+ - **`ui-implement` 1.1.1**: Return Contract documenta i campi `bindings`/`lane_coverage`/`lanes` (schema SSOT in integration.md).
21
+
8
22
  ## [4.92.0] - 2026-07-02
9
23
 
10
24
  **Tier 3 del programma "deep analysis /new+/ui-implement": parity tripwire, bound dei loop, verifica payload new2.** Chiude il programma a 3 tier (4.90.0 → 4.92.0).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.92.0
1
+ 4.92.1
@@ -2,6 +2,16 @@
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
+ - Phase 3.8: la lane visual del route non renderizzabile con mockup code-form viene
8
+ marcata esplicitamente `skipped:render_unavailable_structural_covered` nel Lane
9
+ Matrix (l'enum canonico che l'assertion di Phase 5 step 0 valida); `gaps[]` tiene
10
+ il diagnostico `render_unavailable` — due stringhe deliberatamente distinte.
11
+ - Phase 5b step 3: chiave di convergenza estesa con `viewport_role`
12
+ (`route, source, category, viewport_role, region`) — una regressione
13
+ viewport-scoped non può più matchare falsamente il finding pre-fix.
14
+
5
15
  ## 1.2.0 — 2026-07-02
6
16
 
7
17
  - **Self-heal convergence detection (Phase 5b step 3)**: se il fix produce finding
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: e2e-review
3
3
  effort: medium
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  description: >
6
6
  Deterministic, BLOCKING end-to-end review run after a feature is implemented.
7
7
  Combines functional E2E (Playwright spec written by `coder`, executed via
@@ -520,8 +520,12 @@ For each route with a mockup (`mockup_source.level != "skip"`):
520
520
  - A comparable render was produced → it is the implementation screenshot for Phase 4.
521
521
  - Still degenerate after the loop **and** the mockup is **code-form** (a Phase 2.7
522
522
  `structural_source` exists) → the structural pass already covered fidelity; record
523
- `render_unavailable` in `gaps[]` and let Phase 4's visual step skip this route's
524
- desktop diff (no false coverage-gap — structure was checked route-free).
523
+ `render_unavailable` in `gaps[]` (diagnostic), set the route's visual lane in the
524
+ Lane Matrix to **`skipped:render_unavailable_structural_covered`** (the canonical
525
+ enum the Phase 5 step 0 assertion validates — the two strings are deliberately
526
+ distinct: `gaps[]` carries the diagnostic, `lanes` carries the closed-set reason),
527
+ and let Phase 4's visual step skip this route's desktop diff (no false
528
+ coverage-gap — structure was checked route-free).
525
529
  - Still degenerate after the loop **and** the mockup is **image-only**
526
530
  (`level ∈ {figma, local}` with no `structural_source`) → mark the route
527
531
  `coverage_obligation_unmet = true` for Phase 4 step 1c (it will synthesize a
@@ -894,7 +898,10 @@ For each iteration:
894
898
  the Phase 4b quality pre-filter already make a clean re-run cheap.
895
899
  3. **Re-aggregate and re-decide** (back to Phase 5 step 1), with **convergence
896
900
  detection (v4.92.0)**: compare the new gating-finding set with the pre-fix one by
897
- `(route, source, category, region)`. If the fix produced **NEW gating findings that
901
+ `(route, source, category, viewport_role, region)` the Phase 4 step 4
902
+ de-duplication key extended with `source`, so a viewport-scoped regression (a fix
903
+ that repairs desktop while breaking the mobile pass on the same route/category) is
904
+ a NEW finding, never a false match. If the fix produced **NEW gating findings that
898
905
  were not present before** (the fixer is regressing while fixing — the documented
899
906
  non-convergent failure mode), do NOT burn another iteration: STOP the loop now and
900
907
  transition to the override path with both sets in the report (`convergence:
@@ -2,6 +2,22 @@
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
+
5
21
  ## 1.2.0 — 2026-07-02
6
22
 
7
23
  - **Resume loop HARD-BOUNDED (Step 5.3)**: `resumeMaxAttempts = 3` con contatore
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: new2
3
3
  effort: high
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  description: >
6
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
@@ -79,7 +79,12 @@ pre-v4.16 install).
79
79
  `/new`'s AUTONOMOUS OFF-CONTEXT DELEGATION GATE invokes this skill with the same
80
80
  residual grammar (card IDs + `-full` + `-stats` + `effort=`) — never
81
81
  `-auto`/`-auto-ship`/`-inline` (this skill IS autonomous by contract; the
82
- `AUTO_SHIP` deploy boundary stays in `/new`'s classic path). `/new` only
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
83
88
  delegates **migration-free** batches (it probes `migration_plan.required`
84
89
  itself), so Step 3.5's one interactive question cannot fire on a delegated run —
85
90
  if you nonetheless find a declared, unapplied migration in a delegated batch,
@@ -139,6 +144,14 @@ untouched), so the schema is live **before** the workflow starts. It mirrors `/n
139
144
  modalities (always include "Già applicata — prosegui" and "Abort"). This is the SAME class as the
140
145
  Step-2 "ONE pre-launch question" — pre-launch, not a mid-run gate; the zero-ask contract is about
141
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).
142
155
  6. **Execute the choice in `$MAIN`**:
143
156
  - **Schema-deploy-from-trunk-only guard** (when `stack.schema_deploy_from_trunk_only: true`): a
144
157
  **command** modality is a remote schema deploy → run it only from trunk. Check
@@ -260,14 +273,21 @@ returns when the batch is done. It returns:
260
273
  new `ts`). The per-card **skip-completed** guard makes the resume idempotent —
261
274
  already-committed cards are skipped, only the incomplete/blocked ones run.
262
275
  Bounds (BOTH enforced, count explicitly — do not rely on remembering):
263
- - **`resumeMaxAttempts = 3`** total re-invocations per batch. Keep a literal
264
- counter in your working notes (`resume 1/3`, `2/3`, …); at 3 still `degraded`
265
- STOP resuming: surface `"batch stalled after 3 resumes — manual
266
- intervention needed"` with the incomplete card list (their follow-ups are
267
- already on disk from step 1 nothing is lost by stopping).
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).
268
287
  - **Stall detection**: if a resume completes with the SAME incomplete card set
269
- as the previous attempt (zero progress), stop immediately even before the
270
- cap a second identical pass will not converge either.
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.
271
291
  An unbounded resume loop re-pays the whole batch-loop cost per cycle (~0–200k+
272
292
  tokens each) with no new information; a persistent `degraded` is an outage or a
273
293
  structural blocker, not something iteration fixes.
@@ -283,8 +303,9 @@ returns when the batch is done. It returns:
283
303
  the offer is purely additive over an already-safe ledger — declining (or a closed
284
304
  terminal) never drops a residual.
285
305
  - **Skip this step entirely in AUTONOMOUS mode** (env `BALDART_AUTONOMOUS` / `CI` /
286
- `GITHUB_ACTIONS` set, or no TTY) leave the cards `IN_PROGRESS` + their follow-ups,
287
- 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.
288
309
  - **Eligible set** = the follow-ups whose residual `deferralClass` is **code-actionable**:
289
310
  `unresolved`, `out-of-ownership`, `scope-expansion`. EXCLUDE `owner-gated` /
290
311
  `not-a-code-defect` / `policy-deferred-ac` (external infra steps — `/new` cannot perform
@@ -2,6 +2,11 @@
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
+
5
10
  ## 1.1.0 — 2026-07-02
6
11
 
7
12
  - **Binding-compliance gate (Step 5b)**: nuovo script deterministico
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ui-implement
3
3
  effort: medium
4
- version: 1.1.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
@@ -233,7 +233,12 @@ older install), `/new` falls back to an inline **coarse** build brief + a direct
233
233
 
234
234
  **Mode:** COMPACT (default). Your final message is bounded — verdict
235
235
  (`passed | blocked | overridden | skipped | error`) + the chosen build workflow +
236
- 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
237
242
  `/e2e-review` report + tracker), return the short form. FULL narrative only when the
238
243
  user invoked you directly. Schema + persist-then-summarize rule:
239
244
  `framework/agents/return-contract-protocol.md`.
@@ -283,7 +283,7 @@ try {
283
283
  preflight = await agentSafe(
284
284
  `You are the deterministic PRE-FLIGHT for an autonomous /new batch (variant new2). Follow ${REF}/setup.md (Phase 0 + Pre-flight) and ${REF}/implement.md (Phase 1 depends-on gate) for the SEMANTICS, but replace EVERY AskUserQuestion with the deterministic policy below. You run all git/bash yourself (the workflow cannot).\n\n` +
285
285
  `${projectBrief}\n\nCards in batch (Read each YAML):\n${cardPaths.join('\n')}\nCard IDs: ${cardIds.join(' ')}\n\n` +
286
- `Create/maintain the recovery tracker at "$(git -C ${MAIN} rev-parse --git-common-dir)/baldart/run/batch-tracker-${firstCard}.md" (mkdir -p its dir first; per setup.md § Context Tracking — DURABLE location, v4.91.0: /tmp is volatile and a paused/overnight run lost it — B4).\n\n` +
286
+ `Create/maintain the recovery tracker at "$(git -C "${MAIN}" rev-parse --git-common-dir)/baldart/run/batch-tracker-${firstCard}.md" (mkdir -p its dir first; per setup.md § Context Tracking — DURABLE location, v4.91.0: /tmp is volatile and a paused/overnight run lost it — B4).\n\n` +
287
287
  `ROLE BOUNDARY (specialization integrity): you are the OPS/GIT agent. You NEVER edit source or doc files — any needed content change belongs to the coder specialist; report it instead.\n\n` +
288
288
  `DETERMINISTIC GATE POLICIES (NO user prompts):\n` +
289
289
  `• G1 dirty-tree (main repo ${MAIN}): partition framework-managed noise exactly as setup.md step 3 ($METRICS=${METRICS}, .baldart/generated|state.json|skill-conflicts.json — NOT overlays/). Genuine user work → auto-stash 'baldart-new2-${firstCard}' (main checkout) and record the label. Never commit/abort/prompt.\n` +
@@ -614,7 +614,7 @@ async function runCard(cardId, cardPath) {
614
614
  `Return: { epic, buildBlocked, blockedGate, unmetACs:[{n,text}], scopeFiles, mayEditPaths, revertedOutOfOwnership:[paths], fileDiffViolation, note }`,
615
615
  { label: `implement:${cardId}`, phase: 'Implement', agentType: ownerAgent,
616
616
  schema: { type: 'object', required: ['epic', 'buildBlocked', 'unmetACs', 'scopeFiles'], additionalProperties: true,
617
- properties: { epic: { type: 'boolean' }, buildBlocked: { type: 'boolean' }, blockedGate: { type: 'string' }, unmetACs: { type: 'array', items: { type: 'object', additionalProperties: true } }, scopeFiles: { type: 'array', items: { type: 'string' } }, mayEditPaths: { type: 'array', items: { type: 'string' } }, revertedOutOfOwnership: { type: 'array', items: { type: 'string' } }, fileDiffViolation: { type: 'boolean' }, note: { type: 'string' } } } }
617
+ properties: { epic: { type: 'boolean' }, buildBlocked: { type: 'boolean' }, blockedGate: { type: 'string' }, unmetACs: { type: 'array', items: { type: 'object', additionalProperties: true } }, scopeFiles: { type: 'array', items: { type: 'string' } }, mayEditPaths: { type: 'array', items: { type: 'string' } }, revertedOutOfOwnership: { type: 'array', items: { type: 'string' } }, fileDiffViolation: { type: 'boolean' }, note: { type: 'string' }, bindingCheck: { type: 'object', additionalProperties: true, description: 'check-bindings.mjs one-line JSON (mockup+bindings cards only)' } } } }
618
618
  )
619
619
  } catch (e) {
620
620
  if (e && e.transientExhausted) { noteDegraded('outage'); return { card: cardId, status: 'pending', gates } }
@@ -653,6 +653,23 @@ async function runCard(cardId, cardPath) {
653
653
  if (r.status !== 'resolved') { deferredOpen = true; deferredClasses.add(r.deferralClass || 'unresolved') }
654
654
  }
655
655
 
656
+ // --- R1-F2 (v4.92.1): the binding gate result is CONSUMED, not write-only. The agent
657
+ // was already told to run check-bindings.mjs and fix before returning; here the workflow
658
+ // verifies the report: violations survive the agent's own pass → resolve (ui domain);
659
+ // report absent on a bindings card → fail-open but LEDGERED (audit trail, never silent).
660
+ if (node.hasMockup && node.hasBindings) {
661
+ const bc = impl && impl.bindingCheck
662
+ const bcViol = (bc && Array.isArray(bc.violations)) ? bc.violations : []
663
+ if (!bc) g('binding-gate', 'NOT-REPORTED', 'agent returned no bindingCheck — fail-open (script may be absent on older installs), recorded for audit')
664
+ else if (bcViol.length) {
665
+ g('binding-gate', 'VIOLATIONS', bcViol.map((v) => v.component).join(', '))
666
+ for (const v of bcViol) {
667
+ const r = await resolve('blocker', cardId, `binding-not-honored: region "${v.region || ''}" is bound action:${v.action || 'reuse'} → ${v.component}, but the component appears in NO touched implementation file — the binding map is authoritative (card-schema.md § component_bindings)`, { mayEditPaths: mayEdit, scopeFiles, domain: 'ui' })
668
+ if (r.status !== 'resolved') { deferredOpen = true; deferredClasses.add(r.deferralClass || 'unresolved') }
669
+ }
670
+ } else g('binding-gate', 'OK', `${(bc && bc.honored) || 0}/${(bc && bc.checked) || 0} bindings honored`)
671
+ }
672
+
656
673
  // --- T1.A (v4.90.0): route-free structural fidelity lane — the new2 parallel of
657
674
  // /e2e-review Phase 2.7. new2 has NO browser lanes (visual/quality verify runs only on
658
675
  // the /new path), so the code-form structural check is its ONE per-card fidelity lane.
@@ -664,26 +681,42 @@ async function runCard(cardId, cardPath) {
664
681
  const structSrc = node.designSrcDir
665
682
  ? { level: 'design_src', design_src_dir: node.designSrcDir }
666
683
  : { level: 'html', mockup_path: node.designHtml }
684
+ const spawnMfv = () => agentSafe(
685
+ `Compare the implementation's code structure against the mockup's code structure and return JSON per your output schema (route-free — no browser, no dev server; cd into ${sharedCtx.worktreePath}).\n\nInput payload:\n${JSON.stringify({ card_id: cardId, route: '(card-level)', mockup_source: structSrc, implementation_files: scopeFiles, tolerance: 'strict' })}\n\nReturn STRICT JSON only: { status, findings:[{category,severity,description,expected,actual,fix_hint}] }`,
686
+ { label: `fidelity:${cardId}:structural`, phase: 'Implement', agentType: 'markup-fidelity-verifier',
687
+ schema: { type: 'object', required: ['findings'], additionalProperties: true, properties: { status: { type: 'string' }, findings: { type: 'array', items: { type: 'object', additionalProperties: true } } } } }
688
+ )
667
689
  let mfv = null
668
- try {
669
- mfv = await agentSafe(
670
- `Compare the implementation's code structure against the mockup's code structure and return JSON per your output schema (route-free — no browser, no dev server; cd into ${sharedCtx.worktreePath}).\n\nInput payload:\n${JSON.stringify({ card_id: cardId, route: '(card-level)', mockup_source: structSrc, implementation_files: scopeFiles, tolerance: 'strict' })}\n\nReturn STRICT JSON only: { findings:[{category,severity,description,expected,actual,fix_hint}] }`,
671
- { label: `fidelity:${cardId}:structural`, phase: 'Implement', agentType: 'markup-fidelity-verifier',
672
- schema: { type: 'object', required: ['findings'], additionalProperties: true, properties: { findings: { type: 'array', items: { type: 'object', additionalProperties: true } } } } }
673
- )
674
- } catch (e) {
675
- if (e && e.transientExhausted) noteDegraded('outage')
676
- }
677
- if (!mfv) {
678
- g('fidelity-structural', 'UNDERRUN', 'planned lane never executed (verifier unavailable) — verification-coverage-gap, card cannot auto-DONE')
690
+ try { mfv = await spawnMfv() } catch (e) { if (e && e.transientExhausted) noteDegraded('outage') }
691
+ // R1-F1 (v4.92.1): a verifier that RETURNS but self-reports `status:"skipped"`
692
+ // (no_structural_ground_truth unreadable mockup / no layout-bearing impl) is an
693
+ // under-run exactly like an unavailable verifier, NEVER a PASS.
694
+ const mfvSkipped = mfv && /skip/i.test(String(mfv.status || ''))
695
+ if (!mfv || mfvSkipped) {
696
+ g('fidelity-structural', 'UNDERRUN', `planned lane did not verify (${!mfv ? 'verifier unavailable' : 'verifier skipped: ' + String((mfv && mfv.reason) || 'no_structural_ground_truth')}) — verification-coverage-gap, card cannot auto-DONE`)
679
697
  deferredOpen = true; deferredClasses.add('unresolved')
680
- residuals.push({ card: cardId, kind: 'verification-coverage-gap', evidence: 'structural fidelity lane planned but not executed (markup-fidelity-verifier unavailable)', materialized: false })
698
+ residuals.push({ card: cardId, kind: 'verification-coverage-gap', evidence: `structural fidelity lane planned but did not verify (${!mfv ? 'markup-fidelity-verifier unavailable' : 'verifier skipped'})`, materialized: false })
681
699
  } else {
682
- const gating = (mfv.findings || []).filter((f) => /critical|major/i.test(String(f.severity || '')))
700
+ const gatingOf = (x) => ((x && x.findings) || []).filter((f) => /critical|major/i.test(String(f.severity || '')))
701
+ const gating = gatingOf(mfv)
683
702
  g('fidelity-structural', gating.length ? 'BLOCKED' : 'PASS', gating.length ? `${gating.length} gating structural finding(s)` : `ran — ${(mfv.findings || []).length} finding(s), none gating`)
703
+ let anyResolved = false
684
704
  for (const f of gating) {
685
705
  const r = await resolve('blocker', cardId, `structural fidelity (${f.category || 'layout'}): ${f.description || ''} — expected: ${f.expected || '(see mockup)'}`, { mayEditPaths: mayEdit, scopeFiles, domain: 'ui' })
686
706
  if (r.status !== 'resolved') { deferredOpen = true; deferredClasses.add(r.deferralClass || 'unresolved') }
707
+ else anyResolved = true
708
+ }
709
+ // R1-F3 (v4.92.1): a resolved structural fix is RE-VERIFIED by the deterministic
710
+ // verifier once (mirror of /e2e-review Phase 5b step 2 — a fixer's self-report is
711
+ // not the verifier's confirmation). Single re-check, no loop: still-gating after
712
+ // the re-run → deferral (the follow-up carries it), never a second fix pass here.
713
+ if (anyResolved) {
714
+ let mfvRe = null
715
+ try { mfvRe = await spawnMfv() } catch (e) { if (e && e.transientExhausted) noteDegraded('outage') }
716
+ const reGating = mfvRe && !/skip/i.test(String(mfvRe.status || '')) ? gatingOf(mfvRe) : null
717
+ if (reGating === null) { g('fidelity-structural-reverify', 'UNDERRUN', 're-verify did not run — treated as unresolved'); deferredOpen = true; deferredClasses.add('unresolved') }
718
+ else if (reGating.length) { g('fidelity-structural-reverify', 'STILL-BLOCKED', `${reGating.length} gating finding(s) survive the fix — deferred`); deferredOpen = true; deferredClasses.add('unresolved'); residuals.push({ card: cardId, kind: 'blocker', domain: 'ui', evidence: `structural fidelity still failing after fix: ${reGating.map((f) => f.category).join(', ')}`, materialized: false }) }
719
+ else g('fidelity-structural-reverify', 'PASS', 'fix confirmed by the verifier')
687
720
  }
688
721
  }
689
722
  }
@@ -1152,7 +1185,7 @@ if (!committed.length) {
1152
1185
  } else {
1153
1186
  try {
1154
1187
  mergeResult = await agentSafe(
1155
- `Auto-merge the batch worktree to ${TRUNK} per ${REF}/merge-cleanup.md: Phase 6 via the DETERMINISTIC script merge-worktree.sh, then Phase 6b status reconciliation + Phase 6c hygiene. Run git yourself.\n\n${projectBrief}\nWorktree: ${sharedCtx.worktreePath}\nBranch: ${sharedCtx.branch}\nmerge_strategy: ${mergeStrategy}\nCommitted cards: ${committed.map((r) => r.card).join(' ')}\nPhase-0 stash to restore (if any): see "$(git -C ${MAIN} rev-parse --git-common-dir)/baldart/run/batch-tracker-${firstCard}.md".\n\n` +
1188
+ `Auto-merge the batch worktree to ${TRUNK} per ${REF}/merge-cleanup.md: Phase 6 via the DETERMINISTIC script merge-worktree.sh, then Phase 6b status reconciliation + Phase 6c hygiene. Run git yourself.\n\n${projectBrief}\nWorktree: ${sharedCtx.worktreePath}\nBranch: ${sharedCtx.branch}\nmerge_strategy: ${mergeStrategy}\nCommitted cards: ${committed.map((r) => r.card).join(' ')}\nPhase-0 stash to restore (if any): see "$(git -C "${MAIN}" rev-parse --git-common-dir)/baldart/run/batch-tracker-${firstCard}.md".\n\n` +
1156
1189
  `PHASE 6 — run the SSOT script (the merge analogue of setup-worktree.sh; NOT a hand-rolled merge, NOT /mw inline):\n` +
1157
1190
  ` MERGE_SH="$(ls .framework/framework/.claude/skills/worktree-manager/scripts/merge-worktree.sh .claude/skills/worktree-manager/scripts/merge-worktree.sh 2>/dev/null | head -1)"\n` +
1158
1191
  ` bash "$MERGE_SH" --worktree ${sharedCtx.worktreePath} --manifest /tmp/new2-merge-status-${firstCard}.txt --skip-checks\n` +
@@ -111,9 +111,14 @@ function signature(texts) {
111
111
  sig.grid_cols_responsive_max = Math.max(sig.grid_cols_responsive_max, countTracks(m[1]));
112
112
  }
113
113
  }
114
- // Tailwind: grid-cols-N (+ responsive variants), incl. arbitrary grid-cols-[a_b]
114
+ // Tailwind: grid-cols-N (+ responsive variants), incl. arbitrary grid-cols-[a_b].
115
+ // Arbitrary values use `_` as the space stand-in and may contain functions —
116
+ // `grid-cols-[repeat(2,_minmax(0,1fr))]` is 2 tracks, not split('_').length=3:
117
+ // normalize `_`→space and reuse the parens-aware countTracks().
115
118
  for (const m of t.matchAll(/(?:^|[\s"'`])(?:(sm|md|lg|xl|2xl):)?grid-cols-(\d+|\[[^\]]+\])/g)) {
116
- const cols = m[2].startsWith('[') ? m[2].split('_').length : parseInt(m[2], 10);
119
+ const cols = m[2].startsWith('[')
120
+ ? countTracks(m[2].slice(1, -1).replace(/_/g, ' '))
121
+ : parseInt(m[2], 10);
117
122
  sig.grid_cols_max = Math.max(sig.grid_cols_max, cols);
118
123
  if (m[1]) {
119
124
  sig.has_responsive_prefixes = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.92.0",
3
+ "version": "4.92.1",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"