baldart 4.63.0 → 4.65.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 +39 -0
- package/README.md +3 -2
- package/VERSION +1 -1
- package/bin/baldart.js +15 -0
- package/framework/.claude/agents/REGISTRY.md +3 -0
- package/framework/.claude/agents/code-reviewer.md +25 -11
- package/framework/.claude/agents/codebase-architect.md +13 -5
- package/framework/.claude/agents/doc-reviewer.md +26 -12
- package/framework/.claude/agents/merge-conflict-resolver.md +58 -0
- package/framework/.claude/agents/ui-expert.md +18 -10
- package/framework/.claude/skills/context-primer/SKILL.md +7 -0
- package/framework/.claude/skills/design-system-init/SKILL.md +164 -156
- package/framework/.claude/skills/design-system-init/scripts/component-spec.template.md +26 -3
- package/framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs +151 -0
- package/framework/.claude/skills/new/references/merge-cleanup.md +23 -18
- package/framework/.claude/skills/worktree-manager/SKILL.md +19 -0
- package/framework/.claude/skills/worktree-manager/scripts/merge-worktree.sh +518 -0
- package/framework/.claude/workflows/new2.js +14 -3
- package/framework/agents/component-manifest-schema.md +145 -0
- package/framework/agents/design-system-protocol.md +63 -23
- package/framework/agents/index.md +2 -0
- package/framework/docs/COMPONENT-MANIFEST-LAYER.md +100 -0
- package/framework/routines/ds-drift.routine.yml +19 -12
- package/framework/templates/baldart.config.template.yml +22 -0
- package/package.json +1 -1
- package/src/commands/configure.js +54 -0
- package/src/commands/doctor.js +85 -0
- package/src/commands/tokens.js +80 -0
- package/src/commands/update.js +7 -0
- package/src/utils/token-emitters/README.md +36 -0
- package/src/utils/token-emitters/css.js +18 -0
- package/src/utils/token-emitters/index.js +47 -0
- package/src/utils/token-emitters/ts.js +32 -0
- package/src/utils/tokens-generator.js +165 -0
|
@@ -6,28 +6,33 @@
|
|
|
6
6
|
|
|
7
7
|
(The batch now enters Merge & cleanup. No visibility emission — the internal tracker is the only state surface, see SKILL.md § "State surface — the tracker only". Record merge/cleanup completion in the tracker only.)
|
|
8
8
|
|
|
9
|
-
After the final review passes AND all cards are committed in the worktree,
|
|
9
|
+
After the final review passes AND all cards are committed in the worktree, run the merge through the **deterministic SSOT script** `scripts/merge-worktree.sh` — the merge analogue of `setup-worktree.sh`. **Why a script, not an inline `/mw` Skill call here:** a `Skill()` runs IN this orchestrator's context, so resolving conflicts inline at end-of-batch re-reads the giant cache on every turn (the measured **~20M-token** regression). The script runs the entire merge (safety commit → rebase → **deterministic** additive conflict resolution → build → land → sync → cleanup → registry) as a **background Bash**, so the orchestrator's only in-context cost is launching it + reading a small status file. The script CANNOT fabricate or stall — it does the work or writes an honest status. The one thing it never does is resolve a **code** conflict (that is irreducible judgment) — it pauses and hands those off, off-context, to the `merge-conflict-resolver` subagent.
|
|
10
10
|
|
|
11
|
-
1. **BEFORE
|
|
11
|
+
1. **BEFORE launching** — verify no uncommitted files remain in the worktree (the script also makes a safety commit, but commit here too for belt-and-suspenders):
|
|
12
12
|
```bash
|
|
13
|
-
|
|
14
|
-
git status --porcelain
|
|
13
|
+
git -C <worktree-path> status --porcelain
|
|
15
14
|
```
|
|
16
|
-
If ANY uncommitted files exist
|
|
17
|
-
2. **
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
If ANY uncommitted files exist, commit them NOW with `[safety] Auto-commit remaining files before merge`.
|
|
16
|
+
2. **Resolve + launch the script as a BACKGROUND Bash** (you ARE the orchestrator — launch it detached, end the turn, read the status file on resume — exactly how setup.md step 4 launches `setup-worktree.sh`):
|
|
17
|
+
```bash
|
|
18
|
+
MERGE_SH="$(ls .framework/framework/.claude/skills/worktree-manager/scripts/merge-worktree.sh .claude/skills/worktree-manager/scripts/merge-worktree.sh 2>/dev/null | head -1)"
|
|
19
|
+
bash "$MERGE_SH" --worktree <absolute worktree path from tracker ## Worktree> \
|
|
20
|
+
--manifest /tmp/new-merge-status-<FIRST-CARD>.txt --skip-checks
|
|
22
21
|
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
`--skip-checks` = checksAlreadyPassed (final review + QA already validated the build; the safety commit still runs regardless). The script resolves branch / trunk / strategy / cards from the registry entry itself — do NOT pass them unless overriding.
|
|
23
|
+
3. **Read the status file** (`/tmp/new-merge-status-<FIRST-CARD>.txt`) and branch on `status:` — **never** inspect the conflicts inline in this orchestrator:
|
|
24
|
+
- **`success`** → the merge landed (`merge_commit` + `merge_ts` are in the file). Proceed to step 5 (record) → Phase 6b.
|
|
25
|
+
- **`code_conflict`** → the script paused the rebase; `conflict_files:` lists the UNRESOLVED code/test files (every additive doc/registry/JSONL conflict is ALREADY resolved). Delegate the judgment **off-context** — spawn the resolver subagent (do NOT read the conflicts yourself):
|
|
26
|
+
```
|
|
27
|
+
Task({ subagent_type: "merge-conflict-resolver",
|
|
28
|
+
description: "Resolve final-merge code conflicts",
|
|
29
|
+
prompt: "worktreePath=<wt> statusPath=/tmp/new-merge-status-<FIRST-CARD>.txt manifestPath=/tmp/new-merge-status-<FIRST-CARD>.txt\nClassify each conflict additive→resolve, semantic→STOP, then run merge-worktree.sh --continue per your agent definition." })
|
|
30
|
+
```
|
|
31
|
+
The subagent resolves the additive code conflicts (build-verified), STOPs on semantic ones, and re-runs the script to land. Read its **COMPACT** return, then run the **anti-fabrication disk gate** (do NOT trust its prose): re-read the status file — it counts as merged ONLY if `status: success` AND `merge_commit:` is non-empty (the script writes `success` only after it has landed AND cleaned up the worktree). Any other terminal status (`code_conflict` still, `build_fail`, `error`) means the resolver did not finish → treat as a blocker (next bullet). "Came to rest" ≠ "done".
|
|
32
|
+
- **`build_fail` / `error` / `sync_needs_decision`** (or the resolver returned a semantic STOP) → **HARD: never hand-merge in this orchestrator.** Do NOT fall back to inline `git merge` / `git rebase` / `git push` (the ~20M regression + a non-isolation-safe `git checkout` on the shared main repo). Surface the status `error:` verbatim, then — **interactive** → `AskUserQuestion` how to proceed; **AUTONOMOUS** → leave the worktree intact and materialize a follow-up card describing the merge blocker (log `AUTO: merge-blocked → follow-up <id>`).
|
|
33
|
+
- **FALLBACK — script absent OR Task tool unavailable (Codex / older subtree):** if `$MERGE_SH` is empty, OR the `Task`/Agent tool is not available at runtime, fall back to the legacy `Skill({ skill: "worktree-manager", args: "/mw worktreePath=<path> checksAlreadyPassed=true" })` (the skill runs the same sequence inline — zero regression; the only valid skill name is `worktree-manager`, NOT `new:new` / `mw` / `__phase6_merge__`).
|
|
34
|
+
4. **Transcribe sync markers into the tracker.** Read `sync_marker:` / `sync_detail:` from the status file. If `sync_marker` ≠ `none`, append the marker line to the tracker `## Worktree Merges` section as `[<sync_marker>] <sync_detail>` — Phase 6c parses it from there to run its workspace-hygiene gate. (The legacy `Skill(/mw)` fallback emits the same `[SYNC-…]` marker on stdout; capture it into the tracker the same way.)
|
|
35
|
+
5. Record the merge result in the tracker. **Record the merge timestamp** under `## Worktree Merges` (`merge_ts: <ISO-8601>`) — read it straight from the status file `merge_ts:` (`local-push` and `pr` both stamp it). Phase 8's `cycle_time_mins` reads THIS field as the strategy-independent end anchor (never a raw hash that may be the PR HEAD).
|
|
31
36
|
|
|
32
37
|
### Phase 6b — Backlog Card Status Reconciliation (MANDATORY — ZERO TOLERANCE)
|
|
33
38
|
|
|
@@ -699,6 +699,25 @@ if [ -z "$WORKTREE_PATH" ] || [ -z "$BRANCH" ] || [ -z "$TRUNK" ] || [ -z "$MAIN
|
|
|
699
699
|
fi
|
|
700
700
|
```
|
|
701
701
|
|
|
702
|
+
### 1b. Delegate to the SSOT script (PREFERRED — steps 2–7 below are the fallback)
|
|
703
|
+
|
|
704
|
+
The ENTIRE merge sequence (safety commit → rebase → **deterministic** additive conflict resolution → build → land via `git.merge_strategy` → sync → cleanup → registry) lives **once** in `scripts/merge-worktree.sh`, the SAME script `/new` Phase 6 and `new2` run — the merge analogue of `setup-worktree.sh`. Do NOT hand-roll the bash: a re-authored copy is the duplication CLAUDE.md forbids, and the script cannot fabricate a merge or stall. Steps 2–7 below remain as the literal **fallback** for when the script is absent (older subtree) and as the human-readable SSOT the script mirrors.
|
|
705
|
+
|
|
706
|
+
```bash
|
|
707
|
+
MERGE_SH="$(ls "$MAIN/.framework/framework/.claude/skills/worktree-manager/scripts/merge-worktree.sh" "$MAIN/.claude/skills/worktree-manager/scripts/merge-worktree.sh" 2>/dev/null | head -1)"
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
- **If `$MERGE_SH` is found** — run it and SKIP steps 2–7 (the script does them). Interactive `/mw` runs the pre-merge quality gates (omit `--skip-checks`); **programmatic** `/mw` (orchestrator passed `checksAlreadyPassed=true`) adds `--skip-checks`:
|
|
711
|
+
```bash
|
|
712
|
+
bash "$MERGE_SH" --worktree "$WORKTREE_PATH" --manifest /tmp/mw-status-$(basename "$BRANCH").txt [--skip-checks]
|
|
713
|
+
```
|
|
714
|
+
Then read the status file and act on `status:`:
|
|
715
|
+
- **`success`** → report the merge (`merge_commit` / `merge_ts` / `strategy` from the file). Done.
|
|
716
|
+
- **`code_conflict`** → the rebase is paused; `conflict_files:` lists the UNRESOLVED code/test files (all additive doc/registry/JSONL conflicts are already resolved). **Programmatic** (orchestrator): the caller spawns the `merge-conflict-resolver` subagent (see `framework/.claude/skills/new/references/merge-cleanup.md` Phase 6). **Interactive standalone `/mw`**: surface `conflict_files` to the user, or — if asked — spawn `merge-conflict-resolver` yourself; after it resolves, re-run with `--continue`. Never resolve a code conflict by guessing.
|
|
717
|
+
- **`build_fail` / `error` / `sync_needs_decision`** → surface `error:` verbatim and STOP (the worktree is left intact). Do NOT hand-roll a merge.
|
|
718
|
+
- The script writes `sync_marker:` / `sync_detail:` into the status file (NOT stdout) — an orchestrator routes them through its workspace-hygiene gate; standalone `/mw` just reports them.
|
|
719
|
+
- **If `$MERGE_SH` is empty** (older subtree) — fall through to steps 2–7 below (the inline fallback).
|
|
720
|
+
|
|
702
721
|
### 2. Env sync check
|
|
703
722
|
|
|
704
723
|
Compare the NEWEST modification time among the `stack.env_files` artifacts in the
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# merge-worktree.sh — SSOT deterministic CODE-worktree MERGE + land + cleanup.
|
|
4
|
+
#
|
|
5
|
+
# The single source of truth for the worktree MERGE sequence, consumed identically
|
|
6
|
+
# by three callers (no per-caller duplication — the sin CLAUDE.md forbids):
|
|
7
|
+
# • /mw (worktree-manager § "/mw") — interactive + programmatic
|
|
8
|
+
# • /new (skill: framework/.claude/skills/new/references/merge-cleanup.md Phase 6)
|
|
9
|
+
# • new2 (framework/.claude/workflows/new2.js Merge phase)
|
|
10
|
+
#
|
|
11
|
+
# Why a deterministic script instead of a model-driven subagent (the SAME rationale
|
|
12
|
+
# as setup-worktree.sh): the merge is MOSTLY PURE PLUMBING (safety-commit → rebase →
|
|
13
|
+
# strip ADDITIVE conflicts → build → land → sync → cleanup → registry). Driving that
|
|
14
|
+
# through a model put a model in the loop for mechanical work — which either FABRICATES
|
|
15
|
+
# a well-formed "merged, all good" with nothing on disk, or STALLS mid-flight. A
|
|
16
|
+
# deterministic script cannot fabricate or stall: it does the work or fails honestly
|
|
17
|
+
# with a status file the caller string-matches (never the script's word).
|
|
18
|
+
#
|
|
19
|
+
# The ONE thing the script does NOT do is resolve a CODE conflict: classifying a code
|
|
20
|
+
# hunk as additive-vs-semantic is irreducible JUDGMENT. On a code/test conflict the
|
|
21
|
+
# script PAUSES the rebase, records the conflicted files, and exits status:code_conflict
|
|
22
|
+
# — the caller spawns the `merge-conflict-resolver` agent (a fresh, isolated context,
|
|
23
|
+
# NOT the bloated orchestrator) to adjudicate, then re-invokes this script with
|
|
24
|
+
# `--continue`. Everything additive (docs, registries, JSONL) is resolved here,
|
|
25
|
+
# deterministically, with zero model involvement.
|
|
26
|
+
#
|
|
27
|
+
# Usage:
|
|
28
|
+
# merge-worktree.sh --worktree <abs> --manifest <path> [opts] # fresh merge
|
|
29
|
+
# merge-worktree.sh --worktree <abs> --manifest <path> --continue # after agent resolved code conflicts
|
|
30
|
+
#
|
|
31
|
+
# Required:
|
|
32
|
+
# --worktree <abs> absolute worktree path (its registry entry supplies
|
|
33
|
+
# branch / trunk / main / cards unless overridden)
|
|
34
|
+
# --manifest <path> status file written on EVERY exit path (the caller reads
|
|
35
|
+
# THIS — never the bg stdout — and runs its own disk gate)
|
|
36
|
+
#
|
|
37
|
+
# Optional (each falls back to the registry entry / baldart.config.yml / autodetect):
|
|
38
|
+
# --continue resume a paused rebase after the resolver agent ran
|
|
39
|
+
# --branch <branch> feature branch (default: registry entry)
|
|
40
|
+
# --trunk <branch> git.trunk_branch (default: registry / config / autodetect)
|
|
41
|
+
# --main <path> main repo root (default: registry / resolve from worktree)
|
|
42
|
+
# --strategy <pr|local-push> git.merge_strategy (default: registry/config / pr)
|
|
43
|
+
# --skip-checks checksAlreadyPassed — skip pre-merge lint/tsc/build (Phase 3)
|
|
44
|
+
# (the safety commit + post-merge build still run)
|
|
45
|
+
# --metrics-dir <dir> paths.metrics (default: config / docs/metrics)
|
|
46
|
+
# --tc-typecheck <cmd> toolchain typecheck cmd (default: config / npx tsc --noEmit)
|
|
47
|
+
# --tc-lint <cmd> toolchain lint cmd (default: config / npx eslint --max-warnings=0)
|
|
48
|
+
# --tc-build <cmd> toolchain build cmd (default: config / npm run build)
|
|
49
|
+
# --log <path> build/merge log sink (default: /tmp/wt-merge-<slug>.log)
|
|
50
|
+
# --build-timeout <s> hard build timeout sec (default: 600)
|
|
51
|
+
#
|
|
52
|
+
# Status file written (parsed by callers — STABLE contract):
|
|
53
|
+
# status: success | code_conflict | build_fail | sync_needs_decision | error
|
|
54
|
+
# phase: identify | safety | rebase | build | land | sync | cleanup | done
|
|
55
|
+
# error: <message | ->
|
|
56
|
+
# worktree_path: <abs | ->
|
|
57
|
+
# branch: <branch | ->
|
|
58
|
+
# trunk: <branch | ->
|
|
59
|
+
# strategy: <pr | local-push | ->
|
|
60
|
+
# merge_commit: <sha | ->
|
|
61
|
+
# merge_ts: <ISO-8601 | ->
|
|
62
|
+
# conflict_files: <csv | -> (set ONLY on status:code_conflict — the UNRESOLVED code/test files)
|
|
63
|
+
# sync_marker: <none | SYNC-DEFERRED | SYNC-NEEDS-DECISION>
|
|
64
|
+
# sync_detail: <message | ->
|
|
65
|
+
# build_log: <path | ->
|
|
66
|
+
#
|
|
67
|
+
# Exit: 0 ONLY on status:success. Non-zero otherwise (the exit code mirrors the
|
|
68
|
+
# status: 10=code_conflict, 11=build_fail, 12=sync_needs_decision, 1/2=error). The
|
|
69
|
+
# caller NEVER trusts the exit code or the status file as sole evidence — it
|
|
70
|
+
# re-verifies on disk (no residual markers, build green, push landed).
|
|
71
|
+
|
|
72
|
+
set -u
|
|
73
|
+
|
|
74
|
+
err() { printf '%s\n' "$*" >&2; }
|
|
75
|
+
|
|
76
|
+
# --- args ------------------------------------------------------------------
|
|
77
|
+
WORKTREE= MANIFEST= CONTINUE=0 BRANCH= TRUNK= MAIN= STRATEGY= SKIP_CHECKS=0
|
|
78
|
+
METRICS_DIR= TC_TC= TC_LINT= TC_BUILD= LOG= BUILD_TIMEOUT=600
|
|
79
|
+
while [ $# -gt 0 ]; do
|
|
80
|
+
case "$1" in
|
|
81
|
+
--worktree) WORKTREE="${2:-}"; shift 2 ;;
|
|
82
|
+
--manifest) MANIFEST="${2:-}"; shift 2 ;;
|
|
83
|
+
--continue) CONTINUE=1; shift ;;
|
|
84
|
+
--branch) BRANCH="${2:-}"; shift 2 ;;
|
|
85
|
+
--trunk) TRUNK="${2:-}"; shift 2 ;;
|
|
86
|
+
--main) MAIN="${2:-}"; shift 2 ;;
|
|
87
|
+
--strategy) STRATEGY="${2:-}"; shift 2 ;;
|
|
88
|
+
--skip-checks) SKIP_CHECKS=1; shift ;;
|
|
89
|
+
--metrics-dir) METRICS_DIR="${2:-}"; shift 2 ;;
|
|
90
|
+
--tc-typecheck) TC_TC="${2:-}"; shift 2 ;;
|
|
91
|
+
--tc-lint) TC_LINT="${2:-}"; shift 2 ;;
|
|
92
|
+
--tc-build) TC_BUILD="${2:-}"; shift 2 ;;
|
|
93
|
+
--log) LOG="${2:-}"; shift 2 ;;
|
|
94
|
+
--build-timeout) BUILD_TIMEOUT="${2:-}"; shift 2 ;;
|
|
95
|
+
*) err "ERROR: unknown arg: $1"; exit 2 ;;
|
|
96
|
+
esac
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
# --- status writer (called on EVERY exit path) -----------------------------
|
|
100
|
+
M_STATUS="error" M_PHASE="identify" M_ERROR="-" M_MERGE="-" M_MERGETS="-"
|
|
101
|
+
M_CONFLICTS="-" M_SYNCMARK="none" M_SYNCDETAIL="-" M_BLOG="-"
|
|
102
|
+
write_status() {
|
|
103
|
+
[ -n "$MANIFEST" ] || return 0
|
|
104
|
+
{
|
|
105
|
+
printf 'status: %s\n' "$M_STATUS"
|
|
106
|
+
printf 'phase: %s\n' "$M_PHASE"
|
|
107
|
+
printf 'error: %s\n' "$M_ERROR"
|
|
108
|
+
printf 'worktree_path: %s\n' "${WORKTREE:--}"
|
|
109
|
+
printf 'branch: %s\n' "${BRANCH:--}"
|
|
110
|
+
printf 'trunk: %s\n' "${TRUNK:--}"
|
|
111
|
+
printf 'strategy: %s\n' "${STRATEGY:--}"
|
|
112
|
+
printf 'merge_commit: %s\n' "$M_MERGE"
|
|
113
|
+
printf 'merge_ts: %s\n' "$M_MERGETS"
|
|
114
|
+
printf 'conflict_files: %s\n' "$M_CONFLICTS"
|
|
115
|
+
printf 'sync_marker: %s\n' "$M_SYNCMARK"
|
|
116
|
+
printf 'sync_detail: %s\n' "$M_SYNCDETAIL"
|
|
117
|
+
printf 'build_log: %s\n' "$M_BLOG"
|
|
118
|
+
} > "$MANIFEST" 2>/dev/null || true
|
|
119
|
+
}
|
|
120
|
+
fail() { M_STATUS="error"; M_ERROR="$1"; write_status; err "ERROR: $1"; exit "${2:-1}"; }
|
|
121
|
+
emit_conflict() { M_STATUS="code_conflict"; M_PHASE="rebase"; M_CONFLICTS="$1"; M_ERROR="code/test conflict needs judgment — spawn merge-conflict-resolver then re-run with --continue"; write_status; err "CODE_CONFLICT: $1"; exit 10; }
|
|
122
|
+
emit_buildfail(){ M_STATUS="build_fail"; M_ERROR="$1"; M_BLOG="${2:-$LOG}"; write_status; err "BUILD_FAIL: $1"; exit 11; }
|
|
123
|
+
|
|
124
|
+
[ -n "$WORKTREE" ] || { err "ERROR: --worktree is required"; exit 2; }
|
|
125
|
+
[ -n "$MANIFEST" ] || { err "ERROR: --manifest is required"; exit 2; }
|
|
126
|
+
|
|
127
|
+
# --- canonical worktree→main resolver (mirrors setup-worktree.sh resolve_main /
|
|
128
|
+
# SKILL.md step 4c: cd INTO the worktree so `git -C ..` resolves the MAIN repo
|
|
129
|
+
# toplevel; NEVER --git-common-dir+/..). ------------------------------------
|
|
130
|
+
resolve_main_from_wt() {
|
|
131
|
+
(cd "$WORKTREE" 2>/dev/null && git -C .. rev-parse --show-toplevel 2>/dev/null)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# --- is a rebase paused for THIS worktree? For a linked worktree the rebase
|
|
135
|
+
# state lives in $MAIN/.git/worktrees/<name>/rebase-{merge,apply}, NOT in
|
|
136
|
+
# $WORKTREE/.git (which is a gitdir-pointer FILE). --absolute-git-dir
|
|
137
|
+
# returns that per-worktree gitdir. -----------------------------------------
|
|
138
|
+
rebase_in_progress() {
|
|
139
|
+
local gd
|
|
140
|
+
gd="$(git -C "$WORKTREE" rev-parse --absolute-git-dir 2>/dev/null)" || return 1
|
|
141
|
+
[ -d "$gd/rebase-merge" ] || [ -d "$gd/rebase-apply" ]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# --- read a paths.* / git.* scalar from baldart.config.yml ------------------
|
|
145
|
+
config_scalar() {
|
|
146
|
+
local block="$1" key="$2" cfg="$MAIN/baldart.config.yml"
|
|
147
|
+
[ -f "$cfg" ] || return 0
|
|
148
|
+
grep -A60 "^${block}:" "$cfg" 2>/dev/null \
|
|
149
|
+
| grep -m1 "[[:space:]]*${key}:" \
|
|
150
|
+
| sed -E "s/.*${key}:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" \
|
|
151
|
+
| sed -E 's/[[:space:]]+$//'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# --- registry entry field reader (by worktree path) via node ----------------
|
|
155
|
+
REG=""
|
|
156
|
+
registry_field() {
|
|
157
|
+
[ -n "$REG" ] && [ -f "$REG" ] || return 0
|
|
158
|
+
REG_FILE="$REG" R_PATH="$WORKTREE" R_KEY="$1" node -e '
|
|
159
|
+
const fs = require("fs");
|
|
160
|
+
try {
|
|
161
|
+
const j = JSON.parse(fs.readFileSync(process.env.REG_FILE, "utf8"));
|
|
162
|
+
const w = (j.worktrees || []).find(x => x && x.path === process.env.R_PATH);
|
|
163
|
+
if (!w) process.exit(0);
|
|
164
|
+
const v = w[process.env.R_KEY];
|
|
165
|
+
if (v == null) process.exit(0);
|
|
166
|
+
process.stdout.write(Array.isArray(v) ? v.join(",") : String(v));
|
|
167
|
+
} catch (_) {}
|
|
168
|
+
' 2>/dev/null
|
|
169
|
+
}
|
|
170
|
+
registry_remove() {
|
|
171
|
+
[ -n "$REG" ] && [ -f "$REG" ] || return 0
|
|
172
|
+
REG_FILE="$REG" R_PATH="$WORKTREE" node -e '
|
|
173
|
+
const fs = require("fs");
|
|
174
|
+
const f = process.env.REG_FILE;
|
|
175
|
+
try {
|
|
176
|
+
const j = JSON.parse(fs.readFileSync(f, "utf8"));
|
|
177
|
+
if (!Array.isArray(j.worktrees)) process.exit(0);
|
|
178
|
+
j.worktrees = j.worktrees.filter(w => !(w && w.path === process.env.R_PATH));
|
|
179
|
+
const tmp = f + ".tmp";
|
|
180
|
+
fs.writeFileSync(tmp, JSON.stringify(j, null, 2) + "\n");
|
|
181
|
+
fs.renameSync(tmp, f);
|
|
182
|
+
} catch (_) {}
|
|
183
|
+
' 2>>"$LOG"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# --- resolve MAIN (arg → registry → derive from worktree) -------------------
|
|
187
|
+
[ -d "$WORKTREE" ] || fail "worktree path '$WORKTREE' does not exist on disk" 1
|
|
188
|
+
if [ -z "$MAIN" ]; then
|
|
189
|
+
# bootstrap REG from a best-effort main guess so registry_field can read mainRoot
|
|
190
|
+
_guess_main="$(resolve_main_from_wt)"
|
|
191
|
+
if [ -n "$_guess_main" ]; then
|
|
192
|
+
MAIN="$_guess_main"; REG="$MAIN/.worktrees/registry.json"
|
|
193
|
+
_reg_main="$(registry_field mainRoot)"; [ -n "$_reg_main" ] && MAIN="$_reg_main"
|
|
194
|
+
fi
|
|
195
|
+
fi
|
|
196
|
+
[ -n "$MAIN" ] || fail "cannot resolve \$MAIN (not inside a git repo + registry empty)" 1
|
|
197
|
+
[ -d "$MAIN/.git" ] || [ -e "$MAIN/.git" ] || fail "resolved main '$MAIN' is not a git repo" 1
|
|
198
|
+
REG="$MAIN/.worktrees/registry.json"
|
|
199
|
+
|
|
200
|
+
# --- resolve BRANCH (arg → registry → worktree HEAD) ------------------------
|
|
201
|
+
[ -n "$BRANCH" ] || BRANCH="$(registry_field branch)"
|
|
202
|
+
[ -n "$BRANCH" ] || BRANCH="$(git -C "$WORKTREE" rev-parse --abbrev-ref HEAD 2>/dev/null)"
|
|
203
|
+
[ -n "$BRANCH" ] || fail "cannot resolve feature branch for worktree '$WORKTREE'" 1
|
|
204
|
+
|
|
205
|
+
# --- resolve TRUNK (arg → registry → config → origin/HEAD → develop/main/master)
|
|
206
|
+
[ -n "$TRUNK" ] || TRUNK="$(registry_field trunkBranch)"
|
|
207
|
+
[ -n "$TRUNK" ] || TRUNK="$(config_scalar git trunk_branch)"
|
|
208
|
+
[ -n "$TRUNK" ] || TRUNK="$(git -C "$MAIN" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed -E 's#^refs/remotes/origin/##' || true)"
|
|
209
|
+
if [ -z "$TRUNK" ]; then
|
|
210
|
+
for cand in develop main master; do
|
|
211
|
+
if git -C "$MAIN" show-ref --verify --quiet "refs/heads/$cand"; then TRUNK="$cand"; break; fi
|
|
212
|
+
done
|
|
213
|
+
fi
|
|
214
|
+
[ -n "$TRUNK" ] || fail "git.trunk_branch unresolved (arg + registry + config + origin/HEAD + develop/main/master)" 1
|
|
215
|
+
|
|
216
|
+
# --- resolve STRATEGY (arg → config → pr) -----------------------------------
|
|
217
|
+
[ -n "$STRATEGY" ] || STRATEGY="$(config_scalar git merge_strategy)"
|
|
218
|
+
STRATEGY="${STRATEGY:-pr}"
|
|
219
|
+
case "$STRATEGY" in pr|local-push) : ;; *) fail "unknown git.merge_strategy '$STRATEGY' (expected pr|local-push)" 1 ;; esac
|
|
220
|
+
|
|
221
|
+
# --- resolve metrics dir + toolchain commands -------------------------------
|
|
222
|
+
[ -n "$METRICS_DIR" ] || { METRICS_DIR="$(config_scalar paths metrics)"; [ -n "$METRICS_DIR" ] || METRICS_DIR="docs/metrics"; }
|
|
223
|
+
# toolchain.commands.* only when features.has_toolchain: true; else defaults (same _tc as setup-worktree.sh)
|
|
224
|
+
_tc() {
|
|
225
|
+
local cfg="$MAIN/baldart.config.yml" raw
|
|
226
|
+
grep -E '^[[:space:]]*has_toolchain:[[:space:]]*true' "$cfg" >/dev/null 2>&1 || return 0
|
|
227
|
+
raw="$(grep -A20 '^toolchain:' "$cfg" 2>/dev/null | grep -A15 '^[[:space:]]*commands:' \
|
|
228
|
+
| grep -E "^[[:space:]]+$1:" | head -1 \
|
|
229
|
+
| sed -E "s/^[[:space:]]*$1:[[:space:]]*//" \
|
|
230
|
+
| sed -E 's/[[:space:]]+#.*$//; s/[[:space:]]+$//')"
|
|
231
|
+
case "$raw" in \"*\") raw="${raw#\"}"; raw="${raw%\"}" ;; \'*\') raw="${raw#\'}"; raw="${raw%\'}" ;; esac
|
|
232
|
+
case "$raw" in '~'|null|Null|NULL) raw="" ;; esac
|
|
233
|
+
printf '%s' "$raw"
|
|
234
|
+
}
|
|
235
|
+
[ -n "$TC_TC" ] || TC_TC="$(_tc typecheck)"; [ -n "$TC_TC" ] || TC_TC="npx tsc --noEmit"
|
|
236
|
+
[ -n "$TC_LINT" ] || TC_LINT="$(_tc lint)"; [ -n "$TC_LINT" ] || TC_LINT="npx eslint --max-warnings=0 src/"
|
|
237
|
+
BUILD_FROM_CONFIG=1
|
|
238
|
+
[ -n "$TC_BUILD" ] || TC_BUILD="$(_tc build)"
|
|
239
|
+
[ -n "$TC_BUILD" ] || { TC_BUILD="npm run build"; BUILD_FROM_CONFIG=0; }
|
|
240
|
+
|
|
241
|
+
# --- log sink + timeout binary ---------------------------------------------
|
|
242
|
+
SLUG="$(printf '%s' "$BRANCH" | tr '/' '-')"
|
|
243
|
+
[ -n "$LOG" ] || LOG="/tmp/wt-merge-${SLUG}.log"
|
|
244
|
+
[ "$CONTINUE" = 1 ] || : > "$LOG" 2>/dev/null || true
|
|
245
|
+
TIMEOUT_BIN=""
|
|
246
|
+
command -v timeout >/dev/null 2>&1 && TIMEOUT_BIN="timeout"
|
|
247
|
+
[ -z "$TIMEOUT_BIN" ] && command -v gtimeout >/dev/null 2>&1 && TIMEOUT_BIN="gtimeout"
|
|
248
|
+
|
|
249
|
+
# --- portable in-place sed (BSD needs '', GNU does not) ---------------------
|
|
250
|
+
strip_markers() {
|
|
251
|
+
sed -i '' '/^<<<<<<< /d; /^=======$/d; /^>>>>>>> /d' "$1" 2>/dev/null \
|
|
252
|
+
|| sed -i '/^<<<<<<< /d; /^=======$/d; /^>>>>>>> /d' "$1" 2>/dev/null || return 1
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# --- structural validation of a registry-md after auto-strip (SKILL.md:891) -
|
|
256
|
+
validate_structured_md() {
|
|
257
|
+
local f="$1"
|
|
258
|
+
if grep -qE '^(<{7}|={7}|>{7})' "$f"; then return 1; fi
|
|
259
|
+
if awk 'prev~/^\|[ -:|]+\|$/ && $0~/^\|[ -:|]+\|$/{found=1; exit} {prev=$0} END{exit !found}' "$f"; then return 1; fi
|
|
260
|
+
if [ "$(grep -cE '^---[[:space:]]*$' "$f")" -gt 2 ]; then return 1; fi
|
|
261
|
+
return 0
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# --- DETERMINISTIC conflict resolution over the currently-conflicted files.
|
|
265
|
+
# Resolves ADDITIVE classes (docs / structured registries / metrics JSONL)
|
|
266
|
+
# in place + `git add`. Collects code/test/other into UNRESOLVED. Returns
|
|
267
|
+
# the UNRESOLVED list on stdout (empty = all additive, safe to continue).
|
|
268
|
+
# NEVER aborts the rebase — the caller decides (continue vs emit_conflict). -
|
|
269
|
+
resolve_additive_conflicts() {
|
|
270
|
+
local unresolved="" file basename
|
|
271
|
+
local conflicted
|
|
272
|
+
conflicted="$(git -C "$WORKTREE" diff --name-only --diff-filter=U 2>/dev/null)"
|
|
273
|
+
for file in $conflicted; do
|
|
274
|
+
basename="$(basename "$file")"
|
|
275
|
+
# 1. Code / tests — JUDGMENT required, never auto-resolved here.
|
|
276
|
+
case "$file" in
|
|
277
|
+
src/*.ts|src/*.tsx|src/*.js|src/*.jsx|*.test.*|*.spec.*)
|
|
278
|
+
unresolved="$unresolved $file"; continue ;;
|
|
279
|
+
esac
|
|
280
|
+
# 2. Structured registries — strip + structural validation.
|
|
281
|
+
case "$basename" in
|
|
282
|
+
project-status.md|ssot-registry.md|traceability-matrix.md|REGISTRY.md)
|
|
283
|
+
if strip_markers "$WORKTREE/$file" && validate_structured_md "$WORKTREE/$file"; then
|
|
284
|
+
git -C "$WORKTREE" add "$file" 2>>"$LOG"; continue
|
|
285
|
+
else
|
|
286
|
+
unresolved="$unresolved $file"; continue # malformed after strip → needs judgment
|
|
287
|
+
fi ;;
|
|
288
|
+
field-registry.*)
|
|
289
|
+
unresolved="$unresolved $file"; continue ;; # strict JSON — cannot auto-strip
|
|
290
|
+
esac
|
|
291
|
+
# 3. JSONL — only auto-resolve under $METRICS_DIR/ (line-oriented, additive).
|
|
292
|
+
case "$file" in
|
|
293
|
+
"$METRICS_DIR"/*.jsonl)
|
|
294
|
+
strip_markers "$WORKTREE/$file" && git -C "$WORKTREE" add "$file" 2>>"$LOG"; continue ;;
|
|
295
|
+
*.jsonl)
|
|
296
|
+
unresolved="$unresolved $file"; continue ;; # fixtures/seeds — may corrupt
|
|
297
|
+
esac
|
|
298
|
+
# 4. Generic docs/config (.md/.json/.yml outside src/) — additive merge.
|
|
299
|
+
case "$file" in
|
|
300
|
+
*.md|*.json|*.yml|*.yaml)
|
|
301
|
+
strip_markers "$WORKTREE/$file" && git -C "$WORKTREE" add "$file" 2>>"$LOG"; continue ;;
|
|
302
|
+
*)
|
|
303
|
+
unresolved="$unresolved $file"; continue ;; # unknown binary/other → judgment
|
|
304
|
+
esac
|
|
305
|
+
done
|
|
306
|
+
printf '%s' "$(printf '%s' "$unresolved" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+/,/g')"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# --- run the rebase loop: resolve additive each round; on code conflict that
|
|
310
|
+
# survives, emit_conflict (PAUSED on disk for the resolver agent). --------
|
|
311
|
+
rebase_loop() {
|
|
312
|
+
# $1 = "start" | "continue"
|
|
313
|
+
local mode="$1" rc unresolved
|
|
314
|
+
M_PHASE="rebase"
|
|
315
|
+
if [ "$mode" = "start" ]; then
|
|
316
|
+
git -C "$WORKTREE" fetch origin "$TRUNK" --quiet 2>>"$LOG" || err "WARN: fetch origin $TRUNK failed — rebasing onto possibly-stale ref"
|
|
317
|
+
git -C "$WORKTREE" rebase "origin/$TRUNK" >>"$LOG" 2>&1; rc=$?
|
|
318
|
+
else
|
|
319
|
+
# resolver agent already `git add`-ed its code resolutions; continue the paused rebase
|
|
320
|
+
git -C "$WORKTREE" -c core.editor=true rebase --continue >>"$LOG" 2>&1; rc=$?
|
|
321
|
+
fi
|
|
322
|
+
while [ "$rc" -ne 0 ]; do
|
|
323
|
+
# Are we actually mid-rebase with conflicts, or a real error?
|
|
324
|
+
if ! rebase_in_progress \
|
|
325
|
+
&& [ -z "$(git -C "$WORKTREE" diff --name-only --diff-filter=U 2>/dev/null)" ]; then
|
|
326
|
+
fail "rebase failed without conflicts (see $LOG)" 1
|
|
327
|
+
fi
|
|
328
|
+
unresolved="$(resolve_additive_conflicts)"
|
|
329
|
+
if [ -n "$unresolved" ]; then
|
|
330
|
+
emit_conflict "$unresolved" # PAUSE: hand the code/test files to the resolver agent
|
|
331
|
+
fi
|
|
332
|
+
# all conflicts this round were additive → continue
|
|
333
|
+
git -C "$WORKTREE" -c core.editor=true rebase --continue >>"$LOG" 2>&1; rc=$?
|
|
334
|
+
done
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# ===========================================================================
|
|
338
|
+
# 0. Guards
|
|
339
|
+
if git -C "$WORKTREE" rev-parse --git-dir >/dev/null 2>&1; then :; else fail "'$WORKTREE' is not a git worktree" 1; fi
|
|
340
|
+
|
|
341
|
+
# Provisional NON-terminal status — written BEFORE any mutation so a KILL mid-rebase
|
|
342
|
+
# or mid-build never leaves a stale 'success' on disk (mirrors setup-worktree.sh's
|
|
343
|
+
# provisional manifest). The caller's disk gate treats anything that is NOT
|
|
344
|
+
# 'status: success' as not-merged → fallback, never a false pass.
|
|
345
|
+
M_STATUS="error"; M_ERROR="merge incomplete (interrupted before completion)"
|
|
346
|
+
write_status
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
# CONTINUE path: a resolver agent resolved the code conflicts + git-add'd them.
|
|
350
|
+
# Resume the paused rebase, then fall through to build/land/sync/cleanup.
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
if [ "$CONTINUE" = 1 ]; then
|
|
353
|
+
if ! rebase_in_progress; then
|
|
354
|
+
err "WARN: --continue but no rebase in progress — assuming already past rebase; proceeding to build/land."
|
|
355
|
+
else
|
|
356
|
+
# Any UNRESOLVED conflict still on disk means the agent left work undone → re-emit.
|
|
357
|
+
still="$(git -C "$WORKTREE" diff --name-only --diff-filter=U 2>/dev/null | tr '\n' ',' | sed 's/,$//')"
|
|
358
|
+
[ -z "$still" ] || emit_conflict "$still"
|
|
359
|
+
rebase_loop continue
|
|
360
|
+
fi
|
|
361
|
+
else
|
|
362
|
+
# -------------------------------------------------------------------------
|
|
363
|
+
# FRESH path: safety commit → (checks) → rebase
|
|
364
|
+
# -------------------------------------------------------------------------
|
|
365
|
+
# 1. Safety commit (ALWAYS — never skippable; files lost after rebase are unrecoverable)
|
|
366
|
+
M_PHASE="safety"
|
|
367
|
+
CHANGED="$(git -C "$WORKTREE" diff --name-only HEAD 2>/dev/null)"
|
|
368
|
+
UNTRACKED="$(git -C "$WORKTREE" ls-files --others --exclude-standard 2>/dev/null)"
|
|
369
|
+
if [ -n "$CHANGED" ] || [ -n "$UNTRACKED" ]; then
|
|
370
|
+
for f in $CHANGED $UNTRACKED; do git -C "$WORKTREE" add "$f" 2>>"$LOG"; done
|
|
371
|
+
git -C "$WORKTREE" commit -m "[safety] Auto-commit uncommitted files before merge
|
|
372
|
+
|
|
373
|
+
Files were uncommitted when merge-worktree.sh started. This safety commit
|
|
374
|
+
prevents file loss during rebase.
|
|
375
|
+
|
|
376
|
+
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" --quiet >>"$LOG" 2>&1 \
|
|
377
|
+
|| err "WARN: safety commit produced no commit (nothing staged?)"
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# 2. Pre-merge checks (skipped when --skip-checks / checksAlreadyPassed)
|
|
381
|
+
if [ "$SKIP_CHECKS" = 0 ]; then
|
|
382
|
+
M_PHASE="build"
|
|
383
|
+
CHANGED_TS="$(git -C "$WORKTREE" diff --name-only "origin/$TRUNK" -- '*.ts' '*.tsx' 2>/dev/null | tr '\n' ' ')"
|
|
384
|
+
( cd "$WORKTREE" && CI=1 bash -c "${TC_LINT} ${CHANGED_TS}" </dev/null >>"$LOG" 2>&1 ) || err "WARN: pre-merge lint failed (see $LOG)"
|
|
385
|
+
( cd "$WORKTREE" && CI=1 bash -c "$TC_TC" </dev/null >>"$LOG" 2>&1 ) || err "WARN: pre-merge typecheck failed (see $LOG)"
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
# 3. Rebase onto origin/$TRUNK (deterministic additive resolution; pause on code)
|
|
389
|
+
rebase_loop start
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
# ===========================================================================
|
|
393
|
+
# Past the rebase (clean, or all-additive-resolved, or agent-resolved+continued).
|
|
394
|
+
# 4. Build verification (toolchain-aware, hard timeout). SKIP tier honored.
|
|
395
|
+
M_PHASE="build"
|
|
396
|
+
RUN_BUILD=1
|
|
397
|
+
if [ "$BUILD_FROM_CONFIG" = 0 ] && [ -f "$WORKTREE/package.json" ]; then
|
|
398
|
+
( cd "$WORKTREE" && node -e 'const s=(require("./package.json").scripts)||{};process.exit(s.build?0:1)' 2>/dev/null ) \
|
|
399
|
+
|| { RUN_BUILD=0; err "… post-rebase build SKIPPED (no toolchain build cmd + no 'build' script → no-build project)"; }
|
|
400
|
+
fi
|
|
401
|
+
if [ "$RUN_BUILD" = 1 ]; then
|
|
402
|
+
err "… post-rebase build ($TC_BUILD, timeout ${BUILD_TIMEOUT}s)"
|
|
403
|
+
if [ -n "$TIMEOUT_BIN" ]; then
|
|
404
|
+
( cd "$WORKTREE" && CI=1 "$TIMEOUT_BIN" "$BUILD_TIMEOUT" bash -c "$TC_BUILD" </dev/null >>"$LOG" 2>&1 ); BUILD_RC=$?
|
|
405
|
+
else
|
|
406
|
+
err "WARN: no timeout binary — running build unbounded."
|
|
407
|
+
( cd "$WORKTREE" && CI=1 bash -c "$TC_BUILD" </dev/null >>"$LOG" 2>&1 ); BUILD_RC=$?
|
|
408
|
+
fi
|
|
409
|
+
if [ "$BUILD_RC" -eq 124 ] && [ -n "$TIMEOUT_BIN" ]; then
|
|
410
|
+
emit_buildfail "post-rebase build TIMED OUT after ${BUILD_TIMEOUT}s — partial log at $LOG" "$LOG"
|
|
411
|
+
elif [ "$BUILD_RC" -ne 0 ]; then
|
|
412
|
+
emit_buildfail "post-rebase build FAILED — the rebase introduced an incompatibility; see $LOG" "$LOG"
|
|
413
|
+
fi
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
# 5. Land the feature branch onto $TRUNK via the configured strategy.
|
|
417
|
+
M_PHASE="land"
|
|
418
|
+
if [ "$STRATEGY" = "local-push" ]; then
|
|
419
|
+
git -C "$WORKTREE" fetch origin "$TRUNK" --quiet 2>>"$LOG" || true
|
|
420
|
+
BEHIND="$(git -C "$WORKTREE" rev-list --count "$BRANCH..origin/$TRUNK" 2>/dev/null || echo 0)"
|
|
421
|
+
AHEAD="$(git -C "$WORKTREE" rev-list --count "origin/$TRUNK..$BRANCH" 2>/dev/null || echo 0)"
|
|
422
|
+
if [ "${BEHIND:-0}" -ne 0 ]; then
|
|
423
|
+
fail "local-push aborted: $BRANCH is $BEHIND commit(s) behind origin/$TRUNK (re-run rebase)" 1
|
|
424
|
+
fi
|
|
425
|
+
if [ "${AHEAD:-0}" -eq 0 ]; then
|
|
426
|
+
err "ℹ️ nothing to push — $BRANCH == origin/$TRUNK"
|
|
427
|
+
else
|
|
428
|
+
if ! PUSH_OUT="$(git -C "$WORKTREE" push origin "$BRANCH:$TRUNK" 2>&1)"; then
|
|
429
|
+
err "$PUSH_OUT"
|
|
430
|
+
if printf '%s' "$PUSH_OUT" | grep -qiE "protected branch|required status|review required"; then
|
|
431
|
+
fail "local-push rejected: '$TRUNK' is protected on origin — switch git.merge_strategy: pr" 1
|
|
432
|
+
fi
|
|
433
|
+
fail "local-push failed (see above)" 1
|
|
434
|
+
fi
|
|
435
|
+
fi
|
|
436
|
+
git -C "$WORKTREE" push origin --delete "$BRANCH" 2>/dev/null || true
|
|
437
|
+
M_MERGE="$(git -C "$WORKTREE" rev-parse HEAD 2>/dev/null)"
|
|
438
|
+
else
|
|
439
|
+
# pr strategy — requires gh
|
|
440
|
+
command -v gh >/dev/null 2>&1 || fail "git.merge_strategy=pr but 'gh' CLI is not installed (brew install gh; gh auth login)" 1
|
|
441
|
+
git -C "$WORKTREE" push --force-with-lease origin "$BRANCH" >>"$LOG" 2>&1 || fail "failed to push feature branch '$BRANCH' to origin" 1
|
|
442
|
+
REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null)"
|
|
443
|
+
PR_NUMBER="$(gh -R "$REPO" pr list --head "$BRANCH" --state open --json number -q '.[0].number' 2>/dev/null || true)"
|
|
444
|
+
if [ -z "$PR_NUMBER" ]; then
|
|
445
|
+
PR_URL="$(gh pr create --base "$TRUNK" --head "$BRANCH" \
|
|
446
|
+
--title "[$BRANCH] $(printf '%s' "$BRANCH" | sed 's|.*/||')" \
|
|
447
|
+
--body "Auto-merged by merge-worktree.sh after final review + QA passed." --repo "$REPO" 2>>"$LOG")" \
|
|
448
|
+
|| fail "gh pr create failed (see $LOG)" 1
|
|
449
|
+
PR_NUMBER="$(printf '%s' "$PR_URL" | grep -oE '[0-9]+$')"
|
|
450
|
+
fi
|
|
451
|
+
gh pr merge "$PR_NUMBER" --merge --delete-branch >>"$LOG" 2>&1 || true
|
|
452
|
+
PR_STATE="$(gh pr view "$PR_NUMBER" --json state -q .state 2>/dev/null)"
|
|
453
|
+
if [ "$PR_STATE" != "MERGED" ]; then
|
|
454
|
+
gh api -X PUT "repos/$REPO/pulls/$PR_NUMBER/merge" -f merge_method=merge \
|
|
455
|
+
-f commit_title="[$BRANCH] Merge $BRANCH into $TRUNK" >>"$LOG" 2>&1 || true
|
|
456
|
+
PR_STATE="$(gh pr view "$PR_NUMBER" --json state -q .state 2>/dev/null)"
|
|
457
|
+
fi
|
|
458
|
+
[ "$PR_STATE" = "MERGED" ] || fail "PR #$PR_NUMBER did not merge — inspect manually (see $LOG)" 1
|
|
459
|
+
M_MERGE="$(gh pr view "$PR_NUMBER" --json mergeCommit -q .mergeCommit.oid 2>/dev/null)"
|
|
460
|
+
[ -n "$M_MERGE" ] && [ "$M_MERGE" != "null" ] || M_MERGE="$(git -C "$WORKTREE" rev-parse HEAD 2>/dev/null)"
|
|
461
|
+
fi
|
|
462
|
+
M_MERGETS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
463
|
+
|
|
464
|
+
# 6. Sync local trunk ref — record the marker in the STATUS FILE (callers read
|
|
465
|
+
# it from there, NOT from stdout). NEVER `git checkout $TRUNK` on the main repo.
|
|
466
|
+
M_PHASE="sync"
|
|
467
|
+
CURRENT_BRANCH="$(git -C "$MAIN" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
|
|
468
|
+
if [ "$CURRENT_BRANCH" = "$TRUNK" ]; then
|
|
469
|
+
if git -C "$MAIN" pull --ff-only origin "$TRUNK" >>"$LOG" 2>&1; then
|
|
470
|
+
M_SYNCMARK="none"; M_SYNCDETAIL="-"
|
|
471
|
+
else
|
|
472
|
+
METRICS_LOG="$METRICS_DIR/skill-runs.jsonl"
|
|
473
|
+
DIRTY="$(git -C "$MAIN" status --porcelain --untracked-files=no | sed 's/^...//')"
|
|
474
|
+
FOREIGN="" OWNED=""
|
|
475
|
+
for f in $DIRTY; do
|
|
476
|
+
case "$f" in
|
|
477
|
+
"$METRICS_LOG") OWNED="$OWNED $f" ;;
|
|
478
|
+
.baldart/generated/*|.baldart/state.json|.baldart/skill-conflicts.json) OWNED="$OWNED $f" ;;
|
|
479
|
+
*) FOREIGN="$FOREIGN $f" ;;
|
|
480
|
+
esac
|
|
481
|
+
done
|
|
482
|
+
if [ -n "$FOREIGN" ]; then
|
|
483
|
+
M_SYNCMARK="SYNC-NEEDS-DECISION"; M_SYNCDETAIL="main repo $TRUNK cannot fast-forward; foreign uncommitted files:${FOREIGN}"
|
|
484
|
+
elif [ -n "$OWNED" ]; then
|
|
485
|
+
git -C "$MAIN" add $OWNED 2>>"$LOG"
|
|
486
|
+
git -C "$MAIN" commit -m "chore(baldart): reconcile framework-managed artifacts before $TRUNK sync" --quiet >>"$LOG" 2>&1
|
|
487
|
+
if git -C "$MAIN" pull --rebase origin "$TRUNK" >>"$LOG" 2>&1; then
|
|
488
|
+
git -C "$MAIN" push origin "$TRUNK" >>"$LOG" 2>&1 || true
|
|
489
|
+
M_SYNCMARK="none"; M_SYNCDETAIL="reconciled framework-managed artifacts"
|
|
490
|
+
else
|
|
491
|
+
git -C "$MAIN" rebase --abort 2>/dev/null || true
|
|
492
|
+
M_SYNCMARK="SYNC-NEEDS-DECISION"; M_SYNCDETAIL="framework-managed reconcile rebase conflict on $TRUNK"
|
|
493
|
+
fi
|
|
494
|
+
else
|
|
495
|
+
M_SYNCMARK="SYNC-NEEDS-DECISION"; M_SYNCDETAIL="main repo $TRUNK cannot fast-forward and the working tree is clean (diverged?)"
|
|
496
|
+
fi
|
|
497
|
+
fi
|
|
498
|
+
else
|
|
499
|
+
git -C "$MAIN" fetch origin "$TRUNK" --quiet 2>>"$LOG" || true
|
|
500
|
+
M_SYNCMARK="SYNC-DEFERRED"; M_SYNCDETAIL="main repo HEAD=${CURRENT_BRANCH}, local $TRUNK not fast-forwarded"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
# 7. Cleanup worktree + registry (only after a verified land). Individual cmds.
|
|
504
|
+
M_PHASE="cleanup"
|
|
505
|
+
git -C "$MAIN" worktree remove "$WORKTREE" --force >>"$LOG" 2>&1 || err "WARN: git worktree remove failed (see $LOG)"
|
|
506
|
+
git -C "$MAIN" branch -d "$BRANCH" >>"$LOG" 2>&1 || err "WARN: local branch -d '$BRANCH' failed (already gone / not merged?)"
|
|
507
|
+
git -C "$MAIN" worktree prune >>"$LOG" 2>&1 || true
|
|
508
|
+
registry_remove
|
|
509
|
+
# release backlog-ID reservations (best-effort; high-water mark stays monotonic)
|
|
510
|
+
ALLOC_SH="$MAIN/.framework/framework/.claude/skills/worktree-manager/scripts/allocate-id.sh"
|
|
511
|
+
[ -f "$ALLOC_SH" ] || ALLOC_SH="$MAIN/.claude/skills/worktree-manager/scripts/allocate-id.sh"
|
|
512
|
+
[ -f "$ALLOC_SH" ] && bash "$ALLOC_SH" release "$WORKTREE" 2>/dev/null || true
|
|
513
|
+
|
|
514
|
+
# 8. Done.
|
|
515
|
+
M_STATUS="success"; M_PHASE="done"; M_ERROR="-"; M_BLOG="-"
|
|
516
|
+
write_status
|
|
517
|
+
err "✓ merged: $BRANCH → $TRUNK ($STRATEGY, $M_MERGE)"
|
|
518
|
+
exit 0
|