baldart 4.32.0 → 4.33.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,31 @@ 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.33.0] - 2026-06-12
9
+
10
+ **New opt-in `stack.schema_deploy_from_trunk_only` safety invariant — `/new` + `new2` never push a remote schema/RLS/index/migration deploy from a non-trunk branch or worktree.** Before this release, the Production Readiness checklist (`/new` Phase 7 / `new2` Production phase) and the front-loaded Migration Gate would auto-execute a remote schema deploy (`supabase db push`, `prisma migrate deploy`, `firebase deploy --only firestore:rules|firestore:indexes|storage`, CDK/Terraform apply, any SQL migrator) gated only on `stack.database`/`stack.deployment` — with no guard against running it from an unmerged feature branch / git worktree. On projects that develop multiple cards in parallel worktrees against a single shared remote datastore, that desyncs migration history and produces "code-ahead-of-schema" outages (a real consumer hit a `42703 undefined_column` production outage this way).
11
+
12
+ The new key is **stack-agnostic and opt-in**, a no-op for projects that don't set it:
13
+ - `false` (default) / unset → current behavior preserved exactly (unset → the skill asks; `baldart configure` defaults it to `true` only when it autodetects parallel worktree usage).
14
+ - `true` → a remote schema/RLS/index/migration deploy is auto-executed **only** when `git rev-parse --abbrev-ref HEAD == git.trunk_branch`. From any non-trunk branch / worktree it is **never** auto-run: the migration stays committed and the remote deploy is deferred ("happens from trunk after merge") and routed to the existing **`db-migration-deploy`** residual class (dedup-collapsed onto one external action).
15
+
16
+ **The guard enforces only the branch invariant** — the actual deploy command, env loader, and gate UX stay delegated to the project overlay (`.baldart/overlays/new.md`); core does not move the command in. Datastore-agnostic by design (any persistence layer, not Supabase-specific). The principle is documented as a MUST-rule in `production-readiness.md` and the generic `deployment-protocol.md`. **MINOR** (new opt-in config key + capability; default false ⇒ zero behavior change for existing consumers). Schema-change propagation rule applied end-to-end (template + configure prompt/autodetect + update detector + skills + docs + CHANGELOG).
17
+
18
+ ### Added
19
+
20
+ - **`framework/templates/baldart.config.template.yml`** — new `stack.schema_deploy_from_trunk_only: false` key with documentation.
21
+ - **`src/commands/configure.js`** — `detectWorktreeUsage()` probe (`git worktree list`); the autodetected default and an interactive confirm prompt for the new key (pre-checked when parallel worktrees are detected).
22
+
23
+ ### Changed
24
+
25
+ - **`framework/.claude/skills/new/references/production-readiness.md`** — new "Schema-deploy-from-trunk-only guard" subsection + a MUST-rule in § Rules: when the flag is on, gate every remote schema/RLS/index/migration auto-deploy on `current branch == git.trunk_branch`, else defer as a `db-migration-deploy` residual.
26
+ - **`framework/.claude/skills/new/references/setup.md`** — Migration Gate (Phase 0 step 1b) honors the same guard: a command modality (front-loaded apply) runs only when `$MAIN` is on trunk; otherwise it degrades to the end-of-batch owner-gated deferral.
27
+ - **`framework/.claude/workflows/new2.js`** — reads `stack.schema_deploy_from_trunk_only`; the Production phase agent gates schema deploys on `HEAD == TRUNK` and returns `schemaDeploysDeferred`, which the workflow converts into `db-migration-deploy` residuals (owner-gated, collapsed by `ownerGatedActionKey`).
28
+ - **`framework/.claude/skills/new2/SKILL.md`** — Migration Gate Step 3.5 step 6 honors the off-trunk guard (degrade to deferred when `$MAIN` is not on trunk).
29
+ - **`src/commands/update.js`** — the `missingStack` schema-drift detector now surfaces boolean scalar `stack.*` keys (not just strings), so `schema_deploy_from_trunk_only` is offered to pre-v4.33.0 consumers on update.
30
+ - **`framework/agents/deployment-protocol.md`** — new datastore-agnostic "Schema deploys run from trunk only" best-practice subsection.
31
+ - **`framework/docs/PROJECT-CONFIGURATION.md`** — documents the new `stack.schema_deploy_from_trunk_only` key in § 4.4.
32
+
8
33
  ## [4.32.0] - 2026-06-11
