baldart 4.23.0 → 4.24.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,20 @@ All notable changes to BALDART will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.24.0] - 2026-06-10
9
+
10
+ **Atomic backlog-ID allocator — no FEAT/BUG collisions across parallel worktrees.** When several `/prd` (or `/new`/`new2` follow-up) sessions run in parallel on sibling worktrees, each branched from the same trunk, the old `max(^id: FEAT-) + 1` scan made them all land on the **same next integer**: the other session's card was in flight on an unmerged sibling branch, invisible to both the local backlog and the trunk merge-base — so two epics both became `FEAT-0024` and conflicted at rebase/merge. The `git fetch` + merge-base scan only ever covered *already-merged* IDs, never in-flight ones. A new allocator anchors a lock + per-prefix high-water mark in `$MAIN/.worktrees/` (the shared coordination point every worktree already reaches via `git rev-parse --git-common-dir`, gitignored like `registry.json`), so a reservation is atomic across every worktree **on the same machine**. The high-water mark bumped under the lock is the correctness anchor; `max()` against the real backlog + sibling-worktree backlogs + reservations log + trunk merge-base makes it **self-healing**. **MINOR** (additive capability on the `worktree-manager` skill; opt-in — callers fall back to the inline merge-base scan + `[ID-RACE-RISK]` note when the script is absent, so older installs and cross-machine cloud agents are unaffected. **No `baldart.config.yml` key** — the allocator reuses `paths.backlog_dir` + the gitignored `.worktrees/` convention, so the schema-change propagation rule does not apply).
11
+
12
+ ### Added
13
+
14
+ - **`framework/.claude/skills/worktree-manager/scripts/allocate-id.sh`** — prefix-parametric (`FEAT`/`BUG`/`UI`/`DOC`/`PERF`/…) atomic ID allocator. `reserve <PREFIX> <slug>` prints the next free integer zero-padded to 4 digits; `release <worktree-path>` prunes a finished worktree's reservations. Cross-process mutex via atomic `mkdir` (stale-stolen after 30s via the lock dir's own mtime — a directory, not a git ref, so it never touches the shared `refs/stash` that `git stash` in worktrees is forbidden over). The slow `git fetch` runs **before** the lock, keeping the critical section local-FS-only (sub-second). Reads `paths.backlog_dir` + `git.trunk_branch` from `baldart.config.yml` and resolves `$MAIN` at runtime — no hardcoded project facts (passes the framework-edit-gate). Same-machine scope; the trunk merge-base scan is the best-effort cross-machine guard.
15
+
16
+ ### Changed
17
+
18
+ - **`framework/.claude/skills/worktree-manager/SKILL.md`** — new section **"ID Allocation"** documenting the allocator, the shared `.id-alloc.lock/` / `.id-hwm-<PREFIX>` / `id-reservations.jsonl` files, the same-machine scope + opt-in fallback contract, and the gap-tolerant monotonic-counter design. `release` is wired into the cleanup paths of `/mw` (step 7), `mw-docs` (step 6), and `/cw` (step 4).
19
+ - **`framework/.claude/agents/prd-card-writer.md`** — § "FEAT-XXXX numbering" + Pre-Generation Checklist item 1 now reserve the integer via the allocator (primary path), with the trunk fetch + merge-base scan + `[ID-RACE-RISK]` note as the documented fallback when the script is absent. Applies to any prefix the writer mints.
20
+ - **`framework/.claude/agents/prd.md`** — § 4.1 NAMING CONVENTIONS prefers the allocator (any prefix) over the plain backlog scan, with the same documented fallback.
21
+
8
22
  ## [4.23.0] - 2026-06-09
9
23
 
