baldart 4.73.0 → 4.74.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ 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.74.0] - 2026-06-25
9
+
10
+ **Update in autonomy: a standing `git.on_divergence` policy so `npx baldart update` stops asking every release.** A consumer that keeps local commits on `.framework/` which the updater cannot auto-classify as overlays — the canonical case being a project-specific edit to a `framework/agents/*.md` **protocol module** (e.g. a design-system review rule naming the project's own files), which unlike `.claude/{agents,skills,commands}/` has **no overlay channel** — was stopped on EVERY update with "Custom commits touch non-overlayable paths … cannot be auto-resolved", forcing a manual `--on-divergence pull` each time. The non-interactive escape hatch existed only as a per-run CLI flag; there was no way to persist the choice. Now `git.on_divergence` in `baldart.config.yml` records a standing policy the interactive `update` honours, so a consumer opts in once and never passes the flag again. The `--on-divergence` CLI flag still wins for a single run, and the default (unset/null) preserves the current "stop once and ask" behaviour exactly.
11
+
12
+ **MINOR** — adds capability via a new config key. The **schema-change propagation rule applies** and is satisfied end-to-end: template (`git.on_divergence`) + configure prompt + update detector (already diffs all `git.*` keys, so the new key auto-surfaces on existing configs) + the gating code in `update` + CHANGELOG. Backward compatible: an existing config without the key resolves to null → no policy → unchanged behaviour; a config typo fails loud up-front (validated against the same `DIVERGENCE_STRATEGIES` set as the flag).
13
+
14
+ ### Added
15
+
16
+ - **`git.on_divergence` standing divergence policy** (`framework/templates/baldart.config.template.yml` § `git:`). Values: `null`/unset (DEFAULT — stop once and ask), `pull` (keep the local commits and merge upstream over them; non-destructive, the autonomy choice), `scaffold-overlays` (auto-scaffold the overlay-able subset, then stop). Read by `update` via a new `readDivergencePolicy()` helper; surfaced interactively (`Applying persisted divergence policy …`) so an auto-merge is never a surprise. The `configure` git section now prompts for it (`src/commands/configure.js`), writing the key explicitly (as null when unset) so the schema-drift detector does not re-offer it every release.
17
+
18
+ ### Changed
19
+
20
+ - **`update` resolves the divergence strategy as flag-or-config** (`src/commands/update.js`). `--on-divergence` (explicit per-run override) wins; absent a flag, the resolver falls back to `git.on_divergence`. Up-front validation now reports the correct source (`--on-divergence` vs ``git.on_divergence`` in baldart.config.yml) on a typo. The stale-CLI self-relaunch needs no change — the newer child re-reads the same config and resolves the policy itself.
21
+
22
+ ### Fixed
23
+
24
+ - **`links.design` now resolves to a path that EXISTS — making the v4.73.0 image-load fire in the mockup-only case.** v4.73.0 made the `/new` implementer `Read` the mockup image when `links.design` is a `.png`, but the card field was still hardcoded to `design.html` (`framework/.claude/agents/prd-card-writer.md`, `framework/.claude/skills/prd/assets/card-template.yml`). In **mockup-only** runs — exactly the "I hand `/prd` Claude Design / Figma mockups" flow — no `design.html` is generated (`ui-design-phase.md` sets `design.html_path: n/a (mockup-only)`), so the card pointed at a **non-existent** file: the implementer got a dead path AND the image branch never fired (extension was `.html`). `prd-card-writer` now resolves `links.design` to an on-disk path — design.html when generated, ELSE the screen's canonical mockup PNG from `mockup_analysis.screens[].mockup_ref` / `prerender_png` under `mockups/` (chat-only `chat://image-N` images are never persisted, so such a card is flagged visually-unreferenced rather than pointed at a path that won't resolve). The `/new` briefing (`framework/.claude/skills/new/references/implement.md` § "Design Reference") also falls back to the sibling `mockups/` PNG when a legacy card's `.html` path is absent. No behaviour change for generated-design runs.
25
+
8
26
  ## [4.73.0] - 2026-06-25
9
27
 