9
34
 
10
35
  **`new2`: `deep` is now relevance-gated too — it no longer fires all 5 reviewers on every card regardless of content.** Diagnosing a real batch (the FEAT-0023 supplier-associated-users epic — permissions/RLS/migrations, so 7 of 11 cards are legitimately `review_profile: deep` per `prd-card-writer` Rule C) exposed an **asymmetry**, not a misclassification: the per-card review matrix relevance-gated `balanced` (a specialist runs only if its domain is evidenced by `scopeFiles ∪ MAY-EDIT`) but left `deep` on an **unconditional 5-way fan-out** (`FULL_FANOUT` = code-reviewer + doc-reviewer + qa-sentinel + api-perf-cost-auditor + security-reviewer). So a `deep` card with no doc surface still paid for a doc-reviewer, and one with no API/data surface still paid for api-perf-cost-auditor — every time. This is exactly the "every card gets a full review regardless of what's inside" symptom.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.32.0
1
+ 4.33.0
@@ -97,9 +97,38 @@ is wrong for the project, ask the user — never auto-execute a guess.
97
97
  | Third-party service config | Requires external dashboards and secrets |
98
98
  | DNS / domain changes | Risk of downtime — needs human judgment |
99
99
 
100
+ ### Schema-deploy-from-trunk-only guard (when `stack.schema_deploy_from_trunk_only: true`)
101
+
102
+ **Generic, datastore-agnostic safety invariant.** A remote schema mutation — any
103
+ RLS/access-rule deploy, index deploy, or migration `*deploy`/`*push`/apply command
104
+ (`supabase db push`, `prisma migrate deploy`, `firebase deploy --only firestore:rules|firestore:indexes|storage`,
105
+ `cdk deploy` / `terraform apply` of a table/GSI, a SQL migrator against the live DB,
106
+ etc.) — pushed from a **non-trunk branch / git worktree** desyncs the shared remote
107
+ datastore from every other in-flight branch's migration history, producing
108
+ "code-ahead-of-schema" production outages (a real consumer hit a `42703 undefined_column`
109
+ outage this way).
110
+
111
+ When `stack.schema_deploy_from_trunk_only: true` in `baldart.config.yml`, BEFORE
112
+ auto-executing ANY such remote schema/RLS/index/migration deploy:
113
+
114
+ 1. Resolve the current branch: `git rev-parse --abbrev-ref HEAD`.
115
+ 2. **On `git.trunk_branch`** → behave exactly as today (auto-execute when stack-matched).
116
+ 3. **On any other branch / worktree** → **NEVER auto-execute.** The migration stays
117
+ committed; surface a deferral instead — report it as a `MANUAL` item with the note
118
+ *"migration committed; remote schema deploy happens from `<trunk>` after merge"* and
119
+ record it under the tracker `## Production Readiness` as a deferred
120
+ **`db-migration-deploy`** residual. The deploy is not lost — it is intentionally
121
+ sequenced to run from trunk post-merge.
122
+
123
+ This guard enforces only the **branch invariant**; the actual command, env loader, and
124
+ gate UX stay delegated to the project overlay (`.baldart/overlays/new.md`). When the key
125
+ is `false` / unset, this section is a no-op and behavior is unchanged. Env-var / DNS /
126
+ secret / feature-flag items are already manual-only and unaffected.
127
+
100
128
  **Auto-execution procedure:**
101
129
 
102
- 1. For each auto-executable item detected, run the command immediately.
130
+ 1. For each auto-executable item detected, run the command immediately
131
+ (subject to the Schema-deploy-from-trunk-only guard above when enabled).
103
132
  2. Log the result (success/failure) in the tracker under `## Production Readiness`.
104
133
  3. In the final checklist output, mark auto-executed items with their result:
105
134
  ```
@@ -227,6 +256,7 @@ All GSIs must show `ACTIVE`. `CREATING` → poll; `DELETING` / `UPDATING` → ma
227
256
  - Order items by **deployment sequence** (items that must happen first go first — e.g., indexes before code deploy, env vars before code deploy).
228
257
  - For each item, include the **reason** (which card/feature requires it) and the **exact command or UI path**.
229
258
  - If an item is **uncertain** (e.g., you suspect a new index might be needed but aren't sure), mark it with `VERIFY` and explain what to check.
259
+ - **MUST — schema deploys from trunk only (when `stack.schema_deploy_from_trunk_only: true`):** never auto-execute a remote schema/RLS/index/migration deploy from a non-trunk branch or worktree. Gate on `git rev-parse --abbrev-ref HEAD == git.trunk_branch`; otherwise defer it as a `db-migration-deploy` residual ("deploy happens from trunk after merge"). Datastore-agnostic — applies to every persistence layer, not just one vendor. See the "Schema-deploy-from-trunk-only guard" above.
230
260
  - **Update the tracker** with the full checklist under a new `## Production Readiness` section.
231
261
 
232
262
  ---
@@ -25,6 +25,7 @@
25
25
  4. **(declared + artifacts present) Assemble the apply modalities**, in this source order, de-duplicating by `id`: (a) `migration_plan.apply_modalities` from the epic block; (b) a `## Migration modalities` section in `.baldart/overlays/new.md` if present; (c) any project-memory note on how this project applies migrations; (d) the built-in tail `["Già applicata — prosegui", "Abort"]`. Each modality is `{ id, label, command? }`.
26
26
  5. **`AskUserQuestion`** — `"La epic dichiara una migrazione DB (<summary>). Va applicata PRIMA del batch così le card downstream verificano contro lo schema reale. Come procedo?"` with up to 4 of the assembled modalities (always include "Già applicata — prosegui" and "Abort" as the last options). This is a legitimate Phase 0 question (Auto Mode does not override it).
27
27
  6. **Execute the choice in `$MAIN`** (project env):
28
+ - **Schema-deploy-from-trunk-only guard** (when `stack.schema_deploy_from_trunk_only: true`): a **command** modality is a remote schema deploy, so it may run only from trunk. Before executing one, check `git -C "$MAIN" rev-parse --abbrev-ref HEAD`; if it is **not** `git.trunk_branch`, do **NOT** apply or prompt for a command modality — write `## Migration\ndeclared but $MAIN is on '<branch>' (not trunk '<trunk>') — front-load deferred per schema_deploy_from_trunk_only (apply from trunk, then re-run)` to the tracker and **proceed to step 2** (the migration falls back to the end-of-batch owner-gated deferral — no regression). "Già applicata — prosegui" stays available (it executes no deploy). When the key is `false`/unset this guard is a no-op.
28
29
  - a **command** modality → run it with output to disk (`<cmd> > /tmp/migration-<FIRST-CARD-ID>.log 2>&1`), surface only the exit code; on exit 0 run the optional `migration_plan.verify` probe and require it green too. On **failure** → surface a bounded extract (`tail -n 30`) and re-ask (re-offer the modalities + Abort); never silently proceed against a non-live schema. **Never run a command without the user having selected it.**
29
30
  - **"Già applicata — prosegui"** → run the optional `verify` probe if present; otherwise trust the user.
30
31
  - **"Abort"** → HALT the batch cleanly (leave the tracker in place); do not create a worktree.
@@ -115,6 +115,13 @@ untouched), so the schema is live **before** the workflow starts. It mirrors `/n
115
115
  Step-2 "ONE pre-launch question" — pre-launch, not a mid-run gate; the zero-ask contract is about
116
116
  the *workflow*, which is untouched.
117
117
  6. **Execute the choice in `$MAIN`**:
118
+ - **Schema-deploy-from-trunk-only guard** (when `stack.schema_deploy_from_trunk_only: true`): a
119
+ **command** modality is a remote schema deploy → run it only from trunk. Check
120
+ `git -C "$MAIN" rev-parse --abbrev-ref HEAD`; if it is not `git.trunk_branch`, do NOT apply a
121
+ command modality — set `migration = { status: 'degraded', reason: 'off-trunk: schema_deploy_from_trunk_only' }`
122
+ and skip to Step 4 (the migration falls back to the workflow's end-of-batch owner-gated deferral →
123
+ a `db-migration-deploy` residual; apply it from trunk, then re-run). "Già applicata — prosegui"
124
+ stays valid (no deploy). No-op when the key is `false`/unset.
118
125
  - a **command** modality → run with output to `/tmp/migration-<firstCard>.log`, surface only the
119
126
  exit code; on exit 0 run the optional `migration_plan.verify` probe and require it green. On
120
127
  failure → bounded extract (`tail -n 30`) + re-ask. **Never run a command the user did not pick.**
@@ -60,8 +60,13 @@ const migrationAffects = Array.isArray(migration.affects_cards) ? migration.affe
60
60
  const features = cfg.features || {}
61
61
  const paths = cfg.paths || {}
62
62
  const gitCfg = cfg.git || {}
63
+ const stack = cfg.stack || {}
63
64
  const highRisk = paths.high_risk_modules || []
64
65
  const mergeStrategy = gitCfg.merge_strategy || 'pr'
66
+ // Stack-agnostic schema-deploy safety invariant (v4.33.0). When true, a remote
67
+ // schema/RLS/index/migration deploy is auto-executed ONLY from the trunk branch;
68
+ // from any other branch/worktree it is deferred as a `db-migration-deploy` residual.
69
+ const schemaDeployFromTrunkOnly = stack.schema_deploy_from_trunk_only === true
65
70
 
66
71
  // Mutable batch state — the workflow's variables ARE the tracker (kept out of the main loop).
67
72
  const firstCard = cardIds[0] || 'BATCH'
@@ -1067,14 +1072,37 @@ if (!committed.length) {
1067
1072
  phase('Production')
1068
1073
  if (mergeResult && mergeResult.merged) {
1069
1074
  try {
1075
+ // Schema-deploy-from-trunk-only invariant (v4.33.0): a remote schema/RLS/index/
1076
+ // migration deploy desyncs a shared remote datastore when run off-trunk. When the
1077
+ // flag is on, the agent gates every such deploy on `current branch == ${TRUNK}` and
1078
+ // returns any off-trunk deploy in `schemaDeploysDeferred` instead of executing it —
1079
+ // we then track each as a `db-migration-deploy` residual (the existing class, dedup-
1080
+ // collapsed onto one external action). Datastore-agnostic; the command itself stays
1081
+ // in the project overlay. No-op when the flag is false/unset.
1082
+ const trunkGate = schemaDeployFromTrunkOnly
1083
+ ? `\nSCHEMA-DEPLOY-FROM-TRUNK-ONLY is ON (stack.schema_deploy_from_trunk_only): a remote schema/RLS/index/migration deploy (supabase db push, prisma migrate deploy, firebase deploy --only firestore:rules|firestore:indexes|storage, cdk deploy / terraform apply of a table/GSI, any SQL migrator against the live DB) may run ONLY from the trunk branch. Run \`git rev-parse --abbrev-ref HEAD\`; auto-execute such a deploy ONLY if it equals \`${TRUNK}\`. On ANY other branch/worktree: DO NOT execute it — instead add it to schemaDeploysDeferred:[{ command, reason }] (reason = "remote schema deploy happens from ${TRUNK} after merge"). Index/cron/env/flag/DNS items are unaffected by this gate.`
1084
+ : ''
1070
1085
  prodReadiness = await agentSafe(
1071
- `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 }`,
1086
+ `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.${trunkGate}\n\n${projectBrief}\nChanged files: ${dedupe(committed.flatMap((r) => r.filesChanged || [])).join(', ') || '(derive from git)'}\n\nReturn: { autoExecuted:[...], manualItems:[...], schemaDeploysDeferred:[...], note }`,
1072
1087
  // Sonnet, not the inherited opus: a non-blocking report-not-execute checklist (auto-run only
1073
1088
  // stack-matched deploys, REPORT the rest). Sonnet follows the checklist reliably at a fraction
1074
1089
  // of the cost; nothing here gates the merge (it already happened).
1075
1090
  { label: 'production-readiness', phase: 'Production', agentType: 'general-purpose', model: 'sonnet',
1076
- schema: { type: 'object', required: ['manualItems'], additionalProperties: true, properties: { autoExecuted: { type: 'array', items: { type: 'string' } }, manualItems: { type: 'array', items: { type: 'string' } }, note: { type: 'string' } } } }
1091
+ schema: { type: 'object', required: ['manualItems'], additionalProperties: true, properties: { autoExecuted: { type: 'array', items: { type: 'string' } }, manualItems: { type: 'array', items: { type: 'string' } }, schemaDeploysDeferred: { type: 'array', items: { type: 'object', additionalProperties: true, properties: { command: { type: 'string' }, reason: { type: 'string' } } } }, note: { type: 'string' } } } }
1077
1092
  )
1093
+ // Off-trunk schema deploys the guard withheld → tracked as db-migration-deploy residuals
1094
+ // (owner-gated external action; collapsed by ownerGatedActionKey's db:push branch). The skill
1095
+ // materialises them; the deploy is sequenced from trunk, never silently dropped.
1096
+ let deployDeferred = 0
1097
+ for (const d of (prodReadiness && prodReadiness.schemaDeploysDeferred) || []) {
1098
+ const cmd = (d && d.command) || ''
1099
+ if (!cmd.trim()) continue
1100
+ // Evidence names "migration deploy" so ownerGatedActionKey maps it onto the single
1101
+ // db-migration-deploy action key (one remote deploy pushes all pending migrations).
1102
+ residuals.push({ card: firstCard, kind: 'db-migration-deploy', evidence: `remote schema/migration deploy deferred off-trunk: ${cmd} (${(d && d.reason) || 'deploy from ' + TRUNK + ' after merge'})`, materialized: false, deferralClass: 'owner-gated' })
1103
+ deployDeferred++
1104
+ }
1105
+ if (deployDeferred) ledger(firstCard, 'schema-deploy-trunk-gate', 'DEFERRED', `${deployDeferred} off-trunk schema deploy(s) → db-migration-deploy residual (run from ${TRUNK})`)
1078
1106
  ledger(firstCard, 'phase7-production', 'DONE', `auto=${((prodReadiness && prodReadiness.autoExecuted) || []).length} manual=${((prodReadiness && prodReadiness.manualItems) || []).length}`)
1079
1107
  } catch (_) { ledger(firstCard, 'phase7-production', 'SKIPPED', 'agent failed (non-blocking)') }
1080
1108
  } else {
@@ -161,6 +161,24 @@ Define safe deployment procedures and rollback strategies.
161
161
  - Test with production-sized data
162
162
  - Have rollback SQL ready
163
163
 
164
+ ### Schema deploys run from trunk only (datastore-agnostic)
165
+
166
+ **Never push a schema change to a shared remote datastore from a non-trunk branch
167
+ or git worktree.** A remote schema/RLS/index/migration deploy (`supabase db push`,
168
+ `prisma migrate deploy`, `firebase deploy --only firestore:rules|firestore:indexes`,
169
+ `cdk deploy` / `terraform apply` of a table or index, any SQL migrator against the
170
+ live DB) applied off-trunk desyncs the migration history from every other in-flight
171
+ branch and produces "code-ahead-of-schema" production outages (a stray
172
+ `undefined_column` / missing-table error against the live DB).
173
+
174
+ The rule is one line: **resolve the migration locally on the feature branch, but run
175
+ the remote deploy only after merge, from the trunk branch.** On projects that develop
176
+ multiple cards in parallel worktrees against a single shared remote datastore this is
177
+ mandatory. BALDART's `/new` + `new2` enforce it automatically when
178
+ `stack.schema_deploy_from_trunk_only: true` (see PROJECT-CONFIGURATION.md): they gate
179
+ every auto-deploy on `current branch == git.trunk_branch` and defer off-trunk deploys
180
+ to a tracked `db-migration-deploy` follow-up instead of executing them.
181
+
164
182
  ## Monitoring During Deployment
165
183
 
166
184
  Watch these metrics:
@@ -159,6 +159,9 @@ stack:
159
159
  auth_provider: "firebase-auth" # firebase-auth|supabase-auth|clerk|auth0|cognito|nextauth|lucia|custom|none|""
160
160
  framework: "nextjs" # nextjs|remix|sveltekit|astro|nuxt|rails|django|fastapi|express|none|""
161
161
  deployment: "vercel" # vercel|firebase|aws|gcp|cloudflare|render|fly|self-hosted|none|""
162
+
163
+ # Schema-deploy safety invariant (since v4.33.0). Default false (no-op).
164
+ schema_deploy_from_trunk_only: false # true → /new + new2 auto-deploy schema only from git.trunk_branch
162
165
  ```
163
166
 
164
167
  Skills propose only `canonical` libraries and refuse `forbidden` ones. Empty
@@ -183,6 +186,16 @@ the skill/agent set:
183
186
  - `/new` Production Readiness Checklist picks deploy commands matching
184
187
  `stack.deployment` + `stack.database` (firebase deploy, vercel deploy,
185
188
  supabase db push, CDK/Terraform apply).
189
+ - `stack.schema_deploy_from_trunk_only` (boolean, v4.33.0+, default `false`) is a
190
+ **stack-agnostic safety invariant**: when `true`, `/new` and `new2` auto-execute a
191
+ remote schema/RLS/index/migration deploy **only** when the current branch ==
192
+ `git.trunk_branch`. From any non-trunk branch / git worktree the deploy is never
193
+ auto-run — the migration stays committed and the remote deploy is deferred ("happens
194
+ from trunk after merge") and tracked as a `db-migration-deploy` residual. This prevents
195
+ migration-history desync / "code-ahead-of-schema" outages on projects that build
196
+ multiple cards in parallel worktrees against one shared remote datastore. The actual
197
+ command + env loader stay in the project overlay — core only enforces the branch gate.
198
+ `baldart configure` defaults it to `true` when it detects parallel worktree usage.
186
199
  - `security-reviewer` evaluates access rules per `stack.database`
187
200
  (Firestore rules, Supabase RLS, Mongo validators, DynamoDB IAM,
188
201
  Postgres RLS+GRANT).
@@ -122,6 +122,28 @@ stack:
122
122
  # "render" | "fly" | "self-hosted" | "none" | "".
123
123
  deployment: ""
124
124
 
125
+ # Schema-deploy-from-trunk-only safety invariant (since v4.33.0). Stack-agnostic
126
+ # guard against migration-history desync / "code-ahead-of-schema" production
127
+ # outages on projects that develop multiple cards in PARALLEL git worktrees
128
+ # against a SINGLE shared remote datastore.
129
+ #
130
+ # false (default) — preserves current behavior: /new + new2 may auto-execute a
131
+ # remote schema/RLS/index/migration deploy from whatever branch
132
+ # the deploy step runs on.
133
+ # true — a remote schema/RLS/index/migration deploy is auto-executed
134
+ # ONLY when the current branch == git.trunk_branch. From any
135
+ # non-trunk branch / worktree the deploy is NEVER auto-run:
136
+ # the migration stays committed and the remote deploy is
137
+ # deferred ("happens from trunk after merge") and tracked as a
138
+ # `db-migration-deploy` residual / Production-Readiness manual
139
+ # item. Datastore-agnostic: applies to supabase db push, prisma
140
+ # migrate deploy, firebase deploy --only firestore:rules/indexes,
141
+ # CDK/Terraform apply, etc. The actual command + env loader stay
142
+ # in the project overlay — core only enforces the branch gate.
143
+ # "" / unset — skill asks (and suggests persisting the answer). `baldart
144
+ # configure` defaults it to true when it detects worktree usage.
145
+ schema_deploy_from_trunk_only: false
146
+
125
147
  # ─── FEATURES ────────────────────────────────────────────────────────────
126
148
  # Explicit booleans. ALL keys must be present (true | false) once `baldart
127
149
  # configure` has run. An absent flag means the user hasn't been asked yet —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.32.0",
3
+ "version": "4.33.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"
@@ -99,6 +99,26 @@ function detectTrunkBranch(cwd = process.cwd()) {
99
99
  }
100
100
  }
101
101
 
102
+ /**
103
+ * Autodetect whether this project develops in parallel git worktrees
104
+ * (`git worktree list` shows linked worktrees beyond the main one). Used to
105
+ * default `stack.schema_deploy_from_trunk_only`: parallel worktrees against a
106
+ * single shared remote datastore are exactly the topology where a worktree-run
107
+ * schema deploy causes migration-history desync / code-ahead-of-schema outages.
108
+ * Non-fatal: any failure returns false (the back-compat default).
109
+ */
110
+ function detectWorktreeUsage(cwd = process.cwd()) {
111
+ try {
112
+ const { execSync } = require('child_process');
113
+ const opts = { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, encoding: 'utf8' };
114
+ const out = execSync('git worktree list --porcelain', opts);
115
+ const count = (out.match(/^worktree /gm) || []).length;
116
+ return count > 1; // main worktree always counts as 1 — >1 means linked worktrees exist
117
+ } catch (_) {
118
+ return false;
119
+ }
120
+ }
121
+
102
122
  function detect(cwd = process.cwd()) {
103
123
  const exists = (p) => fs.existsSync(path.join(cwd, p));
104
124
  const findFirst = (...candidates) => candidates.find(exists) || '';
@@ -313,6 +333,10 @@ function detect(cwd = process.cwd()) {
313
333
 
314
334
  const trunkBranchProbe = detectTrunkBranch(cwd);
315
335
  const mergeStrategyProbe = detectMergeStrategy(cwd, trunkBranchProbe.value);
336
+ // Default schema_deploy_from_trunk_only ON when parallel worktrees are in use
337
+ // (the topology prone to migration-history desync). Otherwise keep the
338
+ // back-compat false default — the guard is a no-op unless explicitly enabled.
339
+ const usesWorktrees = detectWorktreeUsage(cwd);
316
340
 
317
341
  // ---- Persistence layer (stack.database) --------------------------------
318
342
  let detectedDatabase = '';
@@ -404,6 +428,9 @@ function detect(cwd = process.cwd()) {
404
428
  auth_provider: detectedAuthProvider,
405
429
  framework: detectedFramework,
406
430
  deployment: detectedDeployment,
431
+ // Stack-agnostic schema-deploy safety invariant (v4.33.0). Defaulted ON when
432
+ // parallel worktrees are detected (the desync-prone topology), else false.
433
+ schema_deploy_from_trunk_only: usesWorktrees,
407
434
  // New: surface the detected monorepo + DS signals so skills can read them.
408
435
  monorepo: isMonorepo ? {
409
436
  detected: true,
@@ -938,6 +965,17 @@ async function interactivePrompts(merged, detected) {
938
965
  merged.stack.deployment || detected.stack.deployment || ''
939
966
  );
940
967
 
968
+ // Schema-deploy-from-trunk-only safety invariant (v4.33.0). Pre-checked when
969
+ // configure detected parallel worktrees — the topology prone to schema desync.
970
+ const sdftDefault = typeof merged.stack.schema_deploy_from_trunk_only === 'boolean'
971
+ ? merged.stack.schema_deploy_from_trunk_only
972
+ : (detected.stack.schema_deploy_from_trunk_only === true);
973
+ merged.stack.schema_deploy_from_trunk_only = await promptForKey(
974
+ 'Only auto-deploy remote schema/migrations from the trunk branch? (recommended when cards are developed in parallel worktrees against one shared remote DB)',
975
+ sdftDefault,
976
+ 'confirm'
977
+ );
978
+
941
979
  return merged;
942
980
  }
943
981
 
@@ -1284,12 +1284,13 @@ async function update(options = {}, unknownArgs = []) {
1284
1284
  .filter((k) => !(k in (cur2.paths || {})));
1285
1285
  const missingGit = Object.keys(tpl.git || {})
1286
1286
  .filter((k) => !(k in (cur2.git || {})));
1287
- // Scalar (string) top-level stack.* keys — e.g. stack.database,
1288
- // stack.auth_provider, stack.framework, stack.deployment. Sub-object
1287
+ // Scalar top-level stack.* keys — e.g. stack.database,
1288
+ // stack.auth_provider, stack.framework, stack.deployment (strings) and
1289
+ // stack.schema_deploy_from_trunk_only (boolean, since v4.33.0). Sub-object
1289
1290
  // keys (charting, animation, testing) are intentionally skipped:
1290
1291
  // their drift is handled by individual sub-prompts in configure.
1291
1292
  const missingStack = Object.keys(tpl.stack || {})
1292
- .filter((k) => typeof tpl.stack[k] === 'string')
1293
+ .filter((k) => ['string', 'boolean'].includes(typeof tpl.stack[k]))
1293
1294
  .filter((k) => !(k in (cur2.stack || {})));
1294
1295
  // Top-level nested config namespaces (e.g. `graph:` since v4.21.0).
1295
1296
  // Unlike `lsp:` (which propagates only via its `has_lsp_layer` flag),