baldart 4.29.1 → 4.30.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,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.30.1] - 2026-06-11
9
+
10
+ **`new2`: stop spending opus on mechanical ops steps — explicit per-step model overrides.** Three `general-purpose` agents had no `model:` override, so they inherited the session's main-loop model (opus) for work that needs none. The Merge step is a deterministic OPS/GIT executor (git merge + YAML status reconciliation + grep-based epic closure + leave-and-report hygiene gates) whose correctness-critical checks (F-029 forcedDone guard, F-040 deferred guard) are enforced in JS AFTER it returns — independent of the agent's reasoning → **sonnet**. The per-card Codex review agent is a pure DRIVER (runs the companion, strips `[codex]` traces, maps findings) — the review intelligence is Codex, run externally → **haiku**. The post-merge Production Readiness checklist is non-blocking report-not-execute → **sonnet**. The Pre-flight agent (DAG + ownership map + idempotency — it grounds the whole batch) intentionally **stays opus**. **PATCH** (cost optimization on the EXPERIMENTAL `new2` surface; no behavior change — the deterministic guards/policies are unchanged; no config key).
11
+
12
+ ### Changed
13
+
14
+ - **`framework/.claude/workflows/new2.js`** — `merge` → `model: 'sonnet'`; `review:<card>:codex` driver → `model: 'haiku'`; `production-readiness` → `model: 'sonnet'`. `preflight` unchanged (opus).
15
+
16
+ ## [4.30.0] - 2026-06-11
17
+
18
+ **`new2`: Cross-Card Integration Pass — implement residuals deferred by a per-card ownership artifact in-batch, instead of leaving them as follow-ups to manage.** A residual deferred `out-of-ownership` is often a *process* artifact: during the per-card pipeline each card may only touch its own MAY-EDIT, so a fix that needs another card's files is deferred. But `new2` is strictly sequential — once every card is committed that boundary no longer binds. A new pass, run BEFORE the final review, re-runs `resolve()` for those residuals with ownership widened to `card MAY-EDIT ∪ remedy files` (domain-routed — never a raw coder), but ONLY when the remedy lands inside the batch union (`within()`); a remedy outside the batch is a future card's work and stays a follow-up. Transient `outage` residuals are retried with the card's own ownership. Everything else (`owner-gated` / `not-a-code-defect` / `baseline-not-reached` / new-AC `scope-expansion`) still defers — a coder physically cannot do those, or it would be silent scope creep. Integrated fixes are committed (one `[integrate]` commit) so the final review covers them, and a fully-freed card is marked DONE. **MINOR** (additive capability on the EXPERIMENTAL `new2` surface; scope per the user decision = out-of-ownership-within-batch + outage only; no `baldart.config.yml` key, so the schema-change propagation rule does not apply; no change to `/new`).
19
+
20
+ ### Added
21
+
22
+ - **`framework/.claude/workflows/new2.js`** — `integrateCrossCard()` (new `Integrate` phase between `Implement` and `Final`): collects `out-of-ownership`/`outage` residuals, batch-union `within()` gate on remedy files, surgical ownership widening, domain-routed `resolve()` re-run, one integration commit + YAML DONE-marking for fully-freed cards, ledger cleanup. New telemetry `cross_card_integrated`; the residual ledger now carries `domain` + `remedyFiles`, and `cardMayEdit` records per-card MAY-EDIT for the union.
23
+ - **`framework/.claude/workflows/new2-resolve.js`** — `noFollowup` arg: when the integration pass re-runs an already-tracked residual, a failed retry returns the verdict WITHOUT minting a duplicate follow-up card (the original residual stays tracked). `materialiseFollowup()` now propagates `remedyFiles` so the caller can decide whether a remedy is in-batch.
24
+ - **`framework/.claude/skills/new2/SKILL.md`** — the A/B record step keeps `cross_card_integrated` alongside `deferral_breakdown` (how many deferrals were genuinely undeferrable vs absorbed in-batch).
25
+
8
26
  ## [4.29.1] - 2026-06-11
9
27
 