10
28
  **Mockup→code fidelity: let the implementer actually SEE the mockup, and carry the intent a flat list loses.** A recurring complaint: a detailed mockup (Claude Design / Figma) is handed to `/prd`, then `/new` ships something that is not 1:1 with the design. The obvious diagnosis — "the PRD's visual mapping is too weak, add a mockup-understanding agent" — was **refuted** by an adversarial pass: the visual data largely already exists (`mockup_analysis` captures affordances + traceability + states + DS violations; `component_bindings` propagates region→component to the implementer), and a parallel machine-readable `design-spec` artifact would be a second SSOT that drifts from the mockup and would feed the verifier a textual proxy instead of the pixels (a bias `visual-fidelity-verifier` explicitly forbids). The **actual driver**: the `/new` UI briefing passes the mockup as a *path* and says "Read the design.html file" — but for a binary PNG/Figma mockup nothing ever instructs the (multimodal) `ui-expert` to **load the image into its context**. The implementer was building from text, never looking at the design.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.73.0
1
+ 4.74.0
@@ -468,7 +468,19 @@ Every card MUST include ALL fields from the template:
468
468
  - `review_profile` (`skip`|`light`|`balanced`|`deep` — computed via Rule C, or honored if pre-set)
469
469
  - `files_likely_touched` (exact paths with NEW/MODIFY)
470
470
  - `links.prd` (path to PRD)
471
- - `links.design` (path to design.html — REQUIRED for any card with UI scope)
471
+ - `links.design` (REQUIRED for any card with UI scope) — the card's screen design
472
+ reference, resolved to a path **that exists on disk in the worktree**:
473
+ - **design.html when one was generated** for this screen (Step 3 produced it).
474
+ - **ELSE the screen's canonical mockup PNG** under `${paths.prd_dir}/<slug>/mockups/`
475
+ — read it from the screen's `mockup_analysis.screens[].mockup_ref` (or
476
+ `prerender_png`). A **mockup-only** run (user-provided Claude Design / Figma
477
+ mockups, no design generated) sets `design.html_path: n/a (mockup-only)` — in
478
+ that case design.html does NOT exist, so pointing at it gives the `/new`
479
+ implementer a dead path and no visual reference. Use the PNG.
480
+ - A **chat-only image** (`mockup_ref: chat://image-N`, never persisted to disk —
481
+ see `discovery-phase.md` 1.6.4 step 3) cannot be referenced: it is unreachable
482
+ from `/new`. Flag the card as visually-unreferenced rather than writing a path
483
+ that won't resolve in the worktree.
472
484
  - `canonical_docs` — REQUIRED for all cards generated from a PRD:
473
485
  - `ssot_registry_entry`: scan `${paths.references_dir}/ssot-registry.md` and copy the exact row name (column 1, bold) matching this card's macro feature. If the feature is new and has no entry yet, use the PRD slug.
474
486
  - `data_model_refs`: list of `${paths.references_dir}/collections/*.md` paths for collections touched by this card — REQUIRED whenever the card has `data_fields`; derive from the collection names in `data_fields`.
@@ -605,7 +617,9 @@ Rules:
605
617
 
606
618
  Rules:
607
619
  - Every UI element MUST map to at least one card
608
- - Every card with UI scope MUST include `links.design`
620
+ - Every card with UI scope MUST include `links.design`, resolved to an on-disk
621
+ path per the `links.design` rule above (design.html when generated, else the
622
+ screen's canonical mockup PNG — never a non-existent design.html)
609
623
  - Every UI requirement MUST reference the specific element number
610
624
 
611
625
  ## Parallel Group Computation (MANDATORY)
@@ -123,7 +123,11 @@
123
123
  actually load and look at the image. (On a non-multimodal tool the `Read`
124
124
  degrades to no-op text; fall back to the structured fields below.)
125
125
  - If `links.design` is an `.html` file: **Read the design.html file** and use
126
- it as the visual reference.
126
+ it as the visual reference. **If that file does not exist** (a legacy
127
+ mockup-only card that points at a `design.html` which was never generated),
128
+ fall back: look under the PRD's `mockups/` dir (sibling of the `links.design`
129
+ path) for the screen's PNG/JPG and `Read` that image instead — do not proceed
130
+ with no visual reference.
127
131
 
128
132
  Use it as the **visual reference** for your implementation (layout, spacing,
129
133
  hierarchy, copy). The design was approved by the user — your implementation
@@ -130,7 +130,13 @@ files_likely_touched:
130
130
 
131
131
  links:
132
132
  prd: "${paths.prd_dir}/{{slug}}/PRD.md"
