baldart 3.17.0 → 3.18.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.
@@ -811,93 +811,107 @@ After completeness is verified, clean up the implementation before it reaches re
811
811
  If any valid finding revealed a reusable pattern or common mistake, append 1-line to `## Lessons Learned`:
812
812
  `SIMPLIFY: <pattern> — <file>`
813
813
 
814
- ### Phase 2.6 — E2E Testing (PRD-driven, card-level)
815
-
816
- This phase uses the `test_plan` field from the backlog card YAML to determine whether
817
- to write and run E2E tests. The test plan was defined during `/prd` discovery.
818
-
819
- **Trigger detection** read the card's `test_plan` field:
820
-
821
- | `test_plan.e2e_required` | Action |
822
- |--------------------------|--------|
823
- | `true` | **AUTO-TRIGGER** proceed directly, no user confirmation needed |
824
- | `false` | **SKIP** log "E2E testing: SKIP (PRD evaluation: not needed)" in tracker |
825
- | field missing | **FALLBACK to legacy detection** (see below) |
826
-
827
- **Legacy fallback** (only when `test_plan` field is absent old cards without PRD test plan):
828
- Check if ALL conditions are met:
829
- 1. The card's `areas` field includes any of: `ui`, `frontend`, `booking`, `customer`, `merchant-dashboard`, `onboarding`, `settings`.
830
- 2. The changed files include at least one `.tsx` page file (under `src/app/`).
831
- 3. The card type is NOT `docs`, `chore`, or `config`.
832
- If all met, ask the user: `Vuoi testarlo con Playwright E2E? (si/no)`
833
-
834
- **When E2E runs** (auto-triggered or user-confirmed):
835
-
836
- 1. **Update tracker**: phase = "2.6-e2e-testing".
837
- 2. Read test scenarios and credentials from `test_plan`:
838
- - `test_scenarios` — pre-planned scenarios with mapped user stories/ACs
839
- - `test_credentials` — persona, auth method, credentials, store
840
- - `test_data_prerequisites` data that must exist
841
- 3. Spawn a **coder** agent to write the Playwright E2E test:
842
- ```
843
- Write a Playwright E2E test for card <CARD-ID>.
844
-
845
- Test file: tests/e2e/<card-id-slug>.spec.ts
846
- Use: import { test, expect } from '@playwright/test'
847
-
848
- ## Pre-planned Test Scenarios (from PRD)
849
- [paste test_scenarios from card YAML — each has description, mapped US/AC, priority]
850
-
851
- ## Test Credentials
852
- Persona: [from test_plan.test_credentials.persona]
853
- Auth method: [from test_plan.test_credentials.auth_method]
854
- Credentials: [from test_plan.test_credentials.credentials]
855
- Store: [from test_plan.test_credentials.store]
856
- Notes: [from test_plan.test_credentials.notes]
857
-
858
- ## Auth Handling
859
- - If persona is CUSTOMER with OTP auth: navigate to login, enter phone number,
860
- then use `await test.step('OTP input (manual)', async () => { await page.pause() })`
861
- to pause for manual OTP entry by the user. After pause resumes, continue the flow.
862
- - If persona is MERCHANT with password auth: use page.fill() for username/password
863
- fields, then submit the login form. No manual intervention needed.
864
-
865
- ## Test Data Prerequisites
866
- [paste test_data_prerequisites — data that must exist in the persistence layer before the test runs]
867
-
868
- ## Guidelines
869
- - Use page.getByRole(), page.getByText(), page.getByTestId() for selectors
870
- - Use expect(locator) assertions with auto-waiting
871
- - Group related steps with test.describe()
872
- - Implement ALL scenarios from the PRD test plan (not just happy path)
873
- - Use baseURL from config (just page.goto('/relative-path'))
874
- - Do NOT use chromium.launch() or manual browser management
875
- - For OTP flows: use page.pause() and document it clearly in test output
814
+ ### Phase 2.6 — End-to-End Review (BLOCKING, since v3.18.0)
815
+
816
+ This phase delegates to the `/e2e-review` skill a deterministic, BLOCKING
817
+ orchestrator that combines functional E2E (Playwright spec written by `coder`,
818
+ executed via `playwright-skill`) with visual fidelity diff
819
+ (`visual-fidelity-verifier` multimodal agent), aggregates findings under a
820
+ strict severity gate, runs a bounded self-heal loop, and either passes /
821
+ blocks / accepts an explicit user override.
822
+
823
+ The skill replaces the previous advisory pair (legacy Phase 2.6 E2E testing
824
+ and Phase 2.7 visual design review). It runs by default on every UI card,
825
+ auto-skips on backend-only cards, and BLOCKS the commit on gating findings.
826
+
827
+ #### Gate (skip the whole phase when any of the below is true)
828
+
829
+ | Condition | Action |
830
+ |-----------|--------|
831
+ | `features.has_e2e_review: false` in `baldart.config.yml` | **SKIP** — log `"e2e-review: SKIP (feature disabled — run `npx baldart configure` to enable)"`. Preserves backwards compatibility for pre-3.18 consumers. |
832
+ | Card has no `links.design` AND `git diff --name-only` shows no file under `${paths.app_dir}` AND no file under `${paths.components_primitives}` AND no `*.tsx` / `*.css` / `*.scss` / `*.svelte` / `*.vue` anywhere | **SKIP** — log `"e2e-review: SKIP (backend-only card)"`. |
833
+ | Card type is `docs`, `chore`, or `config` | **SKIP** — same log. |
834
+
835
+ #### Invocation contract
836
+
837
+ When the gate above passes, the orchestrator invokes `/e2e-review` in
838
+ **programmatic mode**:
839
+
840
+ 1. **Update tracker**: phase = "2.6-e2e-review".
841
+ 2. Spawn the skill with the payload:
842
+
843
+ ```json
844
+ {
845
+ "card_id": "<CARD-ID>",
846
+ "worktree_path": "<absolute worktree path>",
847
+ "mode": "programmatic",
848
+ "tolerance": "<features.e2e_review.fidelity_tolerance>",
849
+ "max_self_heal_iterations": "<features.e2e_review.max_self_heal_iterations>"
850
+ }
876
851
  ```
877
- 4. After the test file is written, run the test in headed mode:
878
- ```bash
879
- npx playwright test tests/e2e/<test-file>.spec.ts --headed --reporter=list
852
+
853
+ The skill is responsible for: reading the card YAML + `test_plan`, deriving
854
+ routes from the diff, walking the 4-level mockup cascade (Figma MCP →
855
+ local image → compliance-only → skip), generating the Playwright spec via
856
+ `coder`, executing it headless, invoking `visual-fidelity-verifier` per
857
+ route, and returning a single JSON object.
858
+
859
+ 3. Parse the returned JSON. The contract is:
860
+
861
+ ```json
862
+ {
863
+ "status": "passed" | "blocked" | "overridden" | "skipped" | "error",
864
+ "iterations": 0,
865
+ "findings": [ ... ],
866
+ "ds_drift_codes": [ "DS_TOKENS_DRIFT" | "DS_COMPONENT_STALE" | "DS_INDEX_DRIFT" ],
867
+ "gaps": [ ... ],
868
+ "transcript_paths": [ ... ]
869
+ }
880
870
  ```
