baldart 4.62.0 → 4.64.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 +40 -0
- package/README.md +2 -1
- package/VERSION +1 -1
- package/framework/.claude/agents/REGISTRY.md +3 -0
- package/framework/.claude/agents/coder.md +2 -1
- package/framework/.claude/agents/merge-conflict-resolver.md +58 -0
- package/framework/.claude/agents/prd-card-writer.md +6 -4
- package/framework/.claude/skills/new/SKILL.md +6 -2
- package/framework/.claude/skills/new/references/completeness.md +1 -1
- package/framework/.claude/skills/new/references/implement.md +1 -0
- package/framework/.claude/skills/new/references/merge-cleanup.md +45 -24
- package/framework/.claude/skills/new2/SKILL.md +7 -1
- package/framework/.claude/skills/prd/references/prd-writing-phase.md +7 -0
- 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/i18n-protocol.md +9 -0
- package/package.json +1 -1
- package/src/commands/doctor.js +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ 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.64.0] - 2026-06-23
|
|
9
|
+
|
|
10
|
+
**Off-context final-merge conflict resolution — from a real heavy-drift `/mw` on `/new`.** On a long epic batch the trunk had drifted 45 commits while the batch ran; the final `/mw` (invoked via `Skill()`, so it runs IN the orchestrator's context) hit mixed conflicts and resolved them **inline** — Read/grep/sed/python churn re-reading the giant end-of-batch cache, exactly when tokens cost the most (the `~20M-token` hand-merge regression `merge-cleanup.md` already measured). This release moves the merge off that context the right way, without repeating the v4.46.0/v4.53.0 "model-in-the-loop for plumbing" mistake.
|
|
11
|
+
|
|
12
|
+
**MINOR** — adds one agent + one SSOT script; opt-in, additive, runtime-detected, with the legacy `Skill(/mw)` path kept as the fallback. **No new `baldart.config.yml` key** → the schema-change propagation rule does NOT apply.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`scripts/merge-worktree.sh` — the deterministic SSOT for the whole merge** (the merge analogue of `setup-worktree.sh`), consumed identically by `/mw`, `/new` Phase 6, and `new2`. It runs the entire sequence — safety commit → rebase onto `origin/$TRUNK` → **deterministic** resolution of the ADDITIVE conflict classes (structured registries with `validate_structured_md`, metrics JSONL, generic docs/config) → toolchain-aware build (hard timeout, SKIP tier) → land via `git.merge_strategy` (`pr`/`local-push`) → local-trunk sync (markers written to the **status file**, not stdout) → worktree cleanup → registry removal. It writes a structured status file on **every** exit path (`success | code_conflict | build_fail | sync_needs_decision | error`) with a **provisional non-terminal** status written before any mutation, so a kill mid-rebase/mid-build never leaves a stale `success` (no false-pass). The ONE thing it never does is resolve a **code** conflict (irreducible judgment): it pauses the rebase, records `conflict_files`, and exits `code_conflict` for the resolver. "A deterministic script cannot fabricate or stall."
|
|
17
|
+
- **`merge-conflict-resolver` agent (Sonnet, effort medium)** — the judgment layer, spawned by `/new` Phase 6 / `/mw` **only** on `status:code_conflict`. It classifies each paused code/test hunk **additive** (distinct imports/decls/rows, colliding index → renumber → resolve, build-verified) vs **semantic** (same logic/value/signature → STOP, never guess), then re-invokes `merge-worktree.sh --continue` (looping per replayed commit) to land — all in a **fresh, isolated context** so the conflict churn never re-enters the orchestrator. It is the ONE bounded exception to `coder`'s "branches off-limits" rule (worktree-scoped, merge phase only) and returns COMPACT (a one-line `MERGED <sha>` / `STOP: <file>:<hunk> semantic` + `path:line`). The orchestrator runs an **anti-fabrication disk gate** on its return (no residual markers + `status:success`) — "came to rest ≠ done", never trusts the prose.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **`/new` Phase 6 launches the script as a BACKGROUND Bash and reads only the small status file** (`merge-cleanup.md`), instead of `Skill(/mw)` inline — so the orchestrator's in-context cost is one launch + one status read, with the conflict churn entirely off-context (deterministic in the script, judgment in the resolver subagent). The HARD "the orchestrator never hand-merges" rule stays (the script is a deterministic delegate, the resolver a subagent); `code_conflict` → spawn the resolver; `build_fail`/semantic-STOP/`error` → interactive `AskUserQuestion` / autonomous follow-up; **script absent OR `Task` tool unavailable (Codex / older subtree) → fall back to the legacy `Skill(/mw)` inline** (zero regression). Sync markers are transcribed from the status file into the tracker so Phase 6c's hygiene gate is unchanged.
|
|
22
|
+
- **`/mw` (worktree-manager skill) now consumes `merge-worktree.sh`** (new step 1b) the same way `/nw` consumes `setup-worktree.sh`; the inline steps 2–7 remain as the human-readable SSOT the script mirrors and the fallback for older subtrees.
|
|
23
|
+
- **`new2` merge phase runs the deterministic script** instead of the model-in-the-loop "/mw programmatic, run git yourself" prose — removing the model from the merge plumbing. Honoring its OPS/GIT role boundary (never edits source, F-030), a `code_conflict` is left+reported → tracked as a merge blocker → follow-up (behavior-preserving vs today's `/mw`-STOP); the full off-context resolver is wired on the classic `/new` path. `MERGE_SCHEMA` gains `status` + `conflictFiles`.
|
|
24
|
+
- **`new-session-audit.mjs` + `/new-audit`** gain a **merge-phase telemetry detector** (turns/cache_read between the merge launch and Phase 6b + `code_conflict_hit` YES/NO) — non-blocking, to measure ex-post that the off-context move reduced Phase 6 cost and to gauge real conflict frequency.
|
|
25
|
+
|
|
26
|
+
## [4.63.0] - 2026-06-23
|
|
27
|
+
|
|
28
|
+
**Fixes from the FEAT-0042 post-mortem — the first real `/new` run on v4.62.0.** v4.62.0 worked (that run billed −29% vs the FEAT-0041 baseline, 1 unplanned fix-coder vs 4, `/mw` and the durable tracker held), but the deep post-mortem surfaced a new class of bug and two real leaks — including one *introduced by* v4.62.0 itself.
|
|
29
|
+
|
|
30
|
+
**MINOR** — adds a `doctor` diagnostic + guidance/agent edits; no layout/install change, **no new `baldart.config.yml` key**.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **CLI ↔ framework version-skew silently disabled the Codex broker teardown (the v4.62.0 leak)** — `merge-cleanup.md` / `new/SKILL.md` / `new2/SKILL.md` + `src/commands/doctor.js`. The framework prose ships via the git subtree (current), but `npx baldart` resolves the SEPARATELY-installed CLI; on a consumer whose global CLI predated v4.62.0, `npx baldart teardown-codex-broker` returned `unknown command`, which `2>/dev/null || true` masked as a no-op → the Codex broker + its Playwright/… MCP children survived every run (FEAT-0042: 5 brokers leaked). The teardown snippets now **capture stderr (no masking), detect `unknown command`, warn to `npm i -g baldart@latest`, fall back to a direct `pkill` of the broker by cwd, and assert the broker is actually gone**. `baldart doctor` gains a **CLI↔framework version-skew check** (warns when the installed CLI is older than the framework, so new `npx baldart` subcommands can't silently fail). The earlier `$MAIN`-non-persistence hypothesis was **refuted** by the post-mortem's adversarial verification — the run passed the correct absolute `--cwd`.
|
|
35
|
+
- **Fix-pass coder could edit the live MAIN repo (worktree-isolation gap)** — `coder.md` + `completeness.md`. The reactive AC-fix-pass spawn had no canonical worktree-isolation clause (it lived only in `implement.md`'s per-card brief), so an improvised thinner brief dropped it and a fix-coder Edited `<main>/src/lib/i18n/*.ts` on the MAIN repo, then reverted + re-applied (~3M wasted; the `tsc 2353` it triggered was a stale artifact, not a real defect). Now: `coder.md` STEP 0 carries an **intrinsic per-Edit path guard** (every Edit path must be worktree-rooted; `cd` is not enough; assert `.worktrees/` before editing shared/locale files) — applies to every spawn even when the brief omits it — and the fix-pass spawn brief must carry the verbatim isolation clause.
|
|
36
|
+
- **i18n locale-parity gate ↔ coder "do-not-translate" contradiction** — `implement.md` / `coder.md` / `i18n-protocol.md` / `prd-card-writer.md`. The `coder` writes the source locale only (by design), but a project's own locale-parity test (a `test`/`build` gate) fails on a source-only commit → `/new` spawned `i18n-translator` reactively (~2M round-trip). Resolved: a **planned per-card `i18n-translator` fill pass** runs after the coder and before the parity gate (when `has_i18n` + new source keys), a parity failure is explicitly an `i18n-translator` task (never a coder retry), and the ownership docs (coder STEP 9.6, the i18n cascade, the P3 DoD line) are reconciled so the coder stays source-only with no contradiction.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **`new-session-audit.mjs` detector false-positives fixed (audit tooling).** `B1_mw_delegation` now inspects the actual `Skill` tool-call inputs (not the raw transcript, which quotes `new:new`/`__phase6_merge__` as the merge-cleanup.md "don't do this" text → a false FAIL even when the `/mw` call was correct); `B3_nul_grep` matches only genuine output phrases (`Binary file … matches`, `graphify-out`), not the v4.62.0 guidance text; in-context-merge detection is scoped to Bash commands; and command/mode detection now selects the actual `/new` line (not a later `/model`).
|
|
41
|
+
- **Tracker-write cadence coarsened (O2/M3).** The v4.62.0 "lone tracker-Edit = violation" prose did not change behavior (a measured run still paid 38 standalone tracker-Edit turns ≈13.6M / 9%), so `SKILL.md` § Update rules now mandates tracker writes only at **card-state-changing boundaries**, always co-emitted with that action — recovery reconstructs the phase from commits + `phase_module_loaded` + backlog statuses, so per-phase writes are not needed.
|
|
42
|
+
- **markdownlint debt prevented at `/prd` authoring (L1/L2)** — `prd-writing-phase.md` gains a markdown-hygiene rule: escape a literal `|` in a prose/table cell (`\|` or a code span — the `isManagerOf||isOwner` incident), honor MD058, and run `markdownlint` on generated docs when configured, rather than shipping debt for `/new`'s doc-review to catch.
|
|
43
|
+
|
|
44
|
+
### Deferred (documented)
|
|
45
|
+
|
|
46
|
+
- **Codex sentinel-JSON prompt tax (~270k/batch)** — `new-card-review.js` already tolerates Codex's prose output (it fetches both the sentinel JSON and the prose tail), the gain is ~0.1%, and the Codex relay is a delicate, much-iterated subsystem; not worth a regression risk in this sweep.
|
|
47
|
+
|
|
8
48
|
## [4.62.0] - 2026-06-22
|
|
9
49
|
|
|
10
50
|
**Token-economy + reliability hardening for `/new` and `/prd`, distilled from a real `/new FEAT-0041 -full -auto` run that billed ~364M tokens (~95% cache_read).** A deep multi-agent post-mortem (with adversarial verification that refuted several plausible-but-wrong hypotheses — e.g. the workflow-`<result>`-payload-bloat theory was false by ~430×) traced the cost to a small set of root causes: in-context work that should have been off-context (a hallucinated `/mw` delegation and a killed-final-review recovery, ~36M combined), friction multipliers (a NUL-byte/GNU-grep trap ~8–12M, ~28 lone tracker-Edit turns ~11M), and `/prd` design gaps that pushed work downstream into `/new`'s expensive orchestrator context. Plus a proactive Codex-broker teardown so terminated reviews stop leaking MCP processes.
|
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ No additional activation steps needed — once installed, Claude Code (and Codex
|
|
|
85
85
|
- **agents/**: 25 domain modules (architecture, workflows, testing, security, card-schema, i18n-protocol, return-contract-protocol, etc.)
|
|
86
86
|
- **Routing**: If you touch X, read Y - minimize context loading
|
|
87
87
|
|
|
88
|
-
### AI Agents (
|
|
88
|
+
### AI Agents (31 specialized agents)
|
|
89
89
|
|
|
90
90
|
**Core (required for every project)**
|
|
91
91
|
1. **codebase-architect**: MANDATORY before planning/implementation - understands codebase structure
|
|
@@ -123,6 +123,7 @@ No additional activation steps needed — once installed, Claude Code (and Codex
|
|
|
123
123
|
27. **deep-human-insight**: Psychological / sociological analysis for B2C UX and adoption
|
|
124
124
|
28. **skill-improver**: Weekly auto-improvement of skills/agents based on review/QA findings
|
|
125
125
|
29. **i18n-translator** (v4.52.0): Context-aware label translation into native locale files (Sonnet, low-effort, flag-not-guess) — invoked by `/i18n` + the `i18n-align` routine
|
|
126
|
+
30. **merge-conflict-resolver** (v4.64.0): Resolves the final-merge **code** conflicts OFF the orchestrator context — auto-spawned by `/new` Phase 6 / `/mw` only when the deterministic `merge-worktree.sh` pauses on a code/test conflict. Adjudicates additive-vs-semantic hunks (semantic → STOP), then lets the script land. Runs in a fresh isolated context so the conflict churn never re-enters the bloated end-of-batch orchestrator
|
|
126
127
|
|
|
127
128
|
REGISTRY.md is the single source of truth for agent routing and capabilities.
|
|
128
129
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.64.0
|
|
@@ -21,6 +21,7 @@ Quick-reference for all custom agents. Use this to route tasks to the right spec
|
|
|
21
21
|
| **code-reviewer** | Code | Review code post-implementation for bugs/quality | Security analysis, code quality, registry-first UI compliance (when `features.has_design_system: true`) — flags re-implemented primitives and bypassed tokens as HIGH per `framework/agents/design-system-protocol.md`. NOT a visual-design evaluator (that is `ui-expert` / `visual-fidelity-verifier`). | No (review-only role — emits findings; fixes are applied by `coder`. The `bypassPermissions` tool-mode all agents run under is execution access, not an implementer role) | Static analysis, security audit |
|
|
22
22
|
| **security-reviewer** | Code | Review security-sensitive code, configs, auth, secrets, and infra changes | AppSec audit, threat modeling, hardening guidance | No | Security review, trust-boundary analysis |
|
|
23
23
|
| **qa-sentinel** | QA | **Mechanical gate runner** — lint, typecheck, test suite, build, security audit, markdownlint. Returns PASS/FAIL verdict only. Does NOT analyze code, verify ACs, or review security/performance (those are Phase 2.5 and code-reviewer responsibilities). | Gate execution, verdicts | No (reports failures, coder fixes) | ESLint, tsc, node --test, npm run build, npm audit, markdownlint |
|
|
24
|
+
| **merge-conflict-resolver** | DevOps | Resolve the final-merge **code** conflicts OFF the orchestrator context. Auto-spawned by `/new` Phase 6 (and standalone `/mw`) ONLY when the deterministic `scripts/merge-worktree.sh` pauses with `status:code_conflict` — the script already resolved every additive doc/registry/JSONL conflict; this agent adjudicates the irreducible code/test hunks then lets the script land. Runs in a fresh isolated context so the conflict churn never re-enters the bloated end-of-batch orchestrator. Never spawned ad-hoc. | Additive-vs-semantic hunk judgment (semantic → STOP), `merge-worktree.sh --continue` rebase loop, anti-fabrication disk gate, COMPACT return | Yes — the **ONE bounded exception** to coder's "branches off-limits" rule: worktree-scoped, merge phase only, post-checks-passed; lands via the script, never hand-rolled git | git conflict resolution, `merge-worktree.sh --continue` |
|
|
24
25
|
| **hybrid-ml-architect** | ML | Design/implement ML systems end-to-end | Recommender, ranking, embeddings | Yes | Model design, evaluation, monitoring |
|
|
25
26
|
| **i18n-translator** | Localization | Translate user-facing labels into the maintained target languages (invoked by `/i18n` + the `i18n-align` routine). Gated on `features.has_i18n`. | Context-aware translation using the per-key registry context, ICU/placeholder preservation, glossary adherence; bounded "flag-not-guess" self-healing (`needs-attention`) | Yes (writes native locale files ONLY — never app code, never the registry) | Native locale files, optional bulk backends (lingo.dev/languine) |
|
|
26
27
|
| **ui-expert** | Design | Design, implement, and review UI/UX | Mobile-first, accessibility, registry-first protocol gate (when `features.has_design_system: true`) — BLOCKING reads on `${paths.design_system}/INDEX.md` + `tokens-reference.md` + `components/<Name>.md` per `framework/agents/design-system-protocol.md`, Component Discovery cascade before any design | Yes (generates UI / HTML mockups in `ui-design` Step C; a separate fresh instance acts as design-quality evaluator in Step D) | ui-ux-pro-max, Playwright |
|
|
@@ -112,6 +113,7 @@ Mechanical gate run? (see QA Protocol) --> qa-sentinel ← gate-runner only, PA
|
|
|
112
113
|
Write a regression test? --> coder (qa-sentinel cannot write code)
|
|
113
114
|
Collateral impact detection? --> code-reviewer / codebase-architect
|
|
114
115
|
Check runtime logs / console errors? --> code-reviewer (requires code-aware analysis)
|
|
116
|
+
Final-merge CODE conflict to resolve? --> merge-conflict-resolver (auto-spawned by /new Phase 6 / /mw on merge-worktree.sh status:code_conflict — not a manual route)
|
|
115
117
|
|
|
116
118
|
New feature/bug to scope? --> prd
|
|
117
119
|
PRD approved, need backlog cards? --> prd-card-writer (called by /prd skill)
|
|
@@ -194,6 +196,7 @@ Use this table when spawning agents via the `Task` tool. The `model` field in ea
|
|
|
194
196
|
| **doc-reviewer** | sonnet | — (always sonnet) | Documentation work, sonnet handles well |
|
|
195
197
|
| **security-reviewer** | sonnet | opus for auth/payments/multi-tenant | Elevate for high-risk security analysis |
|
|
196
198
|
| **qa-sentinel** | sonnet | — (always sonnet) | Mechanical gate runner, no reasoning needed |
|
|
199
|
+
| **merge-conflict-resolver** | sonnet | — (always sonnet) | Bounded additive-vs-semantic judgment over small hunks; the deterministic plumbing lives in `merge-worktree.sh`, not the agent |
|
|
197
200
|
| **codebase-architect** | sonnet | — (always sonnet) | Code navigation + pattern analysis |
|
|
198
201
|
| **plan-auditor** | sonnet | opus for >5 card epics | Complex plan audits benefit from deeper reasoning |
|
|
199
202
|
| **prd** | opus | — (always opus) | PRD creation requires deep product thinking |
|
|
@@ -24,6 +24,7 @@ Violating this rule has caused repeated user frustration. Treat it as a blocking
|
|
|
24
24
|
|
|
25
25
|
## Before Starting
|
|
26
26
|
0. You were spawned by the orchestrator **already on the correct branch / worktree**. The heavy/light decision (worktree or not) was taken before your spawn — see `AGENTS.md § Post-Approval Complexity Gate`. Do not create or switch branches/worktrees. Safety check: at startup run `git rev-parse --show-toplevel` and `git branch --show-current`. If you are inside the main repo but on a branch different from the project's trunk branch (`git.trunk_branch` in `baldart.config.yml`, autodetected default), or the orchestrator asks you to work on a new branch, STOP and report the error: work that requires a new branch must happen inside a worktree (typically `.worktrees/*`), not on the main repo. Do not proceed until you are given a correct worktree path.
|
|
27
|
+
- **WORKTREE ISOLATION — per-Edit path guard (HARD, intrinsic — applies to EVERY spawn, even when the task brief omits it).** Capture your worktree root at startup: `WT="$(git rev-parse --show-toplevel)"`. **Every** path you `Edit`/`Write`/`create` MUST be UNDER `$WT`. `cd`-ing into the worktree is NOT enough — an **absolute** path (or a `../`) that lands in the main repo (`<main>/…`, no `.worktrees/` segment) is checked out live by other sessions and silently pollutes it. Before editing a path, assert it is worktree-rooted; this is non-negotiable for **shared / generated / locale files** (`i18n`/locale `*.ts`, `data.ts`, design-system files) where a main-repo write is most damaging and hardest to spot. If a brief gives you a bare relative path, resolve it against `$WT`, never the process cwd. (FEAT-0042 incident: a fix-pass coder Edited `<main>/src/lib/i18n/{en,fr,es,de}.ts` on the MAIN repo, then had to `git checkout --` + re-apply in the worktree — wasted ~3M tokens and risked main-repo corruption.)
|
|
27
28
|
1. Read `AGENTS.md`, `agents/index.md`, and `.claude/agents/REGISTRY.md`.
|
|
28
29
|
2. Verify a backlog card exists for this work.
|
|
29
30
|
3. Update the card to `IN_PROGRESS`.
|
|
@@ -62,7 +63,7 @@ Load context in this exact order before touching any file:
|
|
|
62
63
|
2. Add the new key to the source-language native locale file (the `source` value).
|
|
63
64
|
3. Add a **stub entry** to the registry (`${paths.i18n_registry}`): `source`, a **hyper-brief `context`** (what the label is for / where it appears — one line), and `domain` (the feature area). Set `char_limit`/`icu` when relevant. The QUALITY of `context` is curated by `doc-reviewer` — your job is to never leave the entry missing.
|
|
64
65
|
4. Follow the naming convention (`namespace.domain.key`, semantic not literal, `global.*` for genuinely reused strings) unless an overlay overrides it.
|
|
65
|
-
5. Do NOT translate into other languages (that is `i18n-translator` / the `/i18n` skill) and do NOT curate other keys (that is `doc-reviewer`). Skipping this step when the feature is on is a protocol violation: a hardcoded user-facing string, or a key referenced in `t()` with no registry entry (`I18N_REGISTRY_DRIFT`), fails review.
|
|
66
|
+
5. Do NOT translate into other languages (that is `i18n-translator` / the `/i18n` skill) and do NOT curate other keys (that is `doc-reviewer`). Skipping this step when the feature is on is a protocol violation: a hardcoded user-facing string, or a key referenced in `t()` with no registry entry (`I18N_REGISTRY_DRIFT`), fails review. **You source-only by design — even if the project has a locale-PARITY test that fails on your source-only commit, do NOT add the other locales yourself: the `/new` pipeline runs a planned `i18n-translator` fill pass after you (and before the parity gate) to satisfy it (`implement.md` Phase 2 step 9; `i18n-protocol.md` cascade). Adding translations yourself is the violation, not the missing locales.**
|
|
66
67
|
|
|
67
68
|
## Conditional Requirements — Binary-Outcome Items (MUST)
|
|
68
69
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: merge-conflict-resolver
|
|
3
|
+
description: "Resolve the final-merge CODE conflicts off the orchestrator's context. Spawned ONLY when scripts/merge-worktree.sh pauses with status:code_conflict — the deterministic script already resolved every additive doc/registry/JSONL conflict; you adjudicate the irreducible code/test hunks (additive → resolve; semantic → STOP), then re-invoke the script with --continue to land the merge. You run in a fresh, isolated context so the conflict churn never re-enters the bloated orchestrator. Returns COMPACT (a one-line verdict + path:line), never the churn."
|
|
4
|
+
model: sonnet
|
|
5
|
+
effort: medium
|
|
6
|
+
color: orange
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
> **YOLO MODE**: All agents spawned via the Task tool MUST use `mode: "bypassPermissions"`. No exceptions.
|
|
10
|
+
|
|
11
|
+
You are **Merge Conflict Resolver** — the judgment layer of the deterministic final-merge pipeline. You exist so that the irreducible *judgment* on code conflicts (additive vs semantic) happens in a **fresh, isolated context**, not in the orchestrator whose context is already enormous at end-of-batch. Every additive doc / structured-registry / metrics-JSONL conflict was ALREADY resolved deterministically by `scripts/merge-worktree.sh` before you were spawned — you never touch those. Your job is narrow: adjudicate the **code/test** hunks the script could not, then let the script finish.
|
|
12
|
+
|
|
13
|
+
## Git authority — the ONE bounded exception
|
|
14
|
+
|
|
15
|
+
The `coder` agent is forbidden from any branch/merge operation (`coder.md` § "Git Branches Are Off-Limits"). **You are the single bounded exception**: you may resolve conflicts, `git add`, and re-invoke the merge script — but ONLY:
|
|
16
|
+
- inside the **worktree you were given** (the one paused mid-rebase), never the main repo working tree;
|
|
17
|
+
- via `scripts/merge-worktree.sh --continue` for build/land/cleanup — you do **not** hand-roll `git push` / `git rebase --continue` / `gh pr merge` yourself (the script owns the deterministic plumbing; re-authoring it is the duplication CLAUDE.md forbids and re-introduces the fabrication risk the script exists to remove);
|
|
18
|
+
- post-checks-passed (the orchestrator only spawns you after final review + QA passed).
|
|
19
|
+
|
|
20
|
+
**WORKTREE ISOLATION (HARD).** Capture the worktree root at startup: `WT="<worktreePath from the task brief>"`. Every path you `Edit` MUST be under `$WT`. An absolute path (or `../`) landing in the main repo silently pollutes shared state — FORBIDDEN, most of all for shared/generated/locale files.
|
|
21
|
+
|
|
22
|
+
## Inputs (from the task brief)
|
|
23
|
+
|
|
24
|
+
- `worktreePath` — the paused worktree (absolute).
|
|
25
|
+
- `statusPath` — the status file `merge-worktree.sh` wrote (`status: code_conflict`, `conflict_files: <csv>`). Read it; the `conflict_files` list is your work-list.
|
|
26
|
+
- `manifestPath` — the status file to pass back to the script on `--continue` (usually the same path).
|
|
27
|
+
|
|
28
|
+
## Procedure
|
|
29
|
+
|
|
30
|
+
1. **Read the status file** at `statusPath`. Confirm `status: code_conflict`. Take `conflict_files` (CSV of repo-relative paths) — these are the UNRESOLVED code/test files, all under `$WT`.
|
|
31
|
+
2. **For EACH conflicted file**, read it and classify every conflict hunk:
|
|
32
|
+
- **ADDITIVE** — the two sides add *independent* things at the same spot and BOTH belong: distinct imports, separate declarations/functions, distinct array/object/enum members, distinct table or changelog rows, a colliding numeric index (renumber yours to the next free value). → Resolve by keeping both sides (strip the `<<<<<<< / ======= / >>>>>>>` markers, union the content, fix ordering/numbering).
|
|
33
|
+
- **SEMANTIC** — the two sides change the *same* logic/value/signature/control-flow incompatibly (same function body, same constant, same condition). → Do **NOT** guess. Leave it for a human.
|
|
34
|
+
- When uncertain whether a hunk is additive or semantic, treat it as **SEMANTIC** (default to STOP — a wrong "additive" merge silently ships a logic regression).
|
|
35
|
+
3. **If every conflicted file was fully additive** → after editing, stage them: `git -C "$WT" add <files>`. Then re-invoke the script (resolve its path like the callers do):
|
|
36
|
+
```bash
|
|
37
|
+
SCRIPT="$(ls "$WT/.framework/framework/.claude/skills/worktree-manager/scripts/merge-worktree.sh" "$WT/.claude/skills/worktree-manager/scripts/merge-worktree.sh" 2>/dev/null | head -1)"
|
|
38
|
+
bash "$SCRIPT" --worktree "$WT" --manifest "<manifestPath>" --continue
|
|
39
|
+
```
|
|
40
|
+
Re-read the status file. The rebase may surface a **fresh** `code_conflict` at the next replayed commit — if so, **loop** (back to step 2) on the new `conflict_files`. Keep looping until the status is terminal (`success` / `build_fail` / `error` / `sync_needs_decision`). This loop is the per-commit rebase cost — it is paid HERE, in your isolated context, never in the orchestrator.
|
|
41
|
+
4. **If ANY hunk is SEMANTIC** → do NOT continue the merge. Leave the rebase paused (do not abort it), do not stage the semantic file. Return a terminal STOP (below). A human resolves the semantic conflict.
|
|
42
|
+
|
|
43
|
+
## Anti-fabrication (you are inside the gate, not above it)
|
|
44
|
+
|
|
45
|
+
The orchestrator re-verifies on disk (no residual markers, build green, push landed) — it does **not** trust your prose. So never report `MERGED` unless the script's status file actually reads `status: success`. If the script returns `build_fail`, the rebase introduced an incompatibility you may have caused (a wrong additive merge) — report it as STOP, do not loop blindly. "Came to rest" ≠ "done".
|
|
46
|
+
|
|
47
|
+
## Return Contract
|
|
48
|
+
|
|
49
|
+
**Mode:** COMPACT (default). Your final message to the orchestrator is bounded — a one-line verdict + the key points as `path:line` (no reasoning dump, no pasted diff/code). Persist nothing extra; the long form is the script's log + status file already on disk.
|
|
50
|
+
- Success: `MERGED <merge_commit> (<N> code conflicts resolved additively)` + the resolved files as `path:line`.
|
|
51
|
+
- Stop: `STOP: <file>:<hunk> semantic conflict — needs human` + the file(s) + a one-line why.
|
|
52
|
+
- Build fail: `STOP: post-rebase build failed — see <build_log>`.
|
|
53
|
+
|
|
54
|
+
**Do NOT** echo the `sync_marker` / `sync_detail` in your return — the script already wrote them to the status file, and the orchestrator's workspace-hygiene phase reads them from there (they are operational state, not your findings). Mode definitions + the persist-then-summarize rule: `framework/agents/return-contract-protocol.md`.
|
|
55
|
+
|
|
56
|
+
## Effort
|
|
57
|
+
|
|
58
|
+
**Baseline:** `effort: medium` (frontmatter). Additive-vs-semantic classification is bounded judgment over small hunks — medium is right. Do not over-reason a clearly-additive import union; do spend the reasoning on a borderline hunk, and when still unsure, STOP. Level→behavior mapping + precedence: `framework/agents/effort-protocol.md`.
|
|
@@ -435,10 +435,12 @@ Every card MUST include ALL fields from the template:
|
|
|
435
435
|
- `acceptance_criteria` (testable `[ ] [AC-N]` items)
|
|
436
436
|
- `definition_of_done` (checklist including doc invariants). **When `features.has_i18n: true`** AND the
|
|
437
437
|
card introduces user-facing strings, the DoD MUST carry an i18n-completeness line — NOT merely "strings
|
|
438
|
-
via `t()` + registered", but: *"new keys authored in the source locale + the i18n context registry
|
|
439
|
-
|
|
440
|
-
gate passes."*
|
|
441
|
-
|
|
438
|
+
via `t()` + registered", but: *"new keys authored in the source locale + the i18n context registry (by
|
|
439
|
+
the `coder`), then filled to ALL maintained target languages by the `/new` pipeline's planned
|
|
440
|
+
`i18n-translator` pass; the i18n parity / no-placeholder gate passes."* This is an OUTCOME the card
|
|
441
|
+
commits to, owned by the pipeline (`implement.md` Phase 2 step 9), **not** a coder-translates instruction
|
|
442
|
+
— the coder stays source-only (`coder.md` STEP 9.6). Stating it makes the translation pass a planned
|
|
443
|
+
step instead of a reactive ~2M-token round-trip after a parity-gate failure. Cross-ref
|
|
442
444
|
`framework/agents/i18n-protocol.md`.
|
|
443
445
|
- `depends_on` / `blocks` (card IDs)
|
|
444
446
|
- `estimated_complexity`
|
|
@@ -167,7 +167,7 @@ Trunk branch: [resolved git.trunk_branch — Phase 0 step 0 populates]
|
|
|
167
167
|
### Update rules
|
|
168
168
|
|
|
169
169
|
- **Before starting a card**: move it to `## Current Card` with phase info, and record `phase_module_loaded: <modulo>` as you Read each phase module (per § "Routing" HARD RULE) so context recovery can re-load it.
|
|
170
|
-
- **
|
|
170
|
+
- **At card-state-changing boundaries only** (NOT after every sub-step): update the tracker's phase status when something recovery-relevant changes — a commit, a gate verdict, a blocker, a phase-module load. **Always co-emit the `Edit` with that action's tool call** (never a standalone status-only turn — § "Context economy" makes a lone tracker-`Edit` turn a violation). A measured run still paid 38 standalone tracker-Edit turns (~13.6M / ~9%) because "after each phase" was read as "emit a status turn per phase" — so the cadence is deliberately coarsened here: between two real actions there is nothing to write. Fine-grained phase recovery does not depend on per-phase writes — § "Context recovery protocol" reconstructs the current phase from the last commit + `phase_module_loaded` + backlog statuses.
|
|
171
171
|
- **After completing a card**: move it from `Current Card` to `## Completed Cards` with:
|
|
172
172
|
- Commit hash
|
|
173
173
|
- One-line summary of what was implemented
|
|
@@ -410,7 +410,11 @@ an unrecoverable HALT, or resuming a run whose final-review workflow was KILLED
|
|
|
410
410
|
§ "workflow-killed recovery") and then deciding to stop — the batch's `codex app-server` broker (and its
|
|
411
411
|
Playwright/… MCP children) is still alive. Before exiting, run it once (idempotent, cwd-scoped, NON-blocking):
|
|
412
412
|
```bash
|
|
413
|
-
|
|
413
|
+
WT="$MAIN/<relative worktree Path from tracker ## Worktree>"
|
|
414
|
+
TD="$(npx baldart teardown-codex-broker --cwd "$WT" 2>&1)"
|
|
415
|
+
printf '%s' "$TD" | grep -qi "unknown command" \
|
|
416
|
+
&& { echo "AUTO: codex-teardown CLI unavailable — run npm i -g baldart@latest; killing broker by cwd"; pkill -f "app-server-broker\.mjs.*--cwd $WT" 2>/dev/null || true; } \
|
|
417
|
+
|| printf '%s\n' "$TD"
|
|
414
418
|
```
|
|
415
419
|
This NEVER touches a broker for another worktree or the user's interactive session (different cwd). If the
|
|
416
420
|
orchestrator is hard-killed and never resumed, the `baldart doctor` reaper is the final safety net.
|
|
@@ -256,7 +256,7 @@ This gate enforces `framework/agents/workflows.md § Scope Closure Discipline` a
|
|
|
256
256
|
For each `deferred` row whose `User-approved?` is `pending`, invoke `AskUserQuestion` with:
|
|
257
257
|
- Question: `"AC-<N> of <CARD-ID> non implementata. Testo verbatim: '<AC text>'. Motivazione dell'implementer: '<reason or 'nessuna — silent skip'>'. Come procedo?"`
|
|
258
258
|
- Options (max 4):
|
|
259
|
-
1. **"Implementa adesso"** — spawn a targeted fix `coder` agent scoped to this card's MAY EDIT files (from `## File Ownership Map`) with the instruction "Implement ONLY AC-<N>: '<AC text>'. Do not refactor or expand scope." Re-run Phase 2.5 verification on the resulting diff — **including the mandatory sub-steps 5b (API contract), 5c (alias-mutation), 5d (caller-pattern test)** if the fix touched a route, an exported helper, or a call site (a single-AC fix can still introduce these). **Hard cap: 2 attempts on the same AC.** On the 2nd failure, do NOT re-offer "Implementa adesso" — re-invoke `AskUserQuestion` with only options 2/3/4 (approve deferral / follow-up card / stop), so the loop cannot recur unbounded.
|
|
259
|
+
1. **"Implementa adesso"** — spawn a targeted fix `coder` agent scoped to this card's MAY EDIT files (from `## File Ownership Map`) with the instruction "Implement ONLY AC-<N>: '<AC text>'. Do not refactor or expand scope." **The brief MUST carry the verbatim WORKTREE ISOLATION clause** (the same one the per-card brief uses — `implement.md` § FORBIDDEN: "every path you Edit/Write/create MUST be UNDER the worktree root `<ABSOLUTE_WORKTREE_PATH>`; `cd` is not enough; an absolute main-repo path bypasses it"). Do NOT improvise a thinner fix-pass brief that drops it — a reactive fix-coder with a weak brief is exactly where the FEAT-0042 main-repo i18n write happened (`coder.md` STEP 0's per-Edit path guard is the intrinsic backstop, but the brief must still carry it). Re-run Phase 2.5 verification on the resulting diff — **including the mandatory sub-steps 5b (API contract), 5c (alias-mutation), 5d (caller-pattern test)** if the fix touched a route, an exported helper, or a call site (a single-AC fix can still introduce these). **Hard cap: 2 attempts on the same AC.** On the 2nd failure, do NOT re-offer "Implementa adesso" — re-invoke `AskUserQuestion` with only options 2/3/4 (approve deferral / follow-up card / stop), so the loop cannot recur unbounded.
|
|
260
260
|
2. **"Approva il deferral"** — record `[USER-APPROVED DEFERRAL] <today>: <user-supplied reason>` on its own line in the card's `implementation_notes`. Mark the row `User-approved? yes`.
|
|
261
261
|
3. **"Sposta su follow-up card"** — create the follow-up card by **delegating to the `prd-card-writer` agent in Standalone Single-Card Mode** (NOT a hand-written stub — backlog cards are owned by `prd-card-writer`, same discipline as `new2`/`new2-resolve`). Brief it: `MODE: standalone`, write `${paths.backlog_dir}/<CARD-ID>-followup-AC<N>.yml` with `status: TODO`, derived from the deferred AC — a non-empty `requirements` (≥1, derived from the AC), `acceptance_criteria` = the verbatim AC text (≥1), `files_likely_touched` (≥1, carried from the parent card's ownership map for this AC), `owner_agent` routed to the AC's domain, and `review_profile` per Rule C — i.e. the full STANDALONE baseline of `framework/agents/card-schema.md`. If `prd-card-writer` is unavailable (total outage), fall back to a minimal valid stub carrying at least the step-1b HALT fields (`requirements`/`acceptance_criteria`/`files_likely_touched`/`scope`); the consumer back-fill (step 1b-ii) then fills `review_profile`/`owner_agent` on first ingestion. Mark the row `User-approved? yes (follow-up: <new-card-id>)`. Do NOT proceed to Phase 2.55 until the follow-up file exists on disk and passes the step-1b field check.
|
|
262
262
|
4. **"Ferma il batch"** — halt the orchestrator, leave the worktree intact, log the reason in `## Issues & Flags`. Do NOT commit.
|
|
@@ -373,6 +373,7 @@
|
|
|
373
373
|
```
|
|
374
374
|
The `echo "<gate>:$?"` lines surface only the **exit code** inline — the full log stays on disk. When `stack.language` does NOT include `typescript`, skip the `tsc` line (no equivalent gate). Capturing to `/tmp/*-<CARD-ID>.txt` guarantees the failing output is available for step 9. On a non-zero exit, read a **bounded** extract only (`tail -n 30 /tmp/<gate>-<CARD-ID>.txt`), never the whole log — do NOT run the gates without redirect-to-file capture.
|
|
375
375
|
9. **If any check fails**: categorize the error (`lint | TypeScript | test | build | i18n`), log it in the tracker as `retry-cause: <category>`. An `i18n` failure means a hardcoded user-facing string — the fix is to externalize it via `t()` + register the key (coder STEP 9), never to weaken the gate. Because the gate is **diff-scoped to this card's changed files**, an `i18n` failure is unambiguously a string **this card** introduced — it is never pre-existing baseline debt in an untouched file (so do NOT spend a retry triaging "is this mine?"; it is). The Final review re-runs the gate over the whole batch diff as the merge backstop.
|
|
376
|
+
- **i18n locale-PARITY ≠ anti-hardcoded (do NOT retry the coder on it).** The `i18n` category above is the *anti-hardcoded* gate. A separate **locale-parity** failure — the project's OWN parity test/build complaining that a new key exists in the source locale but not in the others — surfaces under `test`/`build`, and is **NOT a `coder` retry**: the `coder` writes the source locale only **by design** (`coder.md` STEP 9.6), so retrying it just loops. Route it to **`i18n-translator`** (fills the missing target locales context-aware from the registry), then re-run the gate. **Proactively — to avoid the fail-then-fix round-trip (a measured ~2M tokens, FEAT-0042): when `features.has_i18n` AND this card's diff added new source-locale keys, spawn the `i18n-translator` fill pass right after the coder commits and BEFORE these gates**, so parity holds first-try. This is the planned per-card i18n cascade step (`agents/i18n-protocol.md`); the coder still never translates.
|
|
376
377
|
**Code-recovery check (MUST do before rewriting)**: before spawning a fix agent or rewriting code, check whether lint-staged or a pre-commit hook removed code that is actually needed (e.g. a field "unused" at commit time but consumed by later code). Inspect the captured diff and `git diff`/`git log -p` for the worktree branch and restore the needed code by an explicit file write. **Do NOT `git stash pop` inside the worktree** — `refs/stash` is globally shared across worktrees (`$GIT_COMMON_DIR`), so a stash created or popped in a worktree can corrupt another worktree's state (see Phase 4 WORKTREE COMMIT RULE). If you need to set work aside in a worktree, make a clearly-labelled WIP commit and record its hash in the tracker for later squash, never a stash.
|
|
377
378
|
When spawning a fix agent: scope it exclusively to this card's Edit-allowed files (from `## File Ownership Map`) — it MUST NOT touch files owned by other cards. Pass the fix agent: the **path** to the captured gate log (`/tmp/<gate>-<CARD-ID>.txt` — it Reads the log itself; do NOT inline-paste the full log into its prompt), the error category, and the explicit list of files it may edit. Do NOT ask the user — just fix and re-run. Fix the code, not the tests (unless the test itself is wrong). Repeat up to **3 times**. **Stuck-loop guard**: compare the failing error's fingerprint (file:line + message) between retries; if the SAME error reproduces on 2 consecutive retries, the fix is not converging (often a constraint outside the card's ownership map) — stop early, log `[STUCK-LOOP] <error>` in `## Issues & Flags`, and escalate to the user rather than burning the 3rd retry on an identical failure.
|
|
378
379
|
10. If still failing after 3 retries (or on a stuck loop), log the failure in `## Issues & Flags` and ask the user before continuing.
|
|
@@ -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
|
|
|
@@ -203,22 +208,38 @@ The most common failure mode is leaving cards IN_PROGRESS after merge. This crea
|
|
|
203
208
|
**even though `/mw` already removed the worktree directory** (the broker is matched by its `--cwd`
|
|
204
209
|
token / state-hash, both of which survive the dir removal):
|
|
205
210
|
```bash
|
|
206
|
-
WORKTREE="$MAIN/<relative Path from tracker ## Worktree>" # absolute
|
|
207
|
-
|
|
211
|
+
WORKTREE="$MAIN/<relative Path from tracker ## Worktree>" # absolute path — resolve $MAIN in THIS shell
|
|
212
|
+
# Capture stderr — do NOT mask with 2>/dev/null (that silently hid a real leak: a consumer whose
|
|
213
|
+
# installed CLI predates the subcommand got `unknown command`, swallowed, and the broker survived).
|
|
214
|
+
TD_OUT="$(npx baldart teardown-codex-broker --cwd "$WORKTREE" 2>&1)"
|
|
215
|
+
if printf '%s' "$TD_OUT" | grep -qi "unknown command"; then
|
|
216
|
+
# CLI/framework version skew: the prose ships via the git subtree (current), but `npx baldart`
|
|
217
|
+
# resolves the separately-installed CLI, which may be older and lack this subcommand. Don't leak
|
|
218
|
+
# silently — warn + kill the broker directly by cwd so its MCP children orphan to init and the
|
|
219
|
+
# reap-orphans line below sweeps them.
|
|
220
|
+
echo "AUTO: codex-teardown CLI unavailable — installed baldart predates v4.62.0; run \`npm i -g baldart@latest\`. Falling back to a direct broker kill."
|
|
221
|
+
pkill -f "app-server-broker\.mjs.*--cwd $WORKTREE" 2>/dev/null || true
|
|
222
|
+
else
|
|
223
|
+
printf '%s\n' "$TD_OUT" # one-line summary (method / killed N) for the log
|
|
224
|
+
fi
|
|
208
225
|
```
|
|
209
226
|
This is **scoped strictly by cwd** — it ends ONLY this worktree's broker (graceful plugin shutdown
|
|
210
227
|
when the broker.json is present, else a signal to the cwd-matched broker subtree). It NEVER touches
|
|
211
228
|
a broker serving another worktree or the user's interactive Codex session on the main repo (a
|
|
212
229
|
different cwd). Do NOT run this per-card — the broker is shared and warm across the batch; ending it
|
|
213
230
|
mid-batch would force a cold re-spawn for the next card.
|
|
214
|
-
2. **Then the cumulative orphan sweep** (second line) — mop up any MCP server
|
|
215
|
-
(ppid 1)
|
|
231
|
+
2. **Then the cumulative orphan sweep + assert** (second line) — mop up any MCP server orphaned to init
|
|
232
|
+
(ppid 1), then VERIFY this worktree's broker is actually gone (so a skewed CLI / partial kill is
|
|
233
|
+
surfaced, not silently tolerated):
|
|
216
234
|
```bash
|
|
217
235
|
npx baldart reap-orphans 2>/dev/null || true
|
|
236
|
+
ps -eo command | grep -q "app-server-broker\.mjs.*--cwd $WORKTREE" \
|
|
237
|
+
&& echo "WARN: a Codex broker for $WORKTREE is STILL alive after teardown — update baldart (npm i -g baldart@latest) and/or re-run reap-orphans." || true
|
|
218
238
|
```
|
|
219
239
|
`reap-orphans` reaps ONLY orphaned MCP servers (ppid 1 ⇒ their broker is already dead ⇒ stdio is
|
|
220
|
-
broken ⇒ dead weight); it NEVER kills a live broker.
|
|
221
|
-
|
|
240
|
+
broken ⇒ dead weight); it NEVER kills a live broker. The assert is the safety net for the skew bug
|
|
241
|
+
(FEAT-0042: 5 brokers survived because `teardown-codex-broker` was masked as a no-op on an old CLI).
|
|
242
|
+
Never gate the close on these — any error or a "nothing to do" result is fine; capture each one-line
|
|
222
243
|
summary for the log.
|
|
223
244
|
|
|
224
245
|
6. **Log and exit**:
|
|
@@ -326,7 +326,13 @@ returns when the batch is done. It returns:
|
|
|
326
326
|
# 1. Proactive, cwd-scoped teardown FIRST — end the broker bound to THIS batch's worktree.
|
|
327
327
|
# `worktreePath` is the workflow return's worktreePath (preflight); the broker matches by --cwd /
|
|
328
328
|
# state-hash, so this works even though the workflow already merged + removed the worktree dir.
|
|
329
|
-
|
|
329
|
+
# Capture stderr (NOT 2>/dev/null) — a CLI older than the framework lacks this subcommand and would
|
|
330
|
+
# silently no-op (the FEAT-0042 leak); detect it, warn, and kill the broker by cwd as fallback.
|
|
331
|
+
WT="<worktreePath from the workflow result>"
|
|
332
|
+
TD="$(npx baldart teardown-codex-broker --cwd "$WT" 2>&1)"
|
|
333
|
+
printf '%s' "$TD" | grep -qi "unknown command" \
|
|
334
|
+
&& { echo "AUTO: codex-teardown CLI unavailable — run npm i -g baldart@latest; killing broker by cwd"; pkill -f "app-server-broker\.mjs.*--cwd $WT" 2>/dev/null || true; } \
|
|
335
|
+
|| printf '%s\n' "$TD"
|
|
330
336
|
# 2. Then the cumulative orphan sweep (second line) — mop up any MCP already orphaned to init.
|
|
331
337
|
npx baldart reap-orphans 2>/dev/null || true
|
|
332
338
|
```
|
|
@@ -152,6 +152,13 @@ Before presenting the PRD:
|
|
|
152
152
|
- Verify canonical sources resolve through ssot-registry or valid backlog links.
|
|
153
153
|
- Flag any `REGISTRY_GAP` (feature not yet in ssot-registry).
|
|
154
154
|
- Remove temp/local/absolute paths from canonical sections.
|
|
155
|
+
- **Markdown hygiene (prevents the markdownlint debt that surfaces downstream in `/new`).** A literal
|
|
156
|
+
`|` inside prose or a table CELL is parsed as a column delimiter and breaks the table / trips
|
|
157
|
+
markdownlint (FEAT-0043: `isManagerOf||isOwner` in a cell). Escape any literal pipe in a cell as `\|`,
|
|
158
|
+
or wrap the expression in an inline code span (`` `isManagerOf || isOwner` ``). Also: a table needs a
|
|
159
|
+
delimiter row and a blank line before/after (MD058). If a `markdownlint` config is present in the repo,
|
|
160
|
+
run it on the generated docs before commit and fix any finding — do not ship markdown debt for `/new`'s
|
|
161
|
+
doc-review to catch later.
|
|
155
162
|
|
|
156
163
|
## Step 4.5 — API Performance Gate
|
|
157
164
|
|
|
@@ -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
|
|
@@ -225,6 +225,8 @@ const MERGE_SCHEMA = {
|
|
|
225
225
|
deferredLeftOpen: { type: 'array', items: { type: 'string' }, description: 'F-040 — committed cards left NON-DONE (open owner-gated AC); the skill marks them DONE post-run' },
|
|
226
226
|
epicsClosed: { type: 'array', items: { type: 'string' }, description: 'Epic/parent cards marked DONE by Phase 6b step 5e (all children DONE) — NOT a forcedDone violation' },
|
|
227
227
|
uncommittedLeft: { type: 'boolean', description: 'true if dirty code was left (NOT committed) + reported (F-030)' },
|
|
228
|
+
status: { type: 'string', description: 'merge-worktree.sh terminal status: success|code_conflict|build_fail|sync_needs_decision|error' },
|
|
229
|
+
conflictFiles: { type: 'array', items: { type: 'string' }, description: 'on status:code_conflict — the UNRESOLVED code/test files the script paused on (left for a human / the resolver; new2 OPS agent does NOT hand-resolve)' },
|
|
228
230
|
note: { type: 'string' },
|
|
229
231
|
},
|
|
230
232
|
}
|
|
@@ -1083,16 +1085,24 @@ if (!committed.length) {
|
|
|
1083
1085
|
} else {
|
|
1084
1086
|
try {
|
|
1085
1087
|
mergeResult = await agentSafe(
|
|
1086
|
-
`Auto-merge the batch worktree to ${TRUNK} per ${REF}/merge-cleanup.md
|
|
1088
|
+
`Auto-merge the batch worktree to ${TRUNK} per ${REF}/merge-cleanup.md: Phase 6 via the DETERMINISTIC script merge-worktree.sh, then Phase 6b status reconciliation + Phase 6c hygiene. Run git yourself.\n\n${projectBrief}\nWorktree: ${sharedCtx.worktreePath}\nBranch: ${sharedCtx.branch}\nmerge_strategy: ${mergeStrategy}\nCommitted cards: ${committed.map((r) => r.card).join(' ')}\nPhase-0 stash to restore (if any): see /tmp/batch-tracker-${firstCard}.md.\n\n` +
|
|
1089
|
+
`PHASE 6 — run the SSOT script (the merge analogue of setup-worktree.sh; NOT a hand-rolled merge, NOT /mw inline):\n` +
|
|
1090
|
+
` 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)"\n` +
|
|
1091
|
+
` bash "$MERGE_SH" --worktree ${sharedCtx.worktreePath} --manifest /tmp/new2-merge-status-${firstCard}.txt --skip-checks\n` +
|
|
1092
|
+
` Read /tmp/new2-merge-status-${firstCard}.txt and set status:<its status:>. \n` +
|
|
1093
|
+
` • status:success → set merged:true + mergeCommit/mergeTs from the file, then do Phase 6b/6c below. Read sync_marker:/sync_detail: from the FILE (not stdout) for the G19-23 hygiene gate.\n` +
|
|
1094
|
+
` • status:code_conflict → set merged:false, status:'code_conflict', conflictFiles:[the file's conflict_files split on comma]. The script ALREADY resolved every additive doc/registry/JSONL conflict and PAUSED the rebase on the code/test files. Per your ROLE BOUNDARY you do NOT hand-resolve code — leave the worktree paused (do NOT abort), SKIP 6b/6c, and return (the workflow tracks it as a merge blocker → follow-up).\n` +
|
|
1095
|
+
` • status:build_fail/error/sync_needs_decision → merged:false, note:<the file's error:>, leave the worktree intact, return.\n` +
|
|
1096
|
+
` • $MERGE_SH empty (older subtree) → fall back to /mw programmatic checksAlreadyPassed:true.\n\n` +
|
|
1087
1097
|
`DETERMINISTIC POLICIES (NO prompts):\n` +
|
|
1088
|
-
`• ROLE BOUNDARY: you are the OPS/GIT agent — you NEVER edit source or doc files. Reconciliation touches ONLY card YAML status fields + registry rows. A merge conflict on
|
|
1098
|
+
`• ROLE BOUNDARY: you are the OPS/GIT agent — you NEVER edit source or doc files. Reconciliation touches ONLY card YAML status fields + registry rows. A merge conflict on CODE is leave+report (status:code_conflict), never hand-resolved here.\n` +
|
|
1089
1099
|
`• G24 → auto-merge via merge_strategy.\n` +
|
|
1090
1100
|
`• F-030 HARD RULE: NEVER \`git add\`/commit code that did not pass the per-card gates. If the worktree is dirty with uncommitted code → DO NOT commit it; leave it, set uncommittedLeft:true, and report. NO "safety commit". Security/migration code is NEVER swept in.\n` +
|
|
1091
1101
|
`• F-029 HARD RULE: Phase 6b reconciliation marks a card DONE ONLY if it has a real commit in ${TRUNK}..HEAD AND its gates are green. NEVER force a non-implemented card to DONE. Return forcedDone:[] (must be empty).\n` +
|
|
1092
1102
|
`• F-040 DEFERRED CARDS — leave NON-DONE (do NOT force to DONE in Phase 6b): ${deferredCards.length ? deferredCards.join(' ') : '(none)'}. These committed their code but carry an OPEN owner-gated/policy-deferred AC (e.g. a pending remote db:push). Their YAML is INTENTIONALLY IN_PROGRESS; the new2 skill marks them DONE post-run after materialising the deferral's follow-up. They ARE part of the merge — just skip them in the DONE-reconciliation. Return deferredLeftOpen:[the ones you left non-DONE].\n` +
|
|
1093
1103
|
`• EPIC CLOSURE (Phase 6b step 5e): the epic/parent card (group.is_epic:true) is NOT in the batch and stays TODO unless closed here. For each distinct group.parent of the batch cards (and any epic card in the batch itself): if EVERY child of that epic — \`grep -l "parent: <EPIC-ID>" ${paths.backlog_dir || 'backlog'}/*.yml | xargs grep -L "status: DONE"\` prints nothing — set the epic card status:DONE + completed_date + note "epic-closure gate — all children DONE" and fold into the reconciliation commit. If any child is still open → leave the epic untouched. This is NOT a forcedDone violation (the epic is a tracker, gated on all-children-DONE, not on its own commit). Return epicsClosed:[<EPIC-IDs marked DONE>].\n` +
|
|
1094
1104
|
`• G19 sync-deferred → HEAD==${TRUNK} ff-pull, else leave+report. G20 → leave+report. G21 post-batch dirty → partition-ignore framework artifacts; leave the rest + report (do NOT commit). G22 divergence → behind: ff-pull; ahead/both: leave+report; NEVER reset --hard/force-push. G23 stash restore conflict → leave intact + report.\n\n` +
|
|
1095
|
-
`Return: { merged, mergeCommit, mergeTs, reconciliation, forcedDone:[], deferredLeftOpen:[], uncommittedLeft, note }`,
|
|
1105
|
+
`Return: { merged, mergeCommit, mergeTs, status, conflictFiles, reconciliation, forcedDone:[], deferredLeftOpen:[], uncommittedLeft, note }`,
|
|
1096
1106
|
// Sonnet, not the inherited opus: this is a deterministic OPS/GIT executor (git merge +
|
|
1097
1107
|
// YAML status reconciliation + grep-based epic closure + leave-and-report hygiene gates), and
|
|
1098
1108
|
// the correctness-critical checks (F-029 forcedDone guard, F-040 deferred guard) are enforced
|
|
@@ -1112,6 +1122,7 @@ if (!committed.length) {
|
|
|
1112
1122
|
if (mergeResult && wronglyDone.length) { noteDegraded('false_done'); ledger(firstCard, 'F040-guard', 'VIOLATION', `deferred cards force-DONE by merge: ${wronglyDone.join(' ')}`) }
|
|
1113
1123
|
}
|
|
1114
1124
|
ledger(firstCard, 'G24-merge', (mergeResult && mergeResult.merged) ? 'MERGED' : 'INCOMPLETE', (mergeResult && (mergeResult.mergeCommit || mergeResult.note)) || '')
|
|
1125
|
+
if (mergeResult && mergeResult.status === 'code_conflict') ledger(firstCard, 'merge-code-conflict', 'BLOCKED', `merge-worktree.sh paused on code/test conflicts: ${(mergeResult.conflictFiles || []).join(' ')} — worktree intact, follow-up tracked (new2 OPS boundary does not hand-resolve code)`)
|
|
1115
1126
|
if (mergeResult && mergeResult.reconciliation) ledger(firstCard, 'G19-23-reconcile', 'AUTO', mergeResult.reconciliation)
|
|
1116
1127
|
}
|
|
1117
1128
|
|
|
@@ -135,6 +135,15 @@ Three enforcement layers (mirrors the design-system discipline): **per-task**
|
|
|
135
135
|
context/orphans on new keys) → **weekly** (`i18n-align` routine audits + aligns
|
|
136
136
|
target languages). The on-demand `/i18n audit` covers off-cadence checks.
|
|
137
137
|
|
|
138
|
+
**Per-card translation fill in `/new` (resolves the source-only ↔ parity-gate tension).** The
|
|
139
|
+
`coder` writes the SOURCE locale only (it never translates). But a project may run its OWN
|
|
140
|
+
**locale-parity test** as part of its `test`/`build` gate, which fails on a source-only commit. These
|
|
141
|
+
are not in conflict: in a `/new` run, when `features.has_i18n` AND a card's diff adds new source-locale
|
|
142
|
+
keys, the orchestrator runs a **planned `i18n-translator` fill pass after the coder and before the
|
|
143
|
+
parity gate** (`new/references/implement.md` Phase 2 step 9) — so parity holds first-try, the coder
|
|
144
|
+
still never translates, and the weekly `i18n-align` remains the steady-state aligner. A parity failure
|
|
145
|
+
is therefore an `i18n-translator` task, NEVER a `coder` retry.
|
|
146
|
+
|
|
138
147
|
## Ownership (strict specialization)
|
|
139
148
|
|
|
140
149
|
- **`coder`** — externalizes strings + writes the registry STUB (`source`+`context`+`domain`).
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -39,6 +39,7 @@ const ToolCurrency = require('../utils/tool-currency');
|
|
|
39
39
|
const CodexOrphans = require('../utils/codex-orphans');
|
|
40
40
|
const UpdateNotifier = require('../utils/update-notifier');
|
|
41
41
|
const cliPackageJson = require('../../package.json');
|
|
42
|
+
const { cmpVersions } = require('../utils/semver-lite');
|
|
42
43
|
|
|
43
44
|
const FRAMEWORK_DIR = '.framework';
|
|
44
45
|
const CONFIG_FILE = 'baldart.config.yml';
|
|
@@ -238,6 +239,20 @@ async function detectState(cwd, opts = {}) {
|
|
|
238
239
|
};
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
// CLI ↔ framework version skew (since v4.63.0). The framework PROSE ships via the git
|
|
243
|
+
// subtree (tracks repo HEAD, always current), but `npx baldart` resolves the SEPARATELY-
|
|
244
|
+
// installed CLI. When that CLI is OLDER than the framework, any prose calling a NEW
|
|
245
|
+
// `npx baldart` subcommand (e.g. `teardown-codex-broker`, added v4.62.0) silently fails as
|
|
246
|
+
// `unknown command` — a real, invisible regression (FEAT-0042: leaked Codex brokers/MCP
|
|
247
|
+
// every run). Surface it so the user updates the global CLI.
|
|
248
|
+
state.cliFrameworkSkew = null;
|
|
249
|
+
try {
|
|
250
|
+
if (state.frameworkVersion && cliPackageJson.version &&
|
|
251
|
+
cmpVersions(cliPackageJson.version, state.frameworkVersion) < 0) {
|
|
252
|
+
state.cliFrameworkSkew = { cli: cliPackageJson.version, framework: state.frameworkVersion };
|
|
253
|
+
}
|
|
254
|
+
} catch (_) { /* version parse failure → no skew claim */ }
|
|
255
|
+
|
|
241
256
|
state.remote = await describeRemote(git, ledger.framework_repo || State.DEFAULT_REPO, !!opts.offline);
|
|
242
257
|
state.commitsAhead = await commitsAheadOfRemote(git, state.remote);
|
|
243
258
|
state.workingTreeDirty = await localFrameworkChanges(git);
|
|
@@ -1140,6 +1155,14 @@ function renderDiagnostic(state) {
|
|
|
1140
1155
|
state.cliUpdate ? 'warn' : 'ok'
|
|
1141
1156
|
));
|
|
1142
1157
|
|
|
1158
|
+
if (state.cliFrameworkSkew) {
|
|
1159
|
+
console.log(statusLine(
|
|
1160
|
+
'CLI↔framework',
|
|
1161
|
+
`CLI v${state.cliFrameworkSkew.cli} < framework v${state.cliFrameworkSkew.framework} — new \`npx baldart\` subcommands (e.g. teardown-codex-broker) silently fail. Run: npm i -g baldart@latest`,
|
|
1162
|
+
'warn'
|
|
1163
|
+
));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1143
1166
|
console.log(statusLine('Git repo', state.isGitRepo ? 'yes' : 'no', state.isGitRepo ? 'ok' : 'err'));
|
|
1144
1167
|
|
|
1145
1168
|
if (!state.isGitRepo) return;
|