10
24
  **Functional Traceability Gate — no orphan UI affordances from mockups.** Handed-off mockups (Claude Design / Figma / the internal generators) routinely add interactive-looking chrome — buttons, icons, menu entries — that no requirement backed. Implementing a mockup **1:1 for fidelity** materialised that chrome into real markup + dead handlers + unused icon imports, and the cruft propagated forever. A new **unconditional** gate (independent of `features.has_design_system`) now requires every interactive or iconographic artifact to trace to a function before it becomes code: each artifact is classified **backed** (implement) / **decorative** (static-render, `aria-hidden`) / **orphan** (drop or escalate — never silent). The oracle is the PRD **UI Element Inventory `function_ref`** allowlist → card AC → orphan. Enforced at three boundaries: PRD mockup-intake (orphans become BLOCKING Discovery items), `ui-expert` implementation-time (BLOCKING pre-work), and `code-reviewer` / `/design-review` per-merge (`UI_ORPHAN_AFFORDANCE` HIGH finding). The `visual-fidelity-verifier` gains a `resolved_orphans` carve-out so a deliberately-dropped orphan is *expected-absent*, not a `component-missing` false positive. **MINOR** (additive discipline across existing UI surfaces; no new agent/skill/command and **no `baldart.config.yml` key** — the gate is registry-independent and reads its allowlist from the PRD inventory, so the schema-change propagation rule does not apply).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.23.0
1
+ 4.24.0
@@ -300,18 +300,36 @@ applies even when N=1.
300
300
 
301
301
  ### FEAT-XXXX numbering
302
302
 
303
- - Pick the next free `FEAT-XXXX` integer (Grep `^id: FEAT-` in `${paths.backlog_dir}/`, find max,
304
- add 1). **Reserve the entire integer for this epic** do NOT reuse the integer
305
- for unrelated cards even if it has fewer than ~99 children.
306
- - **Scan the canonical backlog, not just the worktree branch.** The worktree
307
- branched from the trunk may not contain IDs committed on sibling branches not
308
- yet merged. Before computing max, run
309
- `git fetch <git.trunk_branch>` and grep `${paths.backlog_dir}/` on the merge-base of the
310
- trunk (`git grep '^id: FEAT-' $(git merge-base HEAD <git.trunk_branch>)` plus
311
- the local worktree) so concurrent PRD sessions don't both land on the same
312
- "next free" integer. If a `git fetch` is not possible (offline), state the
313
- reservation is best-effort against the local view and surface a
314
- `[ID-RACE-RISK]` note so the caller can re-verify before commit.
303
+ - Pick the next free `FEAT-XXXX` integer. **Reserve the entire integer for this
304
+ epic** do NOT reuse the integer for unrelated cards even if it has fewer than
305
+ ~99 children.
306
+ - **Use the atomic allocator when present (primary path).** Plain `max + 1` is
307
+ unsafe under parallelism: concurrent PRD sessions on sibling worktrees, all
308
+ branched from the same trunk, see the same max (the other session's card is in
309
+ flight on an unmerged branch) and both pick the same integer → merge conflict.
310
+ The `worktree-manager` allocator reserves the integer atomically across every
311
+ worktree on this machine (lock + shared high-water mark under `.worktrees/`):
312
+
313
+ ```bash
314
+ # $MAIN is the main repo root: dirname of `git rev-parse --git-common-dir`.
315
+ ALLOC="$MAIN/.claude/skills/worktree-manager/scripts/allocate-id.sh"
316
+ if [ -x "$ALLOC" ]; then
317
+ N="$("$ALLOC" reserve FEAT "<slug>")" # e.g. prints 0024
318
+ # → the reserved epic id is FEAT-$N. The integer is now claimed; a parallel
319
+ # session calling reserve will get FEAT-$((N+1)).
320
+ fi
321
+ ```
322
+
323
+ State the reserved integer in your response.
324
+ - **Fallback when the allocator is absent** (older install, no script): degrade to
325
+ the canonical-backlog scan — `git fetch <git.trunk_branch>`, then
326
+ `max` over `git grep '^id: FEAT-' $(git merge-base HEAD <git.trunk_branch>)`
327
+ plus the local worktree backlog. This still misses in-flight sibling-worktree
328
+ IDs, so surface a `[ID-RACE-RISK]` note so the caller can re-verify before
329
+ commit. If `git fetch` is impossible (offline), say the reservation is
330
+ best-effort against the local view.
331
+ - The same procedure applies to any other prefix you mint (`BUG`, `UI`, `DOC`,
332
+ `PERF`): `reserve <PREFIX> <slug>` — the allocator is prefix-parametric.
315
333
 