881
- Note: for OTP-based tests, the test will pause at `page.pause()` for manual OTP input.
882
- The user enters the OTP in the browser, then resumes (close Playwright Inspector).
883
- 5. Report the result to the user. If tests fail, the coder agent fixes them (up to 2 retries).
884
- 6. **Update tracker**: phase = "2.6-e2e-testing DONE", log: test file path, result (PASS/FAIL/SKIP), test count, scenarios covered (N/M from PRD plan).
885
-
886
- ### Phase 2.7 Visual Design Review (UI cards only advisory)
887
-
888
- **Trigger**: card has `links.design` field. If absent, SKIP.
889
-
890
- Non-blocking findings are logged but do NOT block commit.
891
-
892
- 1. **Update tracker**: phase = "2.7-design-review".
893
- 2. Extract the route from `files_likely_touched` (look for `page.tsx` under `src/app/`).
894
- If no page file found, SKIP (log "no page route").
895
- 3. Invoke the **design-review** agent (`.claude/commands/design-review.md`) on the route,
896
- passing the worktree dev server port.
897
- 4. Log findings in tracker: `design-review: [N] blockers, [M] high, [K] medium`.
898
- If blockers: log in `## Issues & Flags` as `[DESIGN-ADVISORY]`.
899
- 5. **Do NOT spawn fix agents.** Findings are informational for the user.
900
- 6. **Update tracker**: phase = "2.7-design-review DONE".
871
+
872
+ #### Gating logic (BLOCKING)
873
+
874
+ | `status` from `/e2e-review` | Orchestrator action |
875
+ |----|----|
876
+ | `"passed"` | Log result in tracker. Proceed to Phase 3 (doc review + code review). |
877
+ | `"skipped"` | Log skip reason. Proceed to Phase 3. |
878
+ | `"overridden"` | Log override + reason in `## Issues & Flags` as `[E2E-OVERRIDE] <reason>`. Proceed to Phase 3. |
879
+ | `"blocked"` | **STOP the card**. Log findings in tracker + `## Issues & Flags` as `[E2E-BLOCKED] <count> gating findings (<categories>)`. Ask the user whether to (a) override with reason, (b) escalate (open follow-up card), or (c) abandon this card and continue the batch. Do NOT proceed to Phase 3 silently. |
880
+ | `"error"` | Log error in `## Issues & Flags`. Ask the user whether to retry, skip, or abandon. Do NOT proceed silently. |
881
+
882
+ #### Re-run trigger (after Phase 3.7 `/codexreview`)
883
+
884
+ If the mandatory `/codexreview` gate in Phase 3.7 modifies any file under
885
+ `${paths.app_dir}`, `${paths.components_primitives}`, or any `*.css` /
886
+ `*.scss`, the orchestrator MUST re-invoke `/e2e-review` BEFORE the Phase 4
887
+ commit. This protects against code-review changes that silently reintroduce
888
+ visual / functional drift. The re-run reuses the same
889
+ `.baldart/e2e-review/<CARD-ID>/` state directory and starts from Phase 3
890
+ of the skill (skipping plan extraction and mockup cascade).
891
+
892
+ #### Tracker output
893
+
894
+ Append to `## Current Card` and (on completion) carry into `## Completed Cards`:
895
+
896
+ ```
897
+ e2e-review: <status> | iterations: <N> | findings: <X critical, Y major, Z minor> | drift: [<codes>] | tolerance: <strict|balanced|lenient>
898
+ ```
899
+
900
+ #### Out of scope for this phase
901
+
902
+ - Mockup re-design (the skill never modifies the mockup — only the
903
+ implementation).
904
+ - Code review or doc review (those remain Phase 3 / 3.7 responsibilities).
905
+ - Unit-test execution (that is `qa-sentinel`).
906
+ - Visual polish beyond gating findings (advisory Minor findings under
907
+ balanced/lenient tolerance are logged but not actioned).
908
+
909
+ #### Legacy cards
910
+
911
+ Cards written before v3.18.0 (without a `test_plan` block) still work — the
912
+ skill's Phase 1 falls back to deriving Gherkin scenarios from
913
+ `acceptance_criteria` directly. The mockup cascade gracefully degrades when
914
+ `links.design` is missing.
901
915
 
902
916
  ---
903
917
 
@@ -17,6 +17,35 @@ All browser testing uses the **Playwright Test CLI** via `playwright.config.ts`
17
17
  **Overlay:** loads `.baldart/overlays/playwright-skill.md` if present — project-specific test URLs, auth helpers, theming pairing assertions.
18
18
  **On missing/empty keys:** ask the user; do not assume defaults. See `framework/agents/project-context.md` § 3.
19
19
 
20
+ ## MCP dependencies
21
+
22
+ See [framework/docs/MCP-INTEGRATION.md](../../../docs/MCP-INTEGRATION.md) for the BALDART MCP integration contract and the rationale behind this section.
23
+
24
+ **None.** This skill uses the **Playwright Test CLI** (`@playwright/test`) directly via `npm run test:e2e` — `.spec.ts` files under `${paths.e2e_tests_dir}`. It does NOT use Playwright MCP. Consumers don't need any MCP configured to use this skill. For ad-hoc interactive browser debugging during bug investigation (not E2E test authoring) see [bug/SKILL.md](../bug/SKILL.md), which DOES require `mcp__playwright__browser_*`.
25
+
26
+ ## E2E Review Integration (since v3.18.0)
27
+
28
+ This skill is the tooling layer consumed by `/e2e-review` Phase 3 (functional
29
+ E2E). When the orchestrator invokes this skill, the contract is:
30
+
31
+ - The `coder` agent generates the `.spec.ts` under `${paths.e2e_tests_dir}/`
32
+ from a Gherkin verification plan persisted at
33
+ `.baldart/e2e-review/<CARD-ID>/plan.json` (do not invent scenarios outside
34
+ the plan).
35
+ - The spec MUST screenshot every route in scope and save the file at
36
+ `.baldart/e2e-review/<CARD-ID>/screenshots/<route-slug>.png` — that path is
37
+ consumed verbatim by the `visual-fidelity-verifier` agent in Phase 4.
38
+ - Execution is **headless** in programmatic mode (invocation from `/new`);
39
+ headed mode is reserved for OTP flows during manual `/e2e-review` runs.
40
+ - Failed scenarios become functional findings in the aggregate report; the
41
+ severity is determined by the orchestrator (auth/payments/data-mutation =
42
+ Critical; display-only = Major; flaky-passed-on-retry = Minor).
43
+
44
+ No logic of this skill changes for E2E-review consumption — the consumer
45
+ simply requires the conventions above. See
46
+ [`framework/.claude/skills/e2e-review/SKILL.md`](../e2e-review/SKILL.md) Phase 3
47
+ for the full contract.
48
+
20
49
  ## Quick Reference
21
50
 
22
51
  | Action | Command |
@@ -131,6 +131,13 @@ canonical_docs:
131
131
  additional_refs:
132
132
  - "{{${paths.adrs_dir}/ADR-XXXXX.md}}"
133
133
 
134
+ # test_plan is consumed BOTH by `/new` Phase 2.6 (legacy fallback path) AND by
135
+ # `/e2e-review` (since v3.18.0). The /e2e-review skill maps each test_scenarios[]
136
+ # entry into a Gherkin scenario in Phase 1 (Verification Plan Extraction).
137
+ # When the card has `links.design`, the same scenarios additionally drive the
138
+ # per-route visual fidelity check in Phase 4. Keep scenario.description short
139
+ # and action-oriented (one user-facing intent per scenario) so the Gherkin
140
+ # translation reads cleanly. See framework/.claude/skills/e2e-review/SKILL.md.
134
141
  test_plan:
135
142
  e2e_required: {{true|false}}
136
143
  e2e_rationale: "{{reason from evaluation tree}}"
@@ -15,6 +15,29 @@ license: Complete terms in LICENSE.txt
15
15
 
16
16
  ---
17
17
 
18
+ ## E2E Review Integration (since v3.18.0)
19
+
20
+ This skill is the tooling layer consumed by `/e2e-review` in **compliance-only
21
+ mode** (Phase 2 cascade level c) and as a fallback for runtime inspection.
22
+ When invoked from the orchestrator:
23
+
24
+ - Use the Python Playwright helpers to fetch the rendered DOM's computed
25
+ styles (CSS color / spacing / radius / shadow values) for the routes in
26
+ scope when no mockup image is available.
27
+ - The compliance check reads `${paths.design_system}/tokens-reference.md` and
28
+ flags any computed value that does not match a token (the
29
+ `visual-fidelity-verifier` agent owns the actual cross-reference; this
30
+ skill only surfaces the runtime values).
31
+ - The server lifecycle helper (`scripts/with_server.py`) MAY be used to start
32
+ / stop the dev server when the orchestrator runs outside a worktree that
33
+ already has one running.
34
+
35
+ No logic of this skill changes for E2E-review consumption — see
36
+ [`framework/.claude/skills/e2e-review/SKILL.md`](../e2e-review/SKILL.md)
37
+ Phase 2 (compliance-only branch) for the full contract.
38
+
39
+ ---
40
+
18
41
  To test local web applications, write native Python Playwright scripts.
19
42
 
20
43
  **Helper Scripts Available**:
@@ -16,6 +16,7 @@ This file is generic. Paths referenced as `${paths.X}` resolve from `baldart.con
16
16
  | PRD workflow, PRD sessions | `features.has_prd_workflow: true` |
17
17
  | LLM wiki overlay sync | `features.has_wiki_overlay: true` |
18
18
  | Design-system blocking reads | `features.has_design_system: true` |
19
+ | E2E review BLOCKING gate (Phase 2.6 of `/new` invokes `/e2e-review`) | `features.has_e2e_review: true` |
19
20
 
20
21
  When a MUST rule's feature is `false` or its target file does not exist, the rule is **silent** — do not invent a workflow that isn't there. Project-specific terminology (audience segments, domain entities, brand voice) comes from `identity.audience_segments`, `agents/coding-standards.md`, and skill overlays under `.baldart/overlays/`. See `agents/project-context.md` for the full protocol.
21
22
 
@@ -107,7 +107,26 @@ Empty string `""` means the concept doesn't exist in your project. Skills gated
107
107
  | `prd_dir` | `docs/prd` | prd, prd-add, ui-design |
108
108
  | `references_dir` | `docs/references` | new, prd, context-primer, doc-writing-for-rag, simplify |
109
109
  | `wiki_dir` | `docs/wiki` | capture, context-primer |
110
- | `e2e_tests_dir` | `tests/e2e` | playwright-skill |
110
+ | `e2e_tests_dir` | `tests/e2e` | playwright-skill, e2e-review (v3.18.0) |
111
+
112
+ ### 4.2.1 `e2e_review` — tuning for the BLOCKING end-to-end review gate (v3.18.0+)
113
+
114
+ Only consulted when `features.has_e2e_review: true`. The block governs how the `/e2e-review` skill (auto-invoked by `/new` Phase 2.6 on UI cards) decides what blocks, how aggressively it retries, and what counts as an acceptable user override.
115
+
116
+ ```yaml
117
+ e2e_review:
118
+ fidelity_tolerance: strict # strict | balanced | lenient
119
+ max_self_heal_iterations: 2
120
+ pixel_diff_threshold: 0.02
121
+ require_override_reason: true
122
+ ```
123
+
124
+ | Key | Type | Default | Effect |
125
+ |---|---|---|---|
126
+ | `fidelity_tolerance` | enum (`strict`/`balanced`/`lenient`) | `strict` | `strict` blocks on Critical + Major + Minor findings (Minor with `confidence: low` auto-demote to advisory to absorb font-rendering / anti-aliasing variance); `balanced` blocks on Critical + Major, Minor advisory; `lenient` blocks on Critical only. |
127
+ | `max_self_heal_iterations` | int | `2` | After gating findings, the orchestrator re-spawns `coder` with the findings as fix instructions and re-runs functional + visual phases. Matches the existing `/new` Phase 2.5 retry policy. After exhaustion, the override path engages. |
128
+ | `pixel_diff_threshold` | float `0.0`–`1.0` | `0.02` | When the implementation screenshot vs mockup pixel-diff is below this fraction, the orchestrator skips the Vision call for that route and treats it as a pass. Primary latency / cost saver — most routes pass pixel-diff cleanly. Set higher to skip Vision more aggressively; set to `0.0` to always invoke Vision. |
129
+ | `require_override_reason` | bool | `true` | When self-heal exhausts iterations and the user chooses to override the gate, a written reason is mandatory. The reason is recorded both in the batch tracker's `## Issues & Flags` (as `[E2E-OVERRIDE] <reason>`) and in `.baldart/e2e-review/<CARD-ID>/report.json`. Set `false` to allow silent override (not recommended). |
111
130
 
112
131
  ### 4.3 `identity` — brand and audience facts
113
132
 
@@ -145,6 +145,44 @@ features:
145
145
  # `baldart configure` installs the relevant language servers when enabled.
146
146
  has_lsp_layer: false
147
147
 
148
+ # End-to-end review BLOCKING gate (since v3.18.0). When true, /new Phase 2.6
149
+ # invokes the /e2e-review skill, which combines functional E2E (Playwright
150
+ # spec written by `coder`, executed via playwright-skill) with visual
151
+ # fidelity diff (`visual-fidelity-verifier` multimodal agent). Findings are
152
+ # aggregated under a severity gate (strict by default) with a bounded
153
+ # self-heal loop. Replaces the legacy advisory Phase 2.6 + 2.7. Requires
154
+ # `@playwright/test` installed and `paths.e2e_tests_dir` set. Auto-skips on
155
+ # backend-only cards and on cards without `links.design` when no UI files
156
+ # changed. See framework/.claude/skills/e2e-review/SKILL.md.
157
+ has_e2e_review: false
158
+
159
+ # ─── E2E REVIEW ──────────────────────────────────────────────────────────
160
+ # Tuning for the /e2e-review skill (only consulted when has_e2e_review: true).
161
+ e2e_review:
162
+ # Severity floor that BLOCKS the commit.
163
+ # strict — Critical + Major + Minor block (default, recommended for
164
+ # design-system-strict projects); Minor findings with
165
+ # `confidence: low` auto-demote to advisory (anti font-rendering
166
+ # false positive).
167
+ # balanced — Critical + Major block; Minor advisory.
168
+ # lenient — Critical blocks; Major + Minor advisory.
169
+ fidelity_tolerance: strict
170
+
171
+ # Max self-heal iterations before falling through to the override path. The
172
+ # orchestrator re-spawns `coder` with the gating findings and re-runs the
173
+ # functional + visual phases. 2 matches the existing Phase 2.5 retry policy.
174
+ max_self_heal_iterations: 2
175
+
176
+ # Pre-filter pixel-diff threshold (0.0–1.0). When the implementation vs
177
+ # mockup pixel diff is below this value, the orchestrator skips the Vision
178
+ # call for that route and treats it as a pass. Primary latency / cost saver.
179
+ pixel_diff_threshold: 0.02
180
+
181
+ # When true, the override path (after self-heal exhaustion) requires the
182
+ # user to provide a written reason that is recorded in the tracker's
183
+ # `## Issues & Flags` and in `.baldart/e2e-review/<CARD-ID>/report.json`.
184
+ require_override_reason: true
185
+
148
186
  # ─── LSP ─────────────────────────────────────────────────────────────────
149
187
  # State of the LSP layer for this project. Populated by `baldart configure`
150
188
  # (and by `npx baldart lsp install` / the `/lsp-bootstrap` skill). Leave
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "3.17.0",
3
+ "version": "3.18.0",
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"
@@ -560,6 +560,7 @@ async function interactivePrompts(merged, detected) {
560
560
  ['has_prd_workflow', 'Project uses PRD workflow (docs/prd/)?'],
561
561
  ['has_wiki_overlay', 'Project has LLM-wiki overlay (docs/wiki/)?'],
562
562
  ['has_lsp_layer', 'Enable LSP symbol-search layer? (recommended for large codebases)'],
563
+ ['has_e2e_review', 'Enable BLOCKING end-to-end review (Phase 2.6 of /new invokes /e2e-review — functional + visual fidelity gate)?'],
563
564
  ]) {
564
565
  const [key, question] = flag;
565
566
  const currentVal = merged.features[key];
@@ -619,6 +620,62 @@ async function interactivePrompts(merged, detected) {
619
620
  merged.paths[key] = await promptForKey(`${label}`, proposed);
620
621
  }
621
622
 
623
+ // ---- E2E review tuning (since v3.18.0) --------------------------------
624
+ // Only runs when the user opted in above. Populates the e2e_review.*
625
+ // sub-section with the severity floor, self-heal cap, pixel pre-filter
626
+ // threshold, and override-reason requirement. Idempotent: re-running
627
+ // configure on an already-installed project re-asks each value with the
628
+ // current YAML value as default.
629
+ merged.e2e_review = merged.e2e_review || {};
630
+ if (merged.features.has_e2e_review === true) {
631
+ UI.section('E2E review tuning (/e2e-review skill — Phase 2.6 of /new)');
632
+ const tolDefault = merged.e2e_review.fidelity_tolerance || 'strict';
633
+ const tolerance = await UI.select(
634
+ `Severity floor that BLOCKS the commit (current: ${tolDefault})`,
635
+ [
636
+ { name: 'strict — Critical + Major + Minor block (recommended for design-system-strict projects)', value: 'strict' },
637
+ { name: 'balanced — Critical + Major block; Minor advisory', value: 'balanced' },
638
+ { name: 'lenient — Critical blocks; Major + Minor advisory', value: 'lenient' },
639
+ ]
640
+ );
641
+ merged.e2e_review.fidelity_tolerance = tolerance;
642
+
643
+ const maxIterDefault = merged.e2e_review.max_self_heal_iterations ?? 2;
644
+ const maxIter = await promptForKey(
645
+ 'Max self-heal iterations before override path',
646
+ String(maxIterDefault)
647
+ );
648
+ merged.e2e_review.max_self_heal_iterations = Number.isFinite(Number(maxIter)) ? Number(maxIter) : 2;
649
+
650
+ const diffDefault = merged.e2e_review.pixel_diff_threshold ?? 0.02;
651
+ const diffThr = await promptForKey(
652
+ 'Pixel-diff pre-filter threshold (0.0 = always invoke Vision, 1.0 = never)',
653
+ String(diffDefault)
654
+ );
655
+ merged.e2e_review.pixel_diff_threshold = Number.isFinite(Number(diffThr)) ? Number(diffThr) : 0.02;
656
+
657
+ const reqReasonDefault = merged.e2e_review.require_override_reason !== undefined
658
+ ? merged.e2e_review.require_override_reason
659
+ : true;
660
+ merged.e2e_review.require_override_reason = await promptForKey(
661
+ 'Require a written reason when the user overrides the gate after self-heal exhaustion?',
662
+ reqReasonDefault,
663
+ 'confirm'
664
+ );
665
+
666
+ if (!merged.paths.e2e_tests_dir) {
667
+ UI.info(
668
+ 'E2E review requires `paths.e2e_tests_dir` set and `@playwright/test` installed.\n' +
669
+ 'The path prompt above asked for the directory — confirm it exists or run `npm i -D @playwright/test` later.'
670
+ );
671
+ }
672
+ } else {
673
+ UI.info(
674
+ 'has_e2e_review=false — /new Phase 2.6 silently skips the BLOCKING gate.\n' +
675
+ 'Re-run `baldart configure` to enable when you want functional + visual fidelity enforced per card.'
676
+ );
677
+ }
678
+
622
679
  // ---- LSP layer (since v3.10.0) ----------------------------------------
623
680
  // Only runs when the user opted in above. Idempotent: re-running configure
624
681
  // on an already-installed project re-detects, asks which servers to keep,
@@ -836,6 +893,7 @@ async function configure(opts = {}) {
836
893
  `Backlog: ${detected.features.has_backlog ? 'yes' : 'no'}`,
837
894
  `ADRs: ${detected.features.has_adrs ? 'yes' : 'no'}`,
838
895
  `API docs: ${detected.features.has_api_docs ? 'yes' : 'no'}`,
896
+ `E2E review: ${detected.features.has_e2e_review === true ? 'yes' : detected.features.has_e2e_review === false ? 'no' : '— (will ask)'}`,
839
897
  `Charting libs: ${detected.stack.charting.canonical.join(', ') || '—'}`,
840
898
  `Animation libs: ${detected.stack.animation.canonical.join(', ') || '—'}`,
841
899
  `E2E framework: ${detected.stack.testing.e2e || '—'}`,
@@ -4,6 +4,19 @@ const UI = require('../utils/ui');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
+ // Lightweight semver compare. Returns positive if a > b, negative if a < b,
8
+ // zero if equal. Treats malformed input as 0 — degrade open rather than throw.
9
+ function cmpSemver(a, b) {
10
+ if (!a || !b) return 0;
11
+ const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
12
+ const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
13
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
14
+ const d = (pa[i] || 0) - (pb[i] || 0);
15
+ if (d !== 0) return d;
16
+ }
17
+ return 0;
18
+ }
19
+
7
20
  async function status() {
8
21
  const git = new GitUtils();
9
22
  const symlinks = new SymlinkUtils();
@@ -94,24 +107,43 @@ async function status() {
94
107
  }
95
108
  }
96
109
 
97
- // Check for pending updates
110
+ // Check for pending updates (since v3.17.1 — was a no-op stub before).
98
111
  UI.newline();
99
112
  UI.section('Update Status');
100
113
 
114
+ let updateAvailable = false;
115
+ let remoteVersion = null;
101
116
  try {
102
117
  await git.fetch('antbald/BALDART');
103
- UI.info('Checking for updates...');
104
- // Simplified - would need more sophisticated version comparison
105
- UI.success('Up to date (check with: baldart update)');
118
+ remoteVersion = await git.getRemoteVersion('antbald/BALDART', 'main');
119
+ if (!remoteVersion || remoteVersion === 'unknown') {
120
+ UI.warning('Could not read remote VERSION — check `baldart update` manually.');
121
+ } else if (cmpSemver(remoteVersion, version) > 0) {
122
+ updateAvailable = true;
123
+ UI.warning(`Update available: ${version} → ${remoteVersion}. Run: baldart update`);
124
+ } else if (cmpSemver(remoteVersion, version) < 0) {
125
+ UI.info(`Local v${version} is ahead of remote v${remoteVersion} (likely an unreleased commit).`);
126
+ } else {
127
+ UI.success(`Up to date (v${version})`);
128
+ }
106
129
  } catch (error) {
107
130
  UI.warning('Cannot check for updates (offline?)');
108
131
  }
109
132
 
110
133
  // Summary
111
134
  UI.newline();
135
+ let updateLine;
136
+ if (updateAvailable && remoteVersion) {
137
+ updateLine = `Update: ${version} → ${remoteVersion} available`;
138
+ } else if (remoteVersion && remoteVersion !== 'unknown') {
139
+ updateLine = `Update: up to date`;
140
+ } else {
141
+ updateLine = `Update: unknown (could not reach remote)`;
142
+ }
112
143
  UI.box('SUMMARY', [
113
144
  `Version: ${version}`,
114
145
  `Installation: ${symlinkValid ? 'Valid' : 'Issues detected'}`,
146
+ updateLine,
115
147
  '',
116
148
  'Commands available:',
117
149
  ' • baldart update - Update framework',
@@ -81,7 +81,10 @@ async function postUpdateAutoCommit(git, newVersion, options) {
81
81
  if (userOwned.length > 0) {
82
82
  UI.warning(`${userOwned.length} other dirty path(s) will be left untouched (not BALDART-managed).`);
83
83
  }
84
- const ok = await UI.confirm(`Commit them as \`chore(baldart): post-update reconcile to v${newVersion}\`?`, true);
84
+ const autoYes = options && options.yes === true;
85
+ const ok = autoYes
86
+ ? true
87
+ : await UI.confirm(`Commit them as \`chore(baldart): post-update reconcile to v${newVersion}\`?`, true);
85
88
  if (!ok) {
86
89
  UI.info('Auto-commit declined. Your worktree keeps the changes for manual review.');
87
90
  return;
@@ -110,6 +113,13 @@ async function update(options = {}) {
110
113
  const symlinks = new SymlinkUtils();
111
114
  const repo = 'antbald/BALDART'; // Default repo
112
115
 
116
+ // Non-interactive flags (since v3.17.1). `--yes` implies `--auto-stash`.
117
+ // When `yes` is set, every UI.confirm/UI.select downstream short-circuits
118
+ // to its default — the same value an unattended user would have produced.
119
+ const autoYes = options.yes === true;
120
+ const autoStash = autoYes || options.autoStash === true;
121
+ const confirm = async (msg, def = true) => (autoYes ? def : UI.confirm(msg, def));
122
+
113
123
  try {
114
124
  // Step 1: Verify installation
115
125
  UI.header('STEP 1/5: Verify Installation');
@@ -194,14 +204,14 @@ async function update(options = {}) {
194
204
  const diff = await git.diffWithRemote();
195
205
  if (diff) {
196
206
  UI.info('Changes detected. Review recommended.');
197
- const showDiff = await UI.confirm('Show detailed diff?', false);
207
+ const showDiff = await confirm('Show detailed diff?', false);
198
208
 
199
209
  if (showDiff) {
200
210
  console.log(diff);
201
211
  }
202
212
  }
203
213
 
204
- const proceedUpdate = await UI.confirm('Proceed with update?', true);
214
+ const proceedUpdate = await confirm('Proceed with update?', true);
205
215
  if (!proceedUpdate) {
206
216
  UI.info('Update cancelled');
207
217
  process.exit(0);
@@ -225,10 +235,16 @@ async function update(options = {}) {
225
235
  UI.list(unique.slice(0, 8), 'yellow');
226
236
  if (unique.length > 8) UI.info(`…and ${unique.length - 8} more.`);
227
237
 
228
- const action = await UI.select('How would you like to proceed?', [
229
- { name: 'Auto-stash now and re-apply after the update (recommended)', value: 'stash' },
230
- { name: 'Abort I want to commit or stash manually first', value: 'abort' }
231
- ]);
238
+ let action;
239
+ if (autoStash) {
240
+ UI.info('Auto-stashing dirty paths (--yes / --auto-stash).');
241
+ action = 'stash';
242
+ } else {
243
+ action = await UI.select('How would you like to proceed?', [
244
+ { name: 'Auto-stash now and re-apply after the update (recommended)', value: 'stash' },
245
+ { name: 'Abort — I want to commit or stash manually first', value: 'abort' }
246
+ ]);
247
+ }
232
248
  if (action === 'abort') {
233
249
  UI.info('Aborted. Commit or `git stash -u`, then re-run `npx baldart update`.');
234
250
  process.exit(0);
@@ -361,9 +377,12 @@ async function update(options = {}) {
361
377
  'Convert legacy .claude/skills/ bulk symlink (v2.0.x) into the new per-item layout.',
362
378
  `Merge framework skills into per-tool skill dirs (${enabledTools.join(', ')}) without touching your personal skills.`
363
379
  ]);
364
- const recreate = await UI.confirm('Reconcile symlinks now?', true);
380
+ const recreate = await confirm('Reconcile symlinks now?', true);
365
381
  if (recreate) {
366
- await symlinks.createAllSymlinks({ mode: 'prompt', tools: enabledTools });
382
+ // In auto-yes mode pass 'force' instead of 'prompt' so per-conflict
383
+ // prompts inside SymlinkUtils don't reintroduce blocking I/O.
384
+ const mode = autoYes ? 'force' : 'prompt';
385
+ await symlinks.createAllSymlinks({ mode, tools: enabledTools });
367
386
  }
368
387
  } else {
369
388
  // Re-run the per-item merges for every enabled tool so newly-shipped
@@ -403,10 +422,10 @@ async function update(options = {}) {
403
422
  if (!fs.existsSync(configPath)) {
404
423
  UI.newline();
405
424
  UI.warning('No baldart.config.yml in this project — skills installed by this framework version require it.');
406
- const runConfigure = await UI.confirm('Run `baldart configure` now?', true);
425
+ const runConfigure = await confirm('Run `baldart configure` now?', true);
407
426
  if (runConfigure) {
408
427
  const configureCmd = require('./configure');
409
- await configureCmd();
428
+ await configureCmd(autoYes ? { nonInteractive: true } : undefined);
410
429
  }
411
430
  } else {
412
431
  // Schema migration: backfill `tools.enabled` for configs created
@@ -425,7 +444,7 @@ async function update(options = {}) {
425
444
  UI.warning('Pre-v3.7.0 config detected — `tools.enabled` is missing.');
426
445
  UI.info(`From v3.7.0, BALDART can install framework skills for multiple AI tools (Claude Code, OpenAI Codex CLI, …) from the same source.`);
427
446
  UI.info(`Autodetected on this machine: ${suggested.join(', ')}`);
428
- const ok = await UI.confirm(`Backfill \`tools.enabled: [${suggested.join(', ')}]\` into baldart.config.yml?`, true);
447
+ const ok = await confirm(`Backfill \`tools.enabled: [${suggested.join(', ')}]\` into baldart.config.yml?`, true);
429
448
  if (ok) {
430
449
  cur.tools = cur.tools || {};
431
450
  cur.tools.enabled = suggested;
@@ -470,10 +489,10 @@ async function update(options = {}) {
470
489
  UI.warning(`New config keys in this version: ${allMissing.join(', ')}.`);
471
490
  // Auto-offer configure so the user actually answers, instead of
472
491
  // silently falling back to template defaults on first use.
473
- const runConfigure = await UI.confirm('Run `baldart configure` now to populate them? (existing values are preserved)', true);
492
+ const runConfigure = await confirm('Run `baldart configure` now to populate them? (existing values are preserved)', true);
474
493
  if (runConfigure) {
475
494
  const configureCmd = require('./configure');
476
- await configureCmd();
495
+ await configureCmd(autoYes ? { nonInteractive: true } : undefined);
477
496
  } else {
478
497
  UI.info('Skipped. Skills will prompt for the missing keys on first use.');
479
498
  }