10
28
  **`new2`: `deferral_breakdown` telemetry — see WHY residuals became follow-ups, per class.** Follow-up cards in `new2` are not "useless deferred work": an in-scope fixable anomaly is fixed directly by `resolve()`'s domain fixer in-batch; a follow-up is created ONLY for a residual that is structurally undeferrable (`out-of-ownership` would edit another card's files · `owner-gated`/`not-a-code-defect`/`baseline-not-reached` aren't code a coder can apply · `unresolved` already failed fixer+judge+tier-2 · `scope-expansion` with a new AC needs a PRD decision · `outage`). To diagnose a run with *many* follow-ups, the telemetry now counts residuals per class so a skewed breakdown points at the real root cause (many `out-of-ownership` → PRD MAY-EDIT too narrow · many `unresolved` → fixes genuinely hard · many `scope-expansion` → cards under-specified upstream) — data first, before any change to the deferral logic. **PATCH** (observability only on the EXPERIMENTAL `new2` surface; diagnosis-only — nothing is auto-implemented from a residual; no behavior change, no config key).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.29.1
1
+ 4.30.1
@@ -230,5 +230,8 @@ returns when the batch is done. It returns:
230
230
  of WHY residuals became follow-ups instead of in-batch fixes) in the record — a class dominating
231
231
  it is a root-cause signal (many `out-of-ownership` → PRD MAY-EDIT too narrow · many `unresolved` →
232
232
  fixes genuinely hard · many `scope-expansion` → cards under-specified upstream), and it is the
233
- data to consult BEFORE proposing any change to the deferral logic. Do NOT re-summarise the cards —
234
- the workflow already did.
233
+ data to consult BEFORE proposing any change to the deferral logic. Keep `cross_card_integrated`
234
+ too (count of residuals the pre-final-review Cross-Card Integration Pass implemented in-batch —
235
+ out-of-ownership-within-batch + outage retries — instead of leaving as follow-ups); with
236
+ `deferral_breakdown` it shows how many deferrals were genuinely undeferrable vs absorbed in-batch.
237
+ Do NOT re-summarise the cards — the workflow already did.
@@ -47,6 +47,11 @@ const cfg = a.config || {}
47
47
  const paths = cfg.paths || {}
48
48
  const backlogDir = paths.backlog_dir || 'backlog'
49
49
  const protectedDomain = domain === 'security' || domain === 'migration'
50
+ // v4.30.0 — when the new2 Cross-Card Integration Pass RE-runs resolve on an already-tracked
51
+ // residual (with widened ownership), a failure must NOT mint a SECOND follow-up card: the
52
+ // original residual is still tracked by the caller. noFollowup makes materialiseFollowup return
53
+ // the verdict without spawning prd-card-writer (no duplicate card, no wasted spawn).
54
+ const noFollowup = a.noFollowup === true
50
55
 
51
56
  // F-019 — bounded immediate retry on transient errors (shared pattern; no import allowed).
52
57
  const TRANSIENT = /overload|529|429|rate.?limit|5\d\d|econn|etimedout|timeout|temporar|unavailable/i
@@ -221,7 +226,7 @@ if (attempt && attempt.terminal) {
221
226
  confirmed = !!(tj && tj.confirmed)
222
227
  } catch (_) { confirmed = false }
223
228
  }
224
- if (confirmed) { log(`${kind} terminal (${tr}) — short-circuit to follow-up.`); return await materialiseFollowup(kind, `terminal: ${tr} — ${attempt.note || ''}`, collectOOS(attempt), tr || 'unresolved') }
229
+ if (confirmed) { log(`${kind} terminal (${tr}) — short-circuit to follow-up.`); return await materialiseFollowup(kind, `terminal: ${tr} — ${attempt.note || ''}`, collectOOS(attempt), tr || 'unresolved', attempt.remedyFiles) }
225
230
  log(`terminal verdict (${tr}) rejected — proceeding to multi-attempt.`)
226
231
  }
227
232
 