316
334
  ### FORBIDDEN PATTERNS — agent MUST refuse to generate
317
335
 
@@ -350,10 +368,12 @@ Mirror their structure when generating new epic+child sets.
350
368
 
351
369
  Before writing the first YAML file, confirm and report to the caller:
352
370
 
353
- 1. **Reserved FEAT-XXXX integer**: Grep `^id: FEAT-` in `${paths.backlog_dir}/`, find max,
354
- pick max+1, per the concurrency-safe procedure in § "FEAT-XXXX numbering"
355
- (fetch trunk + merge-base scan to avoid duplicate IDs across parallel
356
- sessions). State the chosen integer in your response.
371
+ 1. **Reserved FEAT-XXXX integer**: reserve it via the atomic allocator
372
+ (`allocate-id.sh reserve FEAT <slug>`), per § "FEAT-XXXX numbering" — this is
373
+ the concurrency-safe path that prevents duplicate IDs across parallel sessions
374
+ on sibling worktrees. If the allocator is absent, fall back to the trunk
375
+ fetch + merge-base scan with an `[ID-RACE-RISK]` note. State the chosen
376
+ integer in your response.
357
377
  2. **Drafted epic + N children list**: list the planned filenames
358
378
  (`FEAT-XXXX-00-<slug>-epic.yml`, `FEAT-XXXX-01-<sub-slug>.yml`, ...) BEFORE
359
379
  writing them. This is a contract — the actual files MUST match the list.
@@ -498,7 +498,20 @@ Create protocol-compliant backlog cards for every actionable step. Cards MUST fo
498
498
 
499
499
  #### 4.1 — NAMING CONVENTIONS
500
500
 
501
- Before creating cards, scan `${paths.backlog_dir}/*.yml` to determine the next available number. File naming rules:
501
+ Before creating cards, reserve the next number for the chosen prefix. **Prefer the
502
+ atomic allocator** — a plain `${paths.backlog_dir}/*.yml` scan collides under
503
+ parallelism (concurrent sessions on sibling worktrees both pick the same "next"
504
+ integer and conflict at merge):
505
+
506
+ ```bash
507
+ # $MAIN = dirname of `git rev-parse --git-common-dir` (shared across worktrees).
508
+ ALLOC="$MAIN/.claude/skills/worktree-manager/scripts/allocate-id.sh"
509
+ [ -x "$ALLOC" ] && N="$("$ALLOC" reserve FEAT <slug>") # also BUG | UI | DOC | PERF
510
+ ```
511
+
512
+ If the allocator is absent (older install), fall back to scanning
513
+ `${paths.backlog_dir}/*.yml` for the max and add 1, noting `[ID-RACE-RISK]`. See
514
+ `worktree-manager` § "ID Allocation" for the mechanism. File naming rules:
502
515
 
503
516
  | Prefix | When to use | Example |
504
517
  |--------|-------------|---------|
@@ -292,7 +292,9 @@ git -C "$WORKTREE_PATH" rebase "origin/$TRUNK"
292
292
  # 5. Sync local trunk ref — reuse /mw "Common" block: ff-only pull when main
293
293
  # repo HEAD is already $TRUNK; otherwise just fetch.
294
294
 