133
- design: "${paths.prd_dir}/{{slug}}/design.html" # REQUIRED for UI cards, omit for API-only cards
133
+ # REQUIRED for UI cards (omit for API-only). Point at the card's screen design
134
+ # reference THAT EXISTS ON DISK: the generated design.html when one was produced,
135
+ # ELSE the screen's canonical mockup PNG under mockups/ (mockup-only runs have NO
136
+ # design.html). Never point at a design.html that was not generated — the /new
137
+ # implementer loads this path; a chat-only image (chat://image-N, never persisted)
138
+ # cannot be referenced.
139
+ design: "${paths.prd_dir}/{{slug}}/design.html" # OR ${paths.prd_dir}/{{slug}}/mockups/<screen>.png
134
140
 
135
141
  # --- Canonical Docs (REQUIRED for cards generated from a PRD) ---
136
142
  # Traces which SSOT documents informed this card's requirements.
@@ -443,6 +443,25 @@ git:
443
443
  # the production-readiness gate, e.g. ["supabase-db-push", "vercel-prod"].
444
444
  auto_deploy: []
445
445
 
446
+ # Standing divergence policy (since v4.74.0). When THIS consumer has local
447
+ # commits on `.framework/` that `npx baldart update` cannot auto-classify as
448
+ # overlays — e.g. a project-specific edit to a `framework/agents/*.md` protocol
449
+ # module, which (unlike `.claude/{agents,skills,commands}/`) has NO overlay
450
+ # channel — the interactive update STOPS once and asks. Set a standing policy
451
+ # here to let `update` self-resolve without passing `--on-divergence` every
452
+ # release (the "update in autonomy" knob):
453
+ #
454
+ # null / unset — DEFAULT. Stop once and ask (current behaviour).
455
+ # pull — keep the local commits and merge upstream over them.
456
+ # Non-destructive: a merge commit is created; resolve any
457
+ # conflicts that surface. This is the autonomy choice.
458
+ # scaffold-overlays — auto-scaffold overlay skeletons for the overlay-able
459
+ # subset, then stop (only helps `.claude/{agents,skills,
460
+ # commands}/` edits — useless for protocol-module drift).
461
+ #
462
+ # The `--on-divergence` CLI flag always overrides this key for a single run.
463
+ on_divergence: null
464
+
446
465
  # ─── TOOLS ───────────────────────────────────────────────────────────────
447
466
  # Which AI CLI tools should the framework target on this machine?
448
467
  # Each enabled tool gets its own per-item skill symlinks pointing at the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.73.0",