@@ -304,8 +309,15 @@ async function judgeVerify(verifiedAttempts) {
304
309
  // own code is complete; the residual is an external step (do NOT roll the card back). Anything
305
310
  // else (unresolved code defect, out-of-ownership, baseline) → genuine block → rollback as before.
306
311
  // The classifier flows back through resolve() in new2.js; never write it into the worktree.
307
- async function materialiseFollowup(k, reason, oos, deferralClass) {
312
+ async function materialiseFollowup(k, reason, oos, deferralClass, remedyFiles) {
308
313
  const cls = deferralClass || 'unresolved'
314
+ const rf = Array.isArray(remedyFiles) ? remedyFiles : []
315
+ // noFollowup (integration re-run): return the verdict WITHOUT minting a duplicate card.
316
+ // remedyFiles rides the verdict so the caller can decide whether the remedy is in-batch.
317
+ if (noFollowup) {
318
+ log(`${k} still unresolved at integration — caller keeps the original residual (no duplicate card).`)
319
+ return { status: 'followup', followupCard: null, reason, deferralClass: cls, outOfScopeFindings: oos || [], remedyFiles: rf }
320
+ }
309
321
  let r = null
310
322
  try {
311
323
  // F-039 — backlog cards are owned by prd-card-writer (card-template + Rule C
@@ -322,9 +334,9 @@ async function materialiseFollowup(k, reason, oos, deferralClass) {
322
334
  // F-020 — could not materialise (e.g. outage): return WITHOUT a followupCard so the
323
335
  // SKILL writes it from the offline-safe residual ledger. Never claim it was created.
324
336
  log(`follow-up materialisation failed (${String(e && e.message)}) — skill will reconcile.`)
325
- return { status: 'followup', followupCard: null, reason, deferralClass: cls, outOfScopeFindings: oos || [] }
337
+ return { status: 'followup', followupCard: null, reason, deferralClass: cls, outOfScopeFindings: oos || [], remedyFiles: rf }
326
338
  }
327
339
  const followupCard = (r && r.created && r.followupCard) ? r.followupCard : null
328
340
  log(`${k} → follow-up ${followupCard || '(deferred to skill)'} (nothing dropped).`)
329
- return { status: 'followup', followupCard, reason, deferralClass: cls, outOfScopeFindings: oos || [] }
341
+ return { status: 'followup', followupCard, reason, deferralClass: cls, outOfScopeFindings: oos || [], remedyFiles: rf }
330
342
  }
@@ -1,10 +1,11 @@
1
1
  export const meta = {
2
2
  name: 'new2',
3
3
  description:
4
- "EXPERIMENTAL workflow host for /new (A/B testing). Runs an ENTIRE backlog-card batch autonomously in the background runtime — pre-flight, a dependency-gated (DAG) per-card implement+review pipeline with specialized agents, cross-batch final review, and an integrity-gated auto-merge — so subagent output never enters the main orchestrator context. Zero AskUserQuestion: each /new gate is a deterministic policy; blocking gates and scope-expanding findings are routed to the new2-resolve self-healing workflow. Resilient by design: transient API errors are retried, a sustained outage degrades cleanly (durable resume), follow-ups for residuals are reconciled by the skill (offline-safe), and the merge is blocked unless the batch is verifiably complete (no false-DONE, no unreviewed code). Agents Read /new's reference modules for semantics (args.refModulesBase) so this script encodes only orchestration shape + gate policy. Claude-only.",
4
+ "EXPERIMENTAL workflow host for /new (A/B testing). Runs an ENTIRE backlog-card batch autonomously in the background runtime — pre-flight, a dependency-gated (DAG) per-card implement+review pipeline with specialized agents, cross-batch final review, and an integrity-gated auto-merge — so subagent output never enters the main orchestrator context. Zero AskUserQuestion: each /new gate is a deterministic policy; blocking gates and scope-expanding findings are routed to the new2-resolve self-healing workflow. Before the final review a cross-card integration pass implements the residuals that were deferred only by the per-card ownership artifact (out-of-ownership whose remedy lands inside the batch union) or by a transient outage — re-running resolve with batch-union ownership, domain-routed — so they are not left as follow-ups to manage later (owner-gated / not-a-code-defect / baseline / new-AC scope-expansion still defer). Resilient by design: transient API errors are retried, a sustained outage degrades cleanly (durable resume), follow-ups for residuals are reconciled by the skill (offline-safe), and the merge is blocked unless the batch is verifiably complete (no false-DONE, no unreviewed code). Agents Read /new's reference modules for semantics (args.refModulesBase) so this script encodes only orchestration shape + gate policy. Claude-only.",
5
5
  phases: [
6
6
  { title: 'Pre-flight', detail: 'deterministic workspace hygiene + worktree + dep-graph + cross-card grounding (Phase 0)' },
7
7
  { title: 'Implement', detail: 'dependency-gated per-card implement+review pipeline with owner_agent + specialized review fan-out' },
8
+ { title: 'Integrate', detail: 'cross-card integration pass: implement residuals deferred by a per-card ownership artifact / outage (batch-union ownership)' },
8
9
  { title: 'Final', detail: 'cross-batch final review (delegates to new-final-review)' },
9
10
  { title: 'Merge', detail: 'integrity-gated auto-merge to trunk via git.merge_strategy + cleanup' },
10
11
  { title: 'Production', detail: 'post-merge production-readiness checklist (Phase 7, non-blocking)' },
@@ -65,9 +66,11 @@ const mergeStrategy = gitCfg.merge_strategy || 'pr'
65
66
  const firstCard = cardIds[0] || 'BATCH'
66
67
  const gateLedger = [] // { card, gate, decision, detail }
67
68
  const residualFollowups = [] // { card, kind, followupCard, reason }
68
- const residuals = [] // F-020 OFFLINE-SAFE ledger: { card, kind, evidence, materialized }
69
+ const residuals = [] // F-020 OFFLINE-SAFE ledger: { card, kind, evidence, materialized, deferralClass, domain, remedyFiles }
70
+ const cardMayEdit = {} // v4.30.0 — per-card MAY-EDIT, for the cross-card integration union
69
71
  const perCardResults = []
70
72
  let prodReadiness = null
73
+ let integratedCount = 0 // v4.30.0 — residuals resolved by the Cross-Card Integration Pass
71
74
  let degraded = false
72
75
  const degradationReasons = []
73
76
 
@@ -379,7 +382,9 @@ async function resolve(kind, card, evidence, extra) {
379
382
  const fc = (res && res.followupCard) || null
380
383
  residualFollowups.push({ card, kind, followupCard: fc || '(pending)', reason: (res && res.reason) || '' })
381
384
  // A3 — deferralClass rides the residual so the skill can gate DONE-reconciliation on it.
382
- residuals.push({ card, kind, evidence, materialized: !!fc, deferralClass })
385
+ // v4.30.0 domain + remedyFiles ride too, so the Cross-Card Integration Pass can re-route by
386
+ // domain and decide whether an out-of-ownership remedy lands inside the batch union.
387
+ residuals.push({ card, kind, evidence, materialized: !!fc, deferralClass, domain: dom, remedyFiles: (res && res.remedyFiles) || [] })
383
388
  }
384
389
  // F-022 — route out-of-scope findings the resolve surfaced.
385
390
  for (const osf of (res && res.outOfScopeFindings) || []) {
@@ -523,6 +528,7 @@ async function runCard(cardId, cardPath) {
523
528
  if (impl && impl.epic) { g('router', 'EPIC-SKIPPED', 'epic card'); return { card: cardId, status: 'epic-skipped', gates, commit: '-' } }
524
529
 
525
530
  const mayEdit = (impl && impl.mayEditPaths) || []
531
+ cardMayEdit[cardId] = mayEdit // v4.30.0 — feeds the cross-card integration union
526
532
  const scopeFiles = (impl && impl.scopeFiles) || []
527
533
  // F-040 — E4 honest label: 'AUTO-REVERTED' used to be a no-op log (files were left orphaned).
528
534
  // Now the owner agent reconciles out-of-ownership edits itself (implement.md §11b); we report
@@ -620,7 +626,11 @@ async function runCard(cardId, cardPath) {
620
626
  `Read the Codex output ONLY through a [codex]-trace-stripping filter. **Fallback**: if it exits non-zero / prints CODEX_NOT_FOUND / stays empty, return note:'codex-unavailable' with EMPTY blocks/scopeExpansion — do NOT review yourself (role boundary); the workflow spawns the real code-reviewer.\n\n` +
621
627
  `Map Codex BLOCKER/HIGH findings to blocks:[{gate:'codex-light',domain,evidence}] (each non-empty gate AND evidence — F-014). Map legitimate findings BEYOND this card's AC to scopeExpansion:[{evidence,domain,withinOwnership,newAC}].\n\n` +
622
628
  `Return: { blocks:[...], scopeExpansion:[...], note }`,
623
- { label: `review:${cardId}:codex`, phase: 'Implement', agentType: 'general-purpose', schema: reviewSchema }
629
+ // Haiku, not the inherited opus: this agent is a pure DRIVER — it runs the Codex companion
630
+ // (node + --wait), strips [codex] traces, and maps the companion's findings into the schema.
631
+ // The review INTELLIGENCE is Codex (a different model, run externally); driving it needs no
632
+ // opus reasoning, same as the other mechanical git/ops steps.
633
+ { label: `review:${cardId}:codex`, phase: 'Implement', agentType: 'general-purpose', model: 'haiku', schema: reviewSchema }
624
634
  ).catch(onErr)
625
635
  }
626
636
  return stdReview(ra)
@@ -802,6 +812,96 @@ async function crashResult(id, e) {
802
812
  return { card: id, status: 'failed', gates: [{ gate: 'runCard', decision: 'ERROR', detail: String(e && e.message) }], commit: '-' }
803
813
  }
804
814
 
815
+ // ───────────────────────────────────────────────────────────────────────────
816
+ // Cross-Card Integration Pass (v4.30.0) — runs BEFORE the final review.
817
+ // A residual deferred ONLY by the per-card ownership artifact (`out-of-ownership`, with its
818
+ // remedy inside the batch union) or by a transient `outage` CAN now be implemented: new2 is
819
+ // strictly sequential, so once every card is committed the per-card MAY-EDIT boundary no longer
820
+ // binds. Re-run resolve() with ownership widened to the batch union (domain-routed — never a raw
821
+ // coder), commit the result so the final review covers it, and mark a fully-freed card DONE.
822
+ // Everything else (owner-gated / not-a-code-defect / baseline / scope-expansion / out-of-batch
823
+ // out-of-ownership) stays a tracked follow-up: a coder physically cannot do those, or doing so
824
+ // would be silent scope creep. Failure keeps the ORIGINAL residual (noFollowup → no duplicate card).
825
+ // ───────────────────────────────────────────────────────────────────────────
826
+ async function integrateCrossCard() {
827
+ if (degraded || !sharedCtx || !sharedCtx.worktreePath) return
828
+ const targets = residuals.filter((r) => !r.materialized && !r.integrated &&
829
+ (r.deferralClass === 'out-of-ownership' || r.deferralClass === 'outage'))
830
+ if (!targets.length) return
831
+ const batchUnion = dedupe(Object.values(cardMayEdit).flat())
832
+ if (!batchUnion.length) return
833
+ phase('Integrate')
834
+ const within = (files) => Array.isArray(files) && files.length &&
835
+ files.every((f) => batchUnion.some((m) => String(f).includes(m) || m.includes(String(f))))
836
+ const freedCards = new Set()
837
+ for (const r of targets) {
838
+ // out-of-ownership is implementable here ONLY if the remedy belongs to a card IN THIS batch;
839
+ // a remedy outside the union is a future card's work → it stays a tracked follow-up.
840
+ if (r.deferralClass === 'out-of-ownership' && !within(r.remedyFiles)) {
841
+ g('integrate', 'STAYS-FOLLOWUP', `${r.card}: remedy outside batch union (future card) — ${r.evidence}`)
842
+ continue
843
+ }
844
+ const dom = r.domain || 'code'
845
+ // Minimal widening (NOT the whole batch union): outage was not an ownership problem → retry with
846
+ // the card's own ownership; out-of-ownership → the card's files PLUS exactly the remedy files
847
+ // (already proven in-batch by within()). Tighter ownership = less room for incidental edits.
848
+ const ownership = r.deferralClass === 'outage'
849
+ ? (cardMayEdit[r.card] || batchUnion)
850
+ : dedupe((cardMayEdit[r.card] || []).concat(r.remedyFiles || []))
851
+ let res = null
852
+ try {
853
+ res = await workflow('new2-resolve', {
854
+ kind: r.kind, cardId: r.card, evidence: r.evidence,
855
+ findings: [{ kind: r.kind, evidence: r.evidence, domain: dom }],
856
+ worktreePath: sharedCtx.worktreePath,
857
+ mayEditPaths: domainMayEdit(dom, ownership),
858
+ scopeFiles: ownership, domain: dom,
859
+ refModulesBase: REF, config: cfg, ts: TS,
860
+ noFollowup: true, // a failed retry keeps the ORIGINAL residual — no duplicate card
861
+ })
862
+ } catch (e) { if (e && (e.transientExhausted || isTransient(e))) noteDegraded('outage'); res = null }
863
+ if (res && res.status === 'resolved') {
864
+ r.integrated = true
865
+ freedCards.add(r.card)
866
+ g('integrate', 'INTEGRATED', `${r.card} (${r.deferralClass}/${dom}): ${r.evidence}`)
867
+ } else {
868
+ g('integrate', 'STILL-DEFERRED', `${r.card} (${r.deferralClass}): ${(res && res.reason) || 'unresolved with widened ownership'}`)
869
+ }
870
+ }
871
+ const integrated = targets.filter((r) => r.integrated)
872
+ if (!integrated.length) return
873
+ // Drop integrated residuals from the offline-safe ledger (the work landed — nothing to follow up).
874
+ for (let i = residuals.length - 1; i >= 0; i--) if (residuals[i].integrated) residuals.splice(i, 1)
875
+ // A card is FULLY freed (→ DONE) only when no residual remains for it AND its sole deferral
876
+ // reasons were the integration classes; a card still owner-gated/policy-deferred stays NON-DONE.
877
+ const fullyFreed = []
878
+ for (const cardId of freedCards) {
879
+ const pc = perCardResults.find((x) => x.card === cardId)
880
+ if (!pc) continue
881
+ const stillResidual = residuals.some((x) => x.card === cardId)
882
+ const remaining = (pc.deferredClasses || []).filter((c) => c !== 'out-of-ownership' && c !== 'outage')
883
+ pc.deferredClasses = remaining
884
+ if (!stillResidual && !remaining.length) { pc.deferred = false; fullyFreed.push(cardId) }
885
+ for (let i = residualFollowups.length - 1; i >= 0; i--) {
886
+ if (residualFollowups[i].card === cardId && !stillResidual) residualFollowups.splice(i, 1)
887
+ }
888
+ }
889
+ // ONE integration commit (the fixes cross card boundaries); mark fully-freed cards DONE in YAML.
890
+ try {
891
+ await agentSafe(
892
+ `Commit the cross-card integration fixes in worktree ${sharedCtx.worktreePath}. MECHANICAL — do NOT modify source beyond what is already changed on disk. Steps:\n` +
893
+ `(1) \`git status --porcelain\`; (2) stage ONLY dirty files within the batch union ${JSON.stringify(batchUnion)} — NEVER \`git add -A\`, NEVER \`git stash\`; (3) for each fully-freed card YAML [${fullyFreed.join(', ') || '(none)'}] set status to DONE and add implementation_note "cross-card integration: deferred AC resolved with batch-union ownership (new2 v4.30.0)"; (4) commit message \`[integrate] cross-card residual fixes${fullyFreed.length ? ' (' + fullyFreed.join(', ') + ')' : ''}\`; (5) 'nothing to commit' = the resolve already committed (record HEAD).\n` +
894
+ `Return: { committed, commit, filesChanged }`,
895
+ { label: 'integrate:commit', phase: 'Integrate', agentType: 'general-purpose', model: 'haiku',
896
+ schema: { type: 'object', required: ['committed'], additionalProperties: true, properties: { committed: { type: 'boolean' }, commit: { type: 'string' }, filesChanged: { type: 'array', items: { type: 'string' } } } } }
897
+ )
898
+ } catch (e) { if (e && e.transientExhausted) noteDegraded('outage') }
899
+ integratedCount = integrated.length
900
+ ledger(fullyFreed[0] || firstCard, 'cross-card-integration', 'DONE', `${integrated.length} residual(s) integrated; freed→DONE: ${fullyFreed.join(', ') || 'none (code landed, cards still deferred for other reasons)'}`)
901
+ }
902
+
903
+ await integrateCrossCard()
904
+
805
905
  const committed = perCardResults.filter((r) => r.status === 'committed')
806
906
 
807
907
  // ───────────────────────────────────────────────────────────────────────────
@@ -920,7 +1020,12 @@ if (!committed.length) {
920
1020
  `• EPIC CLOSURE (Phase 6b step 5e): the epic/parent card (group.is_epic:true) is NOT in the batch and stays TODO unless closed here. For each distinct group.parent of the batch cards (and any epic card in the batch itself): if EVERY child of that epic — \`grep -l "parent: <EPIC-ID>" ${paths.backlog_dir || 'backlog'}/*.yml | xargs grep -L "status: DONE"\` prints nothing — set the epic card status:DONE + completed_date + note "epic-closure gate — all children DONE" and fold into the reconciliation commit. If any child is still open → leave the epic untouched. This is NOT a forcedDone violation (the epic is a tracker, gated on all-children-DONE, not on its own commit). Return epicsClosed:[<EPIC-IDs marked DONE>].\n` +
921
1021
  `• G19 sync-deferred → HEAD==${TRUNK} ff-pull, else leave+report. G20 → leave+report. G21 post-batch dirty → partition-ignore framework artifacts; leave the rest + report (do NOT commit). G22 divergence → behind: ff-pull; ahead/both: leave+report; NEVER reset --hard/force-push. G23 stash restore conflict → leave intact + report.\n\n` +
922
1022
  `Return: { merged, mergeCommit, mergeTs, reconciliation, forcedDone:[], deferredLeftOpen:[], uncommittedLeft, note }`,
923
- { label: 'merge', phase: 'Merge', agentType: 'general-purpose', schema: MERGE_SCHEMA }
1023
+ // Sonnet, not the inherited opus: this is a deterministic OPS/GIT executor (git merge +
1024
+ // YAML status reconciliation + grep-based epic closure + leave-and-report hygiene gates), and
1025
+ // the correctness-critical checks (F-029 forcedDone guard, F-040 deferred guard) are enforced
1026
+ // in JS AFTER it returns — they do NOT depend on the agent's reasoning. Opus here is pure cost;
1027
+ // haiku would be too weak for the conditional epic-closure / G19-23 divergence logic.
1028
+ { label: 'merge', phase: 'Merge', agentType: 'general-purpose', model: 'sonnet', schema: MERGE_SCHEMA }
924
1029
  )
925
1030
  } catch (e) { if (e && e.transientExhausted) noteDegraded('outage'); mergeResult = null }
926
1031
  if (mergeResult && (mergeResult.forcedDone || []).length) { noteDegraded('false_done'); ledger(firstCard, 'F029-guard', 'VIOLATION', `forcedDone: ${mergeResult.forcedDone.join(' ')}`) }
@@ -945,7 +1050,10 @@ if (mergeResult && mergeResult.merged) {
945
1050
  try {
946
1051
  prodReadiness = await agentSafe(
947
1052
  `Run the post-merge Production Readiness checklist per ${REF}/production-readiness.md (Phase 7) over the batch's changed files. Auto-EXECUTE only stack-matched index/access-rule/cron deploys; REPORT (do not execute) env vars, feature flags, DB migrations, secrets, DNS. NON-BLOCKING. ROLE BOUNDARY: you EXECUTE commands, you never edit repository files — a needed code/config change is reported as a manual item.\n\n${projectBrief}\nChanged files: ${dedupe(committed.flatMap((r) => r.filesChanged || [])).join(', ') || '(derive from git)'}\n\nReturn: { autoExecuted:[...], manualItems:[...], note }`,
948
- { label: 'production-readiness', phase: 'Production', agentType: 'general-purpose',
1053
+ // Sonnet, not the inherited opus: a non-blocking report-not-execute checklist (auto-run only
1054
+ // stack-matched deploys, REPORT the rest). Sonnet follows the checklist reliably at a fraction
1055
+ // of the cost; nothing here gates the merge (it already happened).
1056
+ { label: 'production-readiness', phase: 'Production', agentType: 'general-purpose', model: 'sonnet',
949
1057
  schema: { type: 'object', required: ['manualItems'], additionalProperties: true, properties: { autoExecuted: { type: 'array', items: { type: 'string' } }, manualItems: { type: 'array', items: { type: 'string' } }, note: { type: 'string' } } } }
950
1058
  )
951
1059
  ledger(firstCard, 'phase7-production', 'DONE', `auto=${((prodReadiness && prodReadiness.autoExecuted) || []).length} manual=${((prodReadiness && prodReadiness.manualItems) || []).length}`)
@@ -994,6 +1102,9 @@ function buildTelemetry() {
994
1102
  // ownership), many `unresolved` → fixes genuinely hard, many `scope-expansion` → cards
995
1103
  // under-specified upstream. This is diagnosis-only; nothing is auto-implemented from a residual.
996
1104
  deferral_breakdown: residuals.reduce((b, x) => { const k = x.deferralClass || x.kind || 'unknown'; b[k] = (b[k] || 0) + 1; return b }, {}),
1105
+ // v4.30.0 — residuals the Cross-Card Integration Pass implemented in-batch (out-of-ownership
1106
+ // within the batch union + outage retries) instead of leaving as follow-ups to manage later.
1107
+ cross_card_integrated: integratedCount,
997
1108
  // followups_on_disk is filled by the SKILL after it materialises pending residuals.
998
1109
  followups_materialized_in_workflow: residuals.filter((x) => x.materialized).length,
999
1110
  resolve_invocations: resolvedSignatures.size,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.29.1",
3
+ "version": "4.30.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"