295
- # 6. Remove the registry entry for this worktree.
295
+ # 6. Remove the registry entry for this worktree, then release its backlog-ID
296
+ # reservations (best-effort, high-water mark untouched):
297
+ # .claude/skills/worktree-manager/scripts/allocate-id.sh release "$WORKTREE_PATH" 2>/dev/null || true
296
298
  ```
297
299
 
298
300
  **Forbidden in docs merge**: pre-merge `npm run build`, `npx eslint`, `npx tsc`, `npm run test`, post-merge `npm run build`. The whole point of docs mode is that these gates are inapplicable.
@@ -356,6 +358,57 @@ Create the file if it doesn't exist. Update it on every `/nw`, `/mw`, `/cw` oper
356
358
 
357
359
  ---
358
360
 
361
+ ## ID Allocation — atomic backlog-ID reservation across parallel worktrees
362
+
363
+ When several PRD / card sessions run in parallel on sibling worktrees, each
364
+ branched from the same trunk, a plain `max(^id: PREFIX-) + 1` scan makes them all
365
+ land on the **same next integer**: the other session's card is in flight on an
366
+ unmerged sibling branch, invisible to both the local backlog and the trunk
367
+ merge-base. They collide at rebase/merge time.
368
+
369
+ `scripts/allocate-id.sh` closes that race **on the same machine**. Every worktree
370
+ shares one main repo root (resolved from `git rev-parse --git-common-dir`), and
371
+ `.worktrees/` lives there and is gitignored — so it is the natural shared
372
+ coordination point, exactly like `registry.json`. The allocator anchors a lock
373
+ and a per-prefix high-water-mark there:
374
+
375
+ ```bash
376
+ # Reserve the next free integer for a prefix (FEAT, BUG, UI, DOC, PERF, …).
377
+ # Prints the integer zero-padded to 4 digits on stdout; diagnostics on stderr.
378
+ .claude/skills/worktree-manager/scripts/allocate-id.sh reserve FEAT <slug>
379
+
380
+ # Release a finished worktree's reservations (best-effort prune; never blocks).
381
+ .claude/skills/worktree-manager/scripts/allocate-id.sh release <worktree-path>
382
+ ```
383
+
384
+ Shared files, all under `$MAIN/.worktrees/` (already gitignored):
385
+ - `.id-alloc.lock/` — cross-process mutex (atomic `mkdir`, stale-stolen after 30s
386
+ via the dir's own mtime; it is a directory, NOT a git ref, so it does not touch
387
+ the shared `refs/stash` that `git stash` in worktrees is forbidden over).
388
+ - `.id-hwm-<PREFIX>` — monotonic high-water mark per prefix. **This is the
389
+ correctness anchor**: bumped under the lock, never decremented.
390
+ - `id-reservations.jsonl` — append-only `{prefix,id,worktree,slug,ts}` log for
391
+ observability + `release` pruning. NOT load-bearing for correctness.
392
+
393
+ Inside the lock, `reserve` takes `NEXT = max(high-water file, local backlog, every
394
+ sibling worktree's backlog from registry.json, reservations log, trunk
395
+ merge-base) + 1`. The `max()` against the real backlog makes it **self-healing**:
396
+ if `.id-hwm-*` is ever lost or a card was created bypassing the allocator, the
397
+ next reservation still skips occupied numbers. Abandoned reservations leave
398
+ **gaps**, which is fine — backlog IDs need not be contiguous, and "reserve the
399
+ whole integer, never reuse" is already the rule.
400
+
401
+ **Scope: same machine only.** The shared file cannot reach separate cloud agents;
402
+ the trunk merge-base scan inside `reserve` is the best-effort cross-machine guard.
403
+ Callers (`prd-card-writer`, the `prd` agent) invoke `reserve` when the script is
404
+ present and **fall back to their inline merge-base scan + `[ID-RACE-RISK]` note**
405
+ when it is absent (older installs) — the allocator is additive, never required.
406
+
407
+ `release` is wired into the cleanup path of `/mw`, `mw-docs`, and `/cw` (see those
408
+ steps) so a merged/cleaned worktree's reservations are pruned.
409
+
410
+ ---
411
+
359
412
  ## /nw — New Worktree
360
413
 
361
414
  Supports three modes:
@@ -1077,6 +1130,13 @@ git worktree prune
1077
1130
 
1078
1131
  Remove the entry from `.worktrees/registry.json`.
1079
1132
 
1133
+ Also release this worktree's backlog-ID reservations (best-effort; the high-water
1134
+ mark stays untouched so numbering remains monotonic):
1135
+
1136
+ ```bash
1137
+ .claude/skills/worktree-manager/scripts/allocate-id.sh release "$WORKTREE_PATH" 2>/dev/null || true
1138
+ ```
1139
+
1080
1140
  ### 8. Report
1081
1141
 
1082
1142
  ```
@@ -1184,7 +1244,12 @@ git worktree prune
1184
1244
 
1185
1245
  ### 4. Update registry
1186
1246
 
1187
- Remove cleaned entries from `.worktrees/registry.json`.
1247
+ Remove cleaned entries from `.worktrees/registry.json`. For each removed
1248
+ worktree, also release its backlog-ID reservations (best-effort):
1249
+
1250
+ ```bash
1251
+ .claude/skills/worktree-manager/scripts/allocate-id.sh release "<removed-worktree-path>" 2>/dev/null || true
1252
+ ```
1188
1253
 
1189
1254
  ### 5. Report
1190
1255
 
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # allocate-id.sh — atomic, cross-worktree backlog-ID allocator (same machine).
4
+ #
5
+ # Problem it solves: several PRD/card sessions running in parallel on sibling
6
+ # worktrees all branch from the same trunk, so each computes the same
7
+ # "max(^id: PREFIX-) + 1" and they collide at rebase/merge time. A plain
8
+ # backlog scan cannot see IDs that are still in flight on an unmerged sibling
9
+ # worktree branch.
10
+ #
11
+ # Mechanism: every worktree shares the same main repo root (resolved from
12
+ # `git rev-parse --git-common-dir`), and `.worktrees/` lives there and is
13
+ # gitignored. We anchor a lock + a per-prefix high-water-mark file there, so a
14
+ # reservation is atomic across every worktree on this machine. The high-water
15
+ # bumped under the lock is the correctness mechanism; the max() against the real
16
+ # backlog makes it self-healing if the counter file is ever lost or bypassed.
17
+ #
18
+ # Scope: SAME MACHINE only (a shared local file). Cross-machine (separate cloud
19
+ # agents) is not covered here — the caller keeps the merge-base scan as a
20
+ # best-effort cross-machine guard and surfaces an [ID-RACE-RISK] note.
21
+ #
22
+ # Usage:
23
+ # allocate-id.sh reserve <PREFIX> <slug> # prints the reserved integer, zero-padded to 4
24
+ # allocate-id.sh release <worktree-path> # prunes that worktree's reservations (best-effort)
25
+ #
26
+ # Exit non-zero on hard failure (not a git repo, lock unobtainable) so the
27
+ # caller can fall back to its inline scan. All diagnostics go to stderr; stdout
28
+ # carries ONLY the allocated number on success.
29
+
30
+ set -u
31
+
32
+ LOCK_STALE_SECONDS=30
33
+ LOCK_MAX_TRIES=150 # ~30s at 0.2s/try
34
+
35
+ err() { printf '%s\n' "$*" >&2; }
36
+
37
+ # --- Resolve the shared main repo root from any worktree -------------------
38
+ resolve_main() {
39
+ local common
40
+ common="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
41
+ case "$common" in
42
+ /*) ;; # already absolute
43
+ *) common="$(pwd)/$common" ;; # relative (we're in the main repo) → absolutise
44
+ esac
45
+ (cd "$common/.." 2>/dev/null && pwd) || return 1
46
+ }
47
+
48
+ # --- Read a paths.* / git.* scalar from baldart.config.yml -----------------
49
+ config_scalar() {
50
+ # $1 = top-level block (paths|git), $2 = key
51
+ local block="$1" key="$2" cfg="$MAIN/baldart.config.yml"
52
+ [ -f "$cfg" ] || return 0
53
+ grep -A60 "^${block}:" "$cfg" 2>/dev/null \
54
+ | grep -m1 "[[:space:]]*${key}:" \
55
+ | sed -E "s/.*${key}:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" \
56
+ | sed -E 's/[[:space:]]+$//'
57
+ }
58
+
59
+ # --- Highest integer for PREFIX across one or more directories -------------
60
+ scan_dirs_max() {
61
+ local prefix="$1"; shift
62
+ local max="$1"; shift # starting floor
63
+ local d line n
64
+ for d in "$@"; do
65
+ [ -d "$d" ] || continue
66
+ while IFS= read -r line; do
67
+ [ -n "$line" ] || continue
68
+ n="$(printf '%s' "$line" | sed -E "s/^id:[[:space:]]*${prefix}-0*([0-9]+).*/\1/")"
69
+ case "$n" in ''|*[!0-9]*) continue ;; esac
70
+ [ "$n" -gt "$max" ] && max="$n"
71
+ done <<EOF
72
+ $(grep -rhoE "^id:[[:space:]]*${prefix}-[0-9]+" "$d"/*.yml 2>/dev/null)
73
+ EOF
74
+ done
75
+ printf '%s' "$max"
76
+ }
77
+
78
+ # --- Lock helpers (mkdir is atomic on POSIX; portable, no flock) -----------
79
+ # Staleness uses the lock directory's own mtime (set atomically by mkdir, and
80
+ # refreshed when the holder writes pid) — NOT a ts file that may not be written
81
+ # yet, which would race a just-created lock into an instant false steal. The
82
+ # critical section is local-FS only (the slow `git fetch` runs before the lock),
83
+ # so the stale threshold is never tripped by a legitimate holder.
84
+ file_mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0; }
85
+ acquire_lock() {
86
+ local tries=0 now m age
87
+ mkdir -p "$WT_DIR" 2>/dev/null || true
88
+ while ! mkdir "$LOCKDIR" 2>/dev/null; do
89
+ if [ -d "$LOCKDIR" ]; then
90
+ now="$(date +%s)"; m="$(file_mtime "$LOCKDIR")"
91
+ case "$m" in ''|*[!0-9]*) m=0 ;; esac
92
+ age=$(( now - m ))
93
+ if [ "$m" -gt 0 ] && [ "$age" -gt "$LOCK_STALE_SECONDS" ]; then
94
+ err "WARN: stealing stale id-alloc lock (age ${age}s)"; rm -rf "$LOCKDIR" 2>/dev/null; continue
95
+ fi
96
+ fi
97
+ tries=$(( tries + 1 ))
98
+ [ "$tries" -gt "$LOCK_MAX_TRIES" ] && { err "ERROR: could not acquire id-alloc lock"; return 1; }
99
+ sleep 0.2
100
+ done
101
+ printf '%s\n' "$$" > "$LOCKDIR/pid" 2>/dev/null || true
102
+ }
103
+ release_lock() { rm -rf "$LOCKDIR" 2>/dev/null || true; }
104
+
105
+ # ===========================================================================
106
+ CMD="${1:-}"
107
+ MAIN="$(resolve_main)" || { err "ERROR: not inside a git repository"; exit 1; }
108
+ WT_DIR="$MAIN/.worktrees"
109
+ LOCKDIR="$WT_DIR/.id-alloc.lock"
110
+ RESV="$WT_DIR/id-reservations.jsonl"
111
+ REG="$WT_DIR/registry.json"
112
+
113
+ case "$CMD" in
114
+ reserve)
115
+ PREFIX="${2:-}"; SLUG="${3:-}"
116
+ [ -n "$PREFIX" ] || { err "usage: allocate-id.sh reserve <PREFIX> <slug>"; exit 2; }
117
+ case "$PREFIX" in *[!A-Z]*|'') err "ERROR: PREFIX must be uppercase letters (e.g. FEAT, BUG)"; exit 2 ;; esac
118
+
119
+ BACKLOG_DIR="$(config_scalar paths backlog_dir)"; [ -n "$BACKLOG_DIR" ] || BACKLOG_DIR="backlog"
120
+ TRUNK="$(config_scalar git trunk_branch)"
121
+ WT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$MAIN")"
122
+ HWM="$WT_DIR/.id-hwm-$PREFIX"
123
+
124
+ # Refresh the remote trunk ref BEFORE taking the lock — it's the only slow
125
+ # (network) step; keeping it out of the critical section means a holder can
126
+ # never be stale-stolen mid-allocation.
127
+ [ -n "$TRUNK" ] && git fetch origin "$TRUNK" --quiet 2>/dev/null || true
128
+
129
+ acquire_lock || exit 1
130
+ trap release_lock EXIT
131
+
132
+ # 1) Floor = stored high-water mark (correctness anchor).
133
+ max=0
134
+ if [ -f "$HWM" ]; then
135
+ max="$(tr -cd '0-9' < "$HWM" 2>/dev/null)"; case "$max" in ''|*[!0-9]*) max=0 ;; esac
136
+ fi
137
+
138
+ # 2) Local worktree backlog + every sibling worktree's backlog (in flight).
139
+ dirs="$MAIN/$BACKLOG_DIR"
140
+ [ "$WT" != "$MAIN" ] && dirs="$dirs $WT/$BACKLOG_DIR"
141
+ if [ -f "$REG" ]; then
142
+ while IFS= read -r p; do
143
+ [ -n "$p" ] && dirs="$dirs $p/$BACKLOG_DIR"
144
+ done <<EOF
145
+ $(grep -oE '"path"[[:space:]]*:[[:space:]]*"[^"]*"' "$REG" 2>/dev/null | sed -E 's/.*"([^"]*)"$/\1/')
146
+ EOF
147
+ fi
148
+ # shellcheck disable=SC2086
149
+ max="$(scan_dirs_max "$PREFIX" "$max" $dirs)"
150
+
151
+ # 3) Reservations log (covers a wiped HWM with reservations still in flight).
152
+ if [ -f "$RESV" ]; then
153
+ while IFS= read -r n; do
154
+ n="$(printf '%s' "$n" | sed -E "s/.*\"${PREFIX}-0*([0-9]+).*/\1/")"
155
+ case "$n" in ''|*[!0-9]*) continue ;; esac
156
+ [ "$n" -gt "$max" ] && max="$n"
157
+ done <<EOF
158
+ $(grep -oE "\"id\":\"${PREFIX}-[0-9]+" "$RESV" 2>/dev/null)
159
+ EOF
160
+ fi
161
+
162
+ # 4) Best-effort cross-machine guard: merge-base of trunk (already-merged IDs).
163
+ # The fetch already ran before the lock; this is a local object-store read.
164
+ if [ -n "$TRUNK" ]; then
165
+ MB="$(git merge-base HEAD "origin/$TRUNK" 2>/dev/null || git merge-base HEAD "$TRUNK" 2>/dev/null || true)"
166
+ if [ -n "$MB" ]; then
167
+ while IFS= read -r n; do
168
+ n="$(printf '%s' "$n" | sed -E "s/^id:[[:space:]]*${PREFIX}-0*([0-9]+).*/\1/")"
169
+ case "$n" in ''|*[!0-9]*) continue ;; esac
170
+ [ "$n" -gt "$max" ] && max="$n"
171
+ done <<EOF
172
+ $(git grep -hoE "^id:[[:space:]]*${PREFIX}-[0-9]+" "$MB" -- "$BACKLOG_DIR/*.yml" 2>/dev/null)
173
+ EOF
174
+ fi
175
+ fi
176
+
177
+ NEXT=$(( max + 1 ))
178
+ printf '%s\n' "$NEXT" > "$HWM" 2>/dev/null || true
179
+ printf '{"prefix":"%s","id":"%s-%04d","worktree":"%s","slug":"%s","ts":"%s"}\n' \
180
+ "$PREFIX" "$PREFIX" "$NEXT" "$WT" "$SLUG" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$RESV" 2>/dev/null || true
181
+
182
+ release_lock; trap - EXIT
183
+ printf '%04d\n' "$NEXT"
184
+ ;;
185
+
186
+ release)
187
+ WT="${2:-}"
188
+ [ -n "$WT" ] || { err "usage: allocate-id.sh release <worktree-path>"; exit 2; }
189
+ [ -f "$RESV" ] || exit 0
190
+ acquire_lock || exit 0 # best-effort prune; never block cleanup
191
+ trap release_lock EXIT
192
+ # grep -v exit code: 0 = some lines kept, 1 = none kept (all pruned → empty
193
+ # file is the correct result), >=2 = real error (keep the original).
194
+ grep -vF "\"worktree\":\"$WT\"" "$RESV" > "$RESV.tmp" 2>/dev/null
195
+ if [ "$?" -le 1 ]; then
196
+ mv "$RESV.tmp" "$RESV" 2>/dev/null || rm -f "$RESV.tmp" 2>/dev/null
197
+ else
198
+ rm -f "$RESV.tmp" 2>/dev/null
199
+ fi
200
+ release_lock; trap - EXIT
201
+ ;;
202
+
203
+ *)
204
+ err "usage: allocate-id.sh {reserve <PREFIX> <slug> | release <worktree-path>}"
205
+ exit 2
206
+ ;;
207
+ esac
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.23.0",
3
+ "version": "4.24.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"