3
+ "version": "4.74.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"
@@ -694,6 +694,23 @@ async function interactivePrompts(merged, detected) {
694
694
  ? autoDeployAnswer.split(',').map((s) => s.trim()).filter(Boolean)
695
695
  : [];
696
696
 
697
+ // ---- Standing divergence policy (since v4.74.0) -----------------------
698
+ // Lets `npx baldart update` self-resolve `.framework/` divergence without a
699
+ // manual `--on-divergence` flag every release. Default (blank) = stop & ask.
700
+ const currentDiv = (typeof merged.git.on_divergence === 'string' && merged.git.on_divergence.trim())
701
+ ? merged.git.on_divergence.trim() : '';
702
+ const chosenDiv = await UI.select(
703
+ `Standing divergence policy for \`npx baldart update\` (current: ${currentDiv || 'ask each time'})`,
704
+ [
705
+ { name: 'ask each time — stop & ask when .framework/ has unclassifiable local commits (default)', value: '' },
706
+ { name: 'pull — auto-merge upstream over the local commits (non-destructive; the autonomy choice)', value: 'pull' },
707
+ { name: 'scaffold-overlays — auto-scaffold overlays for the overlay-able subset, then stop', value: 'scaffold-overlays' },
708
+ ]
709
+ );
710
+ // Write the key even when unset (as null) so the schema-drift detector does
711
+ // not re-offer it on every future update.
712
+ merged.git.on_divergence = chosenDiv || null;
713
+
697
714
  UI.section('Features (explicit yes/no — option A: always ask)');
698
715
  for (const flag of [
699
716
  ['has_design_system', 'Project has a documented design system?'],
@@ -29,6 +29,27 @@ function readEnabledTools(cwd = process.cwd()) {
29
29
  // agent-overlay feedback that prompted this).
30
30
  const DIVERGENCE_STRATEGIES = ['abort', 'pull', 'scaffold-overlays'];
31
31
 
32
+ // Persisted divergence policy (since v4.74.0). When a consumer has local commits
33
+ // on `.framework/` that the interactive flow would otherwise STOP on (e.g. a
34
+ // project-specific edit to a `framework/agents/*.md` protocol module, which has
35
+ // no overlay channel), `git.on_divergence` in baldart.config.yml lets `update`
36
+ // self-resolve without a manual `--on-divergence` flag every release. The CLI
37
+ // flag still wins (explicit per-run override). Returns the raw string (validated
38
+ // by the caller, so a config typo fails loud) or null when no policy is set.
39
+ function readDivergencePolicy(cwd = process.cwd()) {
40
+ try {
41
+ const fs = require('fs');
42
+ const path = require('path');
43
+ const yaml = require('js-yaml');
44
+ const cfgPath = path.join(cwd, 'baldart.config.yml');
45
+ if (!fs.existsSync(cfgPath)) return null;
46
+ const cfg = yaml.load(fs.readFileSync(cfgPath, 'utf8'));
47
+ const v = cfg && cfg.git && cfg.git.on_divergence;
48
+ if (typeof v === 'string' && v.trim()) return v.trim();
49
+ } catch (_) { /* fall through — no policy */ }
50
+ return null;
51
+ }
52
+
32
53
  // Machine-readable result channel (v3.32.0+). When --json is active, STDOUT
33
54
  // must carry exactly ONE JSON object and nothing else — UI.setJsonMode() has
34
55
  // already redirected every human line to STDERR. Every terminal point in the
@@ -749,15 +770,25 @@ async function update(options = {}, unknownArgs = []) {
749
770
  }
750
771
  }
751
772
 
752
- // Non-interactive divergence strategy (since v3.29.0). Validated up-front so
753
- // a typo fails fast instead of mid-flow after the network fetch.
754
- const divStrategy = options.onDivergence;
773
+ // Divergence strategy resolution (flag since v3.29.0; persisted policy since
774
+ // v4.74.0). The `--on-divergence` CLI flag is the explicit per-run override;
775
+ // absent a flag, fall back to the standing `git.on_divergence` policy in
776
+ // baldart.config.yml so a consumer that opted in never has to pass the flag.
777
+ // Validated up-front so a typo (in EITHER source) fails fast instead of
778
+ // mid-flow after the network fetch.
779
+ const divFlag = options.onDivergence;
780
+ const divFromConfig = divFlag === undefined ? readDivergencePolicy() : null;
781
+ const divStrategy = divFlag !== undefined ? divFlag : (divFromConfig || undefined);
782
+ const divStrategySource = divFlag !== undefined ? 'flag' : (divFromConfig ? 'config' : null);
755
783
  if (divStrategy !== undefined && !DIVERGENCE_STRATEGIES.includes(divStrategy)) {
784
+ const where = divStrategySource === 'config'
785
+ ? '`git.on_divergence` in baldart.config.yml'
786
+ : '--on-divergence';
756
787
  if (JSON_MODE) {
757
788
  emitUpdateJson({ ok: false, action: 'usage-error',
758
- reason: `Invalid --on-divergence "${divStrategy}". Use one of: ${DIVERGENCE_STRATEGIES.join(', ')}.` }, 1);
789
+ reason: `Invalid ${where} "${divStrategy}". Use one of: ${DIVERGENCE_STRATEGIES.join(', ')}.` }, 1);
759
790
  }
760
- UI.error(`Invalid --on-divergence "${divStrategy}". Use one of: ${DIVERGENCE_STRATEGIES.join(', ')}.`);
791
+ UI.error(`Invalid ${where} "${divStrategy}". Use one of: ${DIVERGENCE_STRATEGIES.join(', ')}.`);
761
792
  process.exit(1);
762
793
  }
763
794
 
@@ -875,6 +906,11 @@ async function update(options = {}, unknownArgs = []) {
875
906
  // a framework file → suggest /overlay. real-custom = prompt 3-way.
876
907
  if (status.hasDivergence) {
877
908
  UI.newline();
909
+ // Surface the persisted policy so an interactive user isn't surprised that
910
+ // `update` auto-resolved (e.g. auto-merged) instead of stopping to ask.
911
+ if (divStrategySource === 'config') {
912
+ UI.info(`Applying persisted divergence policy git.on_divergence: "${divStrategy}" (from baldart.config.yml). Pass --on-divergence to override for one run.`);
913
+ }
878
914
  if (status.divergenceClass === 'all-noise') {
879
915
  UI.info(`Detected ${status.divergenceCommits.length} subtree-merge / chore commit(s) (git plumbing noise — no real content drift). Auto-resolving…`);
880
916
  } else if (status.divergenceClass === 'overlay-able') {