baldart 4.61.0 → 4.62.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 +41 -0
- package/VERSION +1 -1
- package/bin/baldart.js +10 -0
- package/framework/.claude/agents/coder.md +1 -1
- package/framework/.claude/agents/prd-card-writer.md +7 -1
- package/framework/.claude/skills/new/SKILL.md +33 -6
- package/framework/.claude/skills/new/references/codex-gate.md +7 -0
- package/framework/.claude/skills/new/references/final-review.md +30 -0
- package/framework/.claude/skills/new/references/merge-cleanup.md +40 -18
- package/framework/.claude/skills/new/references/metrics.md +2 -2
- package/framework/.claude/skills/new/references/setup.md +4 -2
- package/framework/.claude/skills/new2/SKILL.md +15 -8
- package/framework/.claude/skills/prd/references/audit-phase.md +10 -0
- package/framework/.claude/skills/prd/references/discovery-phase.md +12 -0
- package/framework/.claude/skills/prd/references/validation-phase.md +28 -1
- package/framework/.claude/skills/worktree-manager/SKILL.md +8 -0
- package/framework/.claude/workflows/new-final-review.js +36 -1
- package/framework/.claude/workflows/new2.js +1 -1
- package/framework/agents/card-schema.md +44 -8
- package/framework/agents/code-search-protocol.md +10 -0
- package/package.json +1 -1
- package/src/commands/teardown-codex-broker.js +70 -0
- package/src/utils/codex-orphans.js +167 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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.62.0] - 2026-06-22
|
|
9
|
+
|
|
10
|
+
**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.
|
|
11
|
+
|
|
12
|
+
**MINOR** — adds one CLI verb (`baldart teardown-codex-broker`) + new util export + skill/agent guidance edits; no layout/install change, **no new `baldart.config.yml` key** (so the schema-change propagation rule does NOT apply). All `/new` paths without the `Workflow` tool, and all Codex-less installs, are unaffected.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`baldart teardown-codex-broker --cwd <path>`** (`src/commands/teardown-codex-broker.js` + `bin/baldart.js`) + **`tearDownCodexBroker({cwd, pluginScriptsDir})`** in `src/utils/codex-orphans.js` — a **proactive, cwd-scoped** teardown of the Codex `app-server` broker bound to a specific worktree, freeing its Playwright/Figma/… MCP children. The Codex plugin already ships a clean per-cwd broker shutdown but only fires it on Claude Code `SessionEnd`; a `/new` run that ends (or is killed mid-final-review) hours earlier leaves the broker + MCP subtree burning CPU. Tier 1 reuses the plugin's own `loadBrokerSession`→`sendBrokerShutdown`→`teardownBrokerSession` (graceful); Tier 2 (plugin lib absent / no `broker.json` but the process lingers) matches the broker by its `--cwd` token and SIGTERM-pgroup→SIGKILLs the subtree (reusing `listProcesses`/`collectTree`). Scoped strictly by cwd — never touches a broker for another worktree or the user's interactive session. Validated end-to-end against a real 50-minute-orphaned broker (killed 11 processes). Doctor's `reapOrphans` stays the reactive safety net.
|
|
17
|
+
|
|
18
|
+
### Fixed (`/new` — bugs surfaced by the post-mortem)
|
|
19
|
+
|
|
20
|
+
- **`/mw` Phase-6 merge now has a concrete `Skill()` invocation** (`merge-cleanup.md` + `worktree-manager/SKILL.md`). The prose-only "delegate to `/mw`" let the orchestrator hallucinate `Skill({skill:"new:new", args:"__phase6_merge__ …"})` → `Unknown skill` → it ran the **entire** Phase-6 merge inline in its ~590k-token context (~20M cache_read). Now: a literal `Skill({skill:"worktree-manager", args:"/mw worktreePath=… checksAlreadyPassed=true"})` + a HARD "never fall back to manual git" rule (interactive → ask; AUTONOMOUS → follow-up card).
|
|
21
|
+
- **Killed `new-final-review` workflow now recovers off-context** (`final-review.md` F.1.5 + `new-final-review.js`). When the workflow was KILLED mid-Fix-phase (no structured return, no notification), the orchestrator hand-parsed the journal (explicitly forbidden) and re-verified all findings + committed fixes in-context (~16M). New "workflow-killed recovery" sub-protocol: detect non-completion, delegate ONE off-context subagent to validate+commit the worktree's uncommitted Fix diffs, never re-verify in-context, never block on a human under AUTONOMOUS. The workflow now emits a **terminal completion sentinel** (its absence ⇒ killed) and the Fix output is documented as uncommitted-until-F.5.
|
|
22
|
+
- **`rg` over GNU `grep` for source searches** (`code-search-protocol.md` + `coder.md`). A single NUL byte in a large generated file silently flips GNU `grep` to binary mode (empty output + exit 1, indistinguishable from "no match"); three coders independently re-discovered one such byte in a `data.ts` (~8–12M). Now a HARD default: prefer `rg`; treat empty+exit-1 on an expected-match file as a binary-mode trap, never as "symbol absent".
|
|
23
|
+
- **Batch tracker relocated off volatile `/tmp`** (`SKILL.md` + `metrics.md`). The recovery-SSOT tracker lived in `/tmp` and was wiped during a pause, forcing an in-context rebuild. Now `$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md` — repo-local, session-stable, survives the worktree removal, never in `git status` (no partition/`.gitignore` change). Plus a degrade branch that reconstructs run state from commits + backlog statuses if the tracker is ever missing.
|
|
24
|
+
|
|
25
|
+
### Changed (`/new` — optimizations)
|
|
26
|
+
|
|
27
|
+
- **A turn whose only tool call is a tracker `Edit` is now an explicit VIOLATION** (`SKILL.md` § Context economy) — a measured run paid ~28 such standalone turns (~11M). Ride the tracker write along with the next real tool call.
|
|
28
|
+
- **Out-of-lane `preValidated` guard + build-claim reconciliation** in `new-final-review.js`. A narrow specialist (doc-reviewer / api-perf-cost-auditor) FP-checking a finding OUTSIDE its lane no longer short-circuits to VERIFIED — it routes to the correct domain specialist. And a VERIFIED BLOCKER/HIGH asserting a build/compile failure that the SAME review's build/tsc gate contradicts (PASS) is auto-demoted to NEEDS_MANUAL_CONFIRMATION instead of gating the merge on a phantom (the false-positive perf BLOCKER the post-mortem caught).
|
|
29
|
+
- **Codex broker teardown wired into the run-end finalizers** — `merge-cleanup.md` Phase 6c (cwd-scoped teardown FIRST, then the cumulative `reap-orphans`), `SKILL.md` terminal-hygiene (HALT-before-6c), `new2/SKILL.md` Step 5, and a `codex-gate.md` note that the broker is shared/warm and must NOT be torn down per-card.
|
|
30
|
+
|
|
31
|
+
### Changed (`/prd` — design gates that prevent downstream `/new` waste)
|
|
32
|
+
|
|
33
|
+
- **Consumer enumeration for shared multi-mode functions** (`discovery-phase.md` ISA dimension 9) — when the feature changes a shared list/query/selector/filter fn used in >1 mode/call-site, EVERY consumer is now enumerated as its own ISA touchpoint. This closes the gap that let an orders-wizard catalog picker keep showing hidden products (the exclusion was modeled only at the picker-mode call; the catalog-mode consumer page was unowned → surfaced as 2 HIGH only at the final review, ~28M).
|
|
34
|
+
- **Per-AC seam-file closure + `requirements`-at-write-time** (`card-schema.md` authoring invariants, `audit-phase.md` Codex audit, `validation-phase.md` BLOCKING gates d/e, `prd-card-writer.md`). Every AC's render/seam file (and every file named in requirements/AC/integration_points) must be in `files_likely_touched` (⊆ check); every ISA touchpoint must be owned by some card; and a derivable `requirements` block must be emitted at authoring time — so `/new` no longer pays MAY-EDIT stalls, unplanned fix-coders, or an in-context requirements back-fill.
|
|
35
|
+
- **i18n DoD parity gated on `features.has_i18n`** (`prd-card-writer.md`) — a card introducing user-facing strings now carries a DoD line requiring translation to ALL maintained locales (not just "registered"), shifting the reactive `i18n-translator` spawn author-time-left.
|
|
36
|
+
|
|
37
|
+
## [4.61.1] - 2026-06-22
|
|
38
|
+
|
|
39
|
+
**`/new` pre-flight no longer wrongly HALTs/skips well-specified `/prd` cards that merely omit the top-level `requirements` block.** Real-world friction: implementing a `/prd`-designed epic with `/new` repeatedly hit *"N cards lack the top-level `requirements` field (profile CHILD)"* even though those cards had complete `scope`, `scope_boundaries`, `acceptance_criteria`, and `business_rationale` — the requirements were fully derivable, and the user had to hand-back-fill them every time or risk the gate destroying the epic. Root cause: `card-schema.md` listed `requirements` as an **unconditional** HALT (non-derivable), so a derivable-but-omitted block forced an ask/skip. The `prd-card-writer` still mandates emitting `requirements`; this only adds the consumer-side safety net that already exists for `review_profile`/`owner_agent`.
|
|
40
|
+
|
|
41
|
+
**PATCH** — repairs an over-strict ingestion gate; no new capability, no new `baldart.config.yml` key, no install change. The validator and the matrix `R` classification are unchanged (the writer must still emit `requirements`; the back-fill makes it present before validation runs).
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **`framework/agents/card-schema.md`** (SSOT consumer contract) — `requirements` reclassified from unconditional HALT to a **conditional** HALT member: it HALTs **only when its deriving material is itself absent** (`acceptance_criteria` empty OR `scope` empty — a genuinely thin card). When both are present and non-empty, a missing `requirements` block is recovered via a new BACK-FILL **sub-kind (b): faithful derivation** — a bounded restatement/decomposition of the existing AC + scope (faithful, never generative; never invents new scope).
|
|
46
|
+
- **`framework/.claude/skills/new/references/setup.md`** — pre-flight 1b-i drops `requirements` from the unconditional HALT list (now conditional on AC/scope being empty); 1b-ii gains the faithful-derivation back-fill (derive from AC + scope, persist to the card on disk in the main repo, log `[BACKFILL] … requirements=<N items, AC+scope-derived>`).
|
|
47
|
+
- **`framework/.claude/workflows/new2.js`** (parallel location, gate G4) — a card missing `requirements` with AC + scope present is now faithfully derived + written back (F-040 main-repo discipline) instead of excluded from the batch; only excluded when AC or scope is also absent.
|
|
48
|
+
|
|
8
49
|
## [4.61.0] - 2026-06-22
|
|
9
50
|
|
|
10
51
|
**UI design-quality critic — a separate, rigorous reviewer for *"is this good design?"*, distinct from the existing *"does it match the mockup?"* check.** Born from the observation that implemented UIs were often poor despite the full `ui-expert` + `/e2e-review` machinery. Root cause, confirmed by exploration: every existing UI verification checks **fidelity / conformance**, not **intrinsic quality** — `visual-fidelity-verifier` asks only whether the render matches the mockup (and never reads source, never judges aesthetics), `code-reviewer`'s design-system steward is mechanical (token/primitive compliance), and the only agent judging quality (`ui-expert`) is *also the implementer*, so it grades its own work. The generator-grades-itself antipattern is exactly what the critic-in-the-loop research (vision-guided iterative refinement; rubric-based LLM-as-judge with per-dimension calibration) identifies as the failure mode. This release adds the missing layer: a separate critic that judges the rendered output against a scientific rubric, in the existing bounded self-heal loop.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.62.0
|
package/bin/baldart.js
CHANGED
|
@@ -164,6 +164,16 @@ program
|
|
|
164
164
|
await reapCommand({ json: !!options.json, dryRun: !!options.dryRun });
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
program
|
|
168
|
+
.command('teardown-codex-broker')
|
|
169
|
+
.description('Proactively end the Codex app-server broker bound to a specific worktree cwd (and its MCP children). Used by the /new run-end + abort finalizers. Scoped by --cwd — never touches a broker serving another worktree or your interactive session.')
|
|
170
|
+
.option('--cwd <path>', 'The workspace cwd whose broker to end (the /new batch worktree path). Defaults to the current directory.')
|
|
171
|
+
.option('--json', 'Machine-readable output: emit a single JSON result object on stdout')
|
|
172
|
+
.action(async (options) => {
|
|
173
|
+
const teardownCommand = require('../src/commands/teardown-codex-broker');
|
|
174
|
+
await teardownCommand({ cwd: options.cwd, json: !!options.json });
|
|
175
|
+
});
|
|
176
|
+
|
|
167
177
|
const overlayGroup = program
|
|
168
178
|
.command('overlay')
|
|
169
179
|
.description('Author and check .baldart/overlays/ — scaffolds, validates, and detects drift on skill/agent/command overlays');
|
|
@@ -70,7 +70,7 @@ Any requirement of the form "verify X; if found → do A; if missing → do B/le
|
|
|
70
70
|
is a **binary-outcome item**. BOTH branches have a mandatory artifact — silence is never valid.
|
|
71
71
|
|
|
72
72
|
Rules:
|
|
73
|
-
1. **Verify actively**: read the relevant code (don't assume). Use Grep/Read.
|
|
73
|
+
1. **Verify actively**: read the relevant code (don't assume). Use Grep/Read. **Prefer `rg` over GNU `grep` on source** — GNU `grep` silently flips to binary mode on the first NUL byte (common in large generated/bundled `data.ts`-style files) and then prints nothing + exits 1, *indistinguishable from "no match"*. An empty result + exit 1 on a file you expect to match is a binary-mode trap, NOT proof the symbol is gone: re-run with `rg` (or `grep -a`) before acting. See `framework/agents/code-search-protocol.md` § Budget.
|
|
74
74
|
2. **Branch A (condition satisfied)**: implement A as specified.
|
|
75
75
|
3. **Branch B (condition not met)**: produce B — either:
|
|
76
76
|
- A code change (add the missing piece), OR
|
|
@@ -433,7 +433,13 @@ Every card MUST include ALL fields from the template:
|
|
|
433
433
|
- `scope` (summary, users, value)
|
|
434
434
|
- `requirements` (concrete, specific)
|
|
435
435
|
- `acceptance_criteria` (testable `[ ] [AC-N]` items)
|
|
436
|
-
- `definition_of_done` (checklist including doc invariants)
|
|
436
|
+
- `definition_of_done` (checklist including doc invariants). **When `features.has_i18n: true`** AND the
|
|
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, AND
|
|
439
|
+
translated to ALL maintained target languages (via `i18n-translator`); the i18n parity / no-placeholder
|
|
440
|
+
gate passes."* Otherwise the coder ships only the source locale and `/new`'s per-card review reactively
|
|
441
|
+
spawns `i18n-translator` — a ~2M-token round-trip the DoD should have made author-time-left. Cross-ref
|
|
442
|
+
`framework/agents/i18n-protocol.md`.
|
|
437
443
|
- `depends_on` / `blocks` (card IDs)
|
|
438
444
|
- `estimated_complexity`
|
|
439
445
|
- `review_profile` (`skip`|`light`|`balanced`|`deep` — computed via Rule C, or honored if pre-set)
|
|
@@ -100,13 +100,20 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
100
100
|
|
|
101
101
|
## Context Tracking (CRITICAL)
|
|
102
102
|
|
|
103
|
-
You MUST maintain a **persistent tracking file**
|
|
103
|
+
You MUST maintain a **persistent tracking file** (`$TRACKER`) throughout the entire batch run.
|
|
104
|
+
`$TRACKER` is deterministic — recompute it any time (even after a compaction) from the first card ID:
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
```
|
|
107
|
+
$TRACKER = "$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
i.e. inside the repo's **shared git dir** (e.g. `<main>/.git/baldart/run/batch-tracker-FEAT-0396.md`). Use the **first card ID** as the suffix so parallel-terminal `/new` sessions do NOT conflict; `mkdir -p "$(dirname "$TRACKER")"` once before the first write. **Why the shared git dir and NOT `/tmp`:** `/tmp` is wiped on idle/reboot — a real run lost its tracker mid-pause when `/tmp` was cleaned and had to rebuild it in-context. `git rev-parse --git-common-dir` resolves to `<main>/.git` **even when `/new` runs from inside a worktree** (unlike `--show-toplevel`), so the tracker is session-stable, survives the Phase 6 worktree removal, and is **never tracked by git** (zero `git status` pollution — no clean-tree-partition or `.gitignore` change needed).
|
|
111
|
+
|
|
112
|
+
This file is your single source of truth — if your context gets compacted or you lose track of what happened, **recompute `$TRACKER` and re-read it first** (§ "Context recovery protocol").
|
|
106
113
|
|
|
107
114
|
### Tracking file format
|
|
108
115
|
|
|
109
|
-
At batch start — **before Phase 0 runs** (Phase 0 step 1 writes to the `## Worktree` section and Phase 0 step 5 writes the phase log, so the file MUST already exist) — create
|
|
116
|
+
At batch start — **before Phase 0 runs** (Phase 0 step 1 writes to the `## Worktree` section and Phase 0 step 5 writes the phase log, so the file MUST already exist) — create `$TRACKER` with:
|
|
110
117
|
|
|
111
118
|
```markdown
|
|
112
119
|
# Batch Run: [CARD-IDS]
|
|
@@ -173,14 +180,14 @@ Trunk branch: [resolved git.trunk_branch — Phase 0 step 0 populates]
|
|
|
173
180
|
- QA findings file (e.g. `/qa/FEAT-XXXX.md` or "skipped")
|
|
174
181
|
- **card_status: DONE (verified)** — confirms the backlog YAML was updated and re-read to verify
|
|
175
182
|
- **When blocked**: log the blocker in `## Issues & Flags`.
|
|
176
|
-
- **On context recovery**: if you ever feel lost or after context compaction, IMMEDIATELY read your tracker file (
|
|
183
|
+
- **On context recovery**: if you ever feel lost or after context compaction, IMMEDIATELY read your tracker file (`$TRACKER`) to restore your state.
|
|
177
184
|
- **The tracker is internal and is the ONLY state surface** (since v4.47.0 — see § "State surface — the tracker only"): the user does NOT see it, and there is no separate user-visible mirror to keep in sync. Do NOT spend a dedicated turn restating a transition to the user (no TaskUpdate, no Progress-Bar block) — surface progress only as a short prose line folded into a turn you are already taking for real work (§ "Context economy" turn-count rule).
|
|
178
185
|
|
|
179
186
|
---
|
|
180
187
|
|
|
181
188
|
## State surface — the tracker only
|
|
182
189
|
|
|
183
|
-
`/new` keeps a **single** state surface: the internal
|
|
190
|
+
`/new` keeps a **single** state surface: the internal `$TRACKER`
|
|
184
191
|
recovery SSOT (§ "Context Tracking"). There is **no separate user-visible mirror**. The Progress
|
|
185
192
|
Bar markdown block and the per-transition native Task spine (TaskCreate/TaskUpdate) were **removed
|
|
186
193
|
for context economy (v4.47.0)**: every standalone TaskUpdate / Progress-Bar emission was its own
|
|
@@ -267,6 +274,14 @@ baselines. Keep that bulk on disk and pass **paths**, not bodies.
|
|
|
267
274
|
> the transition happens (a compaction between would lose it — § "Context recovery protocol" re-reads
|
|
268
275
|
> the tracker, not your memory).
|
|
269
276
|
>
|
|
277
|
+
> **A turn whose ONLY tool call is a tracker `Edit` is a VIOLATION.** There is ALWAYS a next real action
|
|
278
|
+
> to co-emit it with — the next phase's first `Read`/`Bash`, the card hand-off's next-card `Read`, the
|
|
279
|
+
> commit. A measured run paid ~28 standalone tracker-`Edit` turns (~490k replay each ≈ **~11M wasted**)
|
|
280
|
+
> despite the batching rule above. So make it reflexive: if you are about to emit a lone tracker `Edit`,
|
|
281
|
+
> STOP and fold it into the next real tool call's message instead. The only turns that may carry the
|
|
282
|
+
> tracker write alone are the unavoidable phase-boundary writes where there is genuinely no next action
|
|
283
|
+
> yet (rare) — and even there, prefer to ride it along with the first action of the next phase.
|
|
284
|
+
>
|
|
270
285
|
|
|
271
286
|
> **HARD RULE — orchestrator is near-silent / maximally cryptic (since v4.49.0).** `/new` is NOT
|
|
272
287
|
> conversational: the user does not read its running narration and keeps the orchestrator attached ONLY so it
|
|
@@ -380,7 +395,8 @@ per-card nei sub-step D.x (mai aggregate). Caricalo quando Pre-flight seleziona
|
|
|
380
395
|
## Context recovery protocol
|
|
381
396
|
|
|
382
397
|
If at ANY point you are unsure where you are in the batch:
|
|
383
|
-
1.
|
|
398
|
+
1. **Recompute `$TRACKER`** = `$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md` (the first card ID is in the batch args — the path is deterministic) and Read it.
|
|
399
|
+
- **If the tracker is MISSING** (a pre-this-release run still on volatile `/tmp`, or the file was deleted during a pause): do **NOT** rebuild it from memory in-context. **Reconstruct from the durable witnesses** — git + the backlog, which are the real SSOT the tracker only caches: the worktree branch's commits (`git -C <worktree> log --oneline "$TRUNK"..HEAD` — each `[CARD-ID]` commit ⇒ that card progressed/completed) and each batch card's backlog YAML `status:` (`DONE` ⇒ completed). Re-create `$TRACKER` from those facts (which cards are DONE, which is current), then continue from there.
|
|
384
400
|
2. Check `## Current Card` — if populated, resume that card at the listed phase.
|
|
385
401
|
2b. **Pre-flight not finished?** If `## Current Card` is empty / `(none — starting pre-flight)` AND `## Worktree` still shows skeleton placeholders (no real `Path:`/`Port:`), then a compaction hit mid-pre-flight (most likely during the step-5 background barrier). Re-enter **Pre-flight at step 4** (Read the `setup.md` module first per § "Routing"): its step-4a2 git pre-check re-detects an already-created worktree via `git worktree list` and resumes-or-resets it safely — so re-entry never double-creates or fails-loud.
|
|
386
402
|
3. **Re-Read the phase module** named in `## Current Card`'s `phase_module_loaded:` field (a compaction may have evacuated it from context — see § "Routing" HARD RULE). If the field is absent, map the listed phase to its module via the § "Routing" table and Read that.
|
|
@@ -388,6 +404,17 @@ If at ANY point you are unsure where you are in the batch:
|
|
|
388
404
|
5. Check `## Completed Cards` — know what's already done (don't redo).
|
|
389
405
|
6. Continue the pipeline from where you left off.
|
|
390
406
|
|
|
407
|
+
**Terminal hygiene — tear down the Codex broker on ANY exit before Phase 6c.** The normal close runs the
|
|
408
|
+
cwd-scoped Codex broker teardown in Phase 6c step 5b. But if the batch ends **before** reaching Phase 6c —
|
|
409
|
+
an unrecoverable HALT, or resuming a run whose final-review workflow was KILLED (see `final-review.md`
|
|
410
|
+
§ "workflow-killed recovery") and then deciding to stop — the batch's `codex app-server` broker (and its
|
|
411
|
+
Playwright/… MCP children) is still alive. Before exiting, run it once (idempotent, cwd-scoped, NON-blocking):
|
|
412
|
+
```bash
|
|
413
|
+
npx baldart teardown-codex-broker --cwd "$MAIN/<relative worktree Path from tracker ## Worktree>" 2>/dev/null || true
|
|
414
|
+
```
|
|
415
|
+
This NEVER touches a broker for another worktree or the user's interactive session (different cwd). If the
|
|
416
|
+
orchestrator is hard-killed and never resumed, the `baldart doctor` reaper is the final safety net.
|
|
417
|
+
|
|
391
418
|
---
|
|
392
419
|
|
|
393
420
|
|
|
@@ -37,6 +37,13 @@ Log the detector output in the tracker for traceability. **This step never short
|
|
|
37
37
|
|
|
38
38
|
#### Step C — Invoke per-card `/codexreview` (ALWAYS)
|
|
39
39
|
|
|
40
|
+
> **Do NOT tear down the Codex broker per-card.** The `codex app-server` broker is **one per worktree
|
|
41
|
+
> cwd, shared and warm-reused across every per-card + final-review Codex call** in the batch. Ending it
|
|
42
|
+
> after a card would force a cold re-spawn (~2s) for the next card's review. The broker (and its MCP
|
|
43
|
+
> children) is torn down ONCE at run-end — `merge-cleanup.md` Phase 6c step 5b (cwd-scoped
|
|
44
|
+
> `baldart teardown-codex-broker`), with the `baldart doctor` reaper as the safety net. Per-card hygiene
|
|
45
|
+
> is none.
|
|
46
|
+
|
|
40
47
|
For EVERY card (no conditional skip — the gate ALWAYS runs; only its DEPTH varies):
|
|
41
48
|
|
|
42
49
|
0. **Select the Codex review profile (deterministic — Review Profile Selector)** by mapping the
|
|
@@ -136,6 +136,36 @@ that is a **gate violation**: log it as
|
|
|
136
136
|
new-final-review workflow (engine: <codexEngine>, fixes: <fixesApplied.length>+<docFixesApplied.length>, residual: <residual.length>)`
|
|
137
137
|
in the tracker.
|
|
138
138
|
|
|
139
|
+
- **IF the delegated workflow did NOT return a structured value** — the `Workflow`
|
|
140
|
+
tool result is `null` / an error, OR no completion `<task-notification>` arrived
|
|
141
|
+
after a bounded poll window — the run was **killed / aborted mid-flight** (e.g. a
|
|
142
|
+
user interrupt, an OOM, a rate-limit kill — the same abort class as the team-mode
|
|
143
|
+
empty-result post-mortem). **Do NOT improvise an in-context recovery.** A measured
|
|
144
|
+
run that hand-parsed the journal and **re-verified all findings + committed the Fix
|
|
145
|
+
diffs in the orchestrator's own ~540k-token context cost ~16M cache_read** — the
|
|
146
|
+
single largest avoidable block in that batch. Recover OFF-context instead:
|
|
147
|
+
1. **Detect, don't reconstruct.** You MAY read ONLY the journal's *terminal status*
|
|
148
|
+
to confirm non-completion (`status` ≠ `completed`, or a `fix … started` entry with
|
|
149
|
+
no matching result). You may **NOT** rebuild the findings/verdicts from the journal —
|
|
150
|
+
that is the forbidden hand-parse, and the Fix phase may have run **partially**, so
|
|
151
|
+
the journal is an unreliable, half-written picture.
|
|
152
|
+
2. **Locate the partial work on disk, hands-off.** `git -C <worktree> status --porcelain`
|
|
153
|
+
+ `git -C <worktree> diff --stat` (name/stat only — never read the diff bodies into
|
|
154
|
+
your context). Uncommitted changes = the killed Fix phase's unvalidated output.
|
|
155
|
+
3. **Delegate ONE off-context subagent** (a `qa-sentinel` for the build/lint/test/tsc
|
|
156
|
+
gate, plus — if the diff is non-trivial — a `code-reviewer`/`coder` to judge the
|
|
157
|
+
partial fixes against the F.3 findings file) to: verify the uncommitted diffs
|
|
158
|
+
compile and pass the gates, **commit them if good or revert them if broken**, and
|
|
159
|
+
return a **COMPACT residual** (the still-unaddressed findings, slim). The
|
|
160
|
+
orchestrator reads only that residual — it does NOT re-verify findings itself.
|
|
161
|
+
4. **Carry `residual` into Step F.5** exactly as the happy path does. **AUTONOMOUS**:
|
|
162
|
+
NEVER stop and wait for a human "riprendi" — if the recovery subagent cannot fully
|
|
163
|
+
resolve, materialize a follow-up card with the residual and proceed (log
|
|
164
|
+
`AUTO: final-review-killed → recovered off-context (residual: N) [+ follow-up <id>]`).
|
|
165
|
+
**Interactive**: surface the residual terse and continue; only `AskUserQuestion` if
|
|
166
|
+
the recovery subagent itself reports an unsafe state (e.g. a broken build it could
|
|
167
|
+
not revert cleanly).
|
|
168
|
+
|
|
139
169
|
- **ELSE** (no `Workflow` tool — older Claude Code, `disableWorkflows`, or a
|
|
140
170
|
Codex install where workflows have no equivalent) → run the **inline F.2–F.4
|
|
141
171
|
below exactly as written**. This prose is the SSOT and the always-available
|
|
@@ -14,10 +14,16 @@ After the final review passes AND all cards are committed in the worktree, deleg
|
|
|
14
14
|
git status --porcelain
|
|
15
15
|
```
|
|
16
16
|
If ANY uncommitted files exist (staged, unstaged, or untracked), commit them NOW with `[safety] Auto-commit remaining files before merge`. Do NOT proceed to `/mw` with a dirty worktree — files WILL be lost during rebase.
|
|
17
|
-
2. Invoke `/mw` with:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
2. **Invoke the worktree-manager skill via the Skill tool — this is the ONLY merge mechanism.** The skill that owns `/mw` is named **`worktree-manager`** (NOT `new`, NOT `new:new`, NOT `mw`). Call it exactly, with a concrete payload — never describe the delegation in prose and hope:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Skill({ skill: "worktree-manager",
|
|
21
|
+
args: "/mw worktreePath=<absolute worktree path from tracker ## Worktree> checksAlreadyPassed=true" })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `worktreePath` — the absolute path from the tracker `## Worktree` section (REQUIRED). The skill resolves the branch + the card IDs for the commit message from the worktree/registry itself — do **NOT** invent `branch` / `card_ids` / `worktree_path` payload keys (the real input contract is `{ worktreePath, checksAlreadyPassed }`, worktree-manager/SKILL.md § "/mw programmatic").
|
|
25
|
+
- `checksAlreadyPassed=true` — the final review + QA already validated the build, so the skill skips re-running quality gates (the safety commit ALWAYS still runs regardless).
|
|
26
|
+
- **HARD — never hand-merge in this orchestrator.** If the Skill call errors (unknown skill, bad args) or the skill STOPs on a conflict/build failure, do **NOT** fall back to inline `git merge` / `git rebase` / `git push` here. Hand-merging runs the entire Phase 6 in the cached prefix (a measured **~20M-token** regression) and risks a non-isolation-safe `git checkout` on the shared main repo. Instead: surface the skill's error/STOP report 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-delegation-failed → follow-up <id>`). `new:new` and `__phase6_merge__` are hallucinations — there is no such skill or arg; the only valid call is the `worktree-manager` one above.
|
|
21
27
|
3. The skill handles: safety commit of any remaining uncommitted files (step 3), rebasing onto the latest trunk (`$TRUNK` = `git.trunk_branch`) (step 4b — auto-resolves doc conflicts, stops on code conflicts), merging into the trunk via the configured `git.merge_strategy` (step 4c — `pr` uses `gh pr merge`, `local-push` does a direct FF push to `origin/$TRUNK`; NEITHER runs `git checkout` of the trunk on the main repo, respecting the absolute terminal-isolation rule), post-merge verification, worktree removal, registry cleanup, and remote branch deletion.
|
|
22
28
|
4. **If code merge conflicts** → the skill STOPs and reports. Doc-only conflicts (ssot-registry.md, project-status.md, etc.) are auto-resolved by keeping both sides.
|
|
23
29
|
5. **If post-merge build fails** → the skill STOPs and keeps the worktree intact for investigation.
|
|
@@ -186,19 +192,34 @@ The most common failure mode is leaving cards IN_PROGRESS after merge. This crea
|
|
|
186
192
|
- Options: `[Lascia lo stash + apri istruzioni per merge manuale]` / `[Mostrami il conflitto inline]` / `[Halt]`.
|
|
187
193
|
- **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (a stash-pop conflict needs human merge) → leave the stash intact (NEVER drop it) and materialize a follow-up card with the restore instructions; log `AUTO: stash-restore-conflict → follow-up <id> (stash retained)`.**
|
|
188
194
|
|
|
189
|
-
5b. **Process hygiene —
|
|
190
|
-
batch's per-card / final-review Codex calls drive `codex app-server
|
|
191
|
-
broker spawns the MCP servers
|
|
192
|
-
…) as children
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
5b. **Process hygiene — end THIS batch's Codex broker, then sweep stragglers (NON-BLOCKING, two steps)**.
|
|
196
|
+
This batch's per-card / final-review Codex calls drive a `codex app-server` broker — **one per
|
|
197
|
+
worktree cwd, shared across all the batch's Codex calls** — whose broker spawns the MCP servers
|
|
198
|
+
declared in `~/.codex/config.toml` (Playwright, …) as children. The Codex plugin only ends that
|
|
199
|
+
broker on Claude Code `SessionEnd` (which can be hours away), so without this the broker and its MCP
|
|
200
|
+
children keep burning CPU for the rest of the session.
|
|
201
|
+
1. **Proactive, cwd-scoped teardown FIRST** — end the broker bound to THIS batch's worktree. Resolve
|
|
202
|
+
the absolute worktree path from the tracker `## Worktree` `Path:` against `$MAIN`, and tear it down
|
|
203
|
+
**even though `/mw` already removed the worktree directory** (the broker is matched by its `--cwd`
|
|
204
|
+
token / state-hash, both of which survive the dir removal):
|
|
205
|
+
```bash
|
|
206
|
+
WORKTREE="$MAIN/<relative Path from tracker ## Worktree>" # absolute worktree path
|
|
207
|
+
npx baldart teardown-codex-broker --cwd "$WORKTREE" 2>/dev/null || true
|
|
208
|
+
```
|
|
209
|
+
This is **scoped strictly by cwd** — it ends ONLY this worktree's broker (graceful plugin shutdown
|
|
210
|
+
when the broker.json is present, else a signal to the cwd-matched broker subtree). It NEVER touches
|
|
211
|
+
a broker serving another worktree or the user's interactive Codex session on the main repo (a
|
|
212
|
+
different cwd). Do NOT run this per-card — the broker is shared and warm across the batch; ending it
|
|
213
|
+
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 already orphaned to init
|
|
215
|
+
(ppid 1) by an earlier broker death this session:
|
|
216
|
+
```bash
|
|
217
|
+
npx baldart reap-orphans 2>/dev/null || true
|
|
218
|
+
```
|
|
219
|
+
`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
|
+
Never gate the close on either — any error or a "nothing to do" result is fine; capture each one-line
|
|
222
|
+
summary for the log.
|
|
202
223
|
|
|
203
224
|
6. **Log and exit**:
|
|
204
225
|
```
|
|
@@ -208,7 +229,8 @@ The most common failure mode is leaving cards IN_PROGRESS after merge. This crea
|
|
|
208
229
|
Divergence (local…origin/$TRUNK): <0\t0 | resolved: pushed/cherry-picked/ff-pulled/rebased>
|
|
209
230
|
Sync-deferred markers: <none | reconciled | user-retained>
|
|
210
231
|
Phase 0 snapshot restore: <n/a | popped clean | conflict-deferred-to-user>
|
|
211
|
-
Codex
|
|
232
|
+
Codex broker teardown: <graceful (pid) | signalled N | none | skipped (error)>
|
|
233
|
+
Codex MCP hygiene (reap): <reaped N/M | nothing to reap | skipped (error)>
|
|
212
234
|
Completed: <timestamp>
|
|
213
235
|
```
|
|
214
236
|
If any step ended in HALT, set `Status: HALT` and report — Phase 7 must NOT start with an unclean main repo unless the user explicitly chose `[Lascia così]`.
|
|
@@ -12,7 +12,7 @@ has no blocking consequence the honest label is "monitoring signal".
|
|
|
12
12
|
|
|
13
13
|
**Steps:**
|
|
14
14
|
|
|
15
|
-
1. Read the batch tracker file (
|
|
15
|
+
1. Read the batch tracker file (`$TRACKER` = `$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md`, see new/SKILL.md § "Context Tracking") to extract — every field below has a NAMED upstream producer; read the structured key, do NOT re-parse prose:
|
|
16
16
|
- Start timestamp (from `Started:` field)
|
|
17
17
|
- Worktree-creation timestamp (from `## Worktree` — the time Phase 0/Pre-flight created the worktree; the strategy-independent cycle-time anchor, see step 2)
|
|
18
18
|
- Card list and total count
|
|
@@ -49,7 +49,7 @@ has no blocking consequence the honest label is "monitoring signal".
|
|
|
49
49
|
4. Copy the batch tracker to archive:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
cp /
|
|
52
|
+
cp "$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-<FIRST-CARD-ID>.md" "$METRICS/archive/"
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
5. Note in tracker: `## Metrics Log: WRITTEN (run_id: batch-<FIRST-CARD-ID>)`
|
|
@@ -136,15 +136,17 @@
|
|
|
136
136
|
1b. **Normalize & validate card baseline (pre-flight gate)** — `/new` is **type-blind**: it runs the same pipeline on `FEAT`/`CHORE`/`BUG`/`DOC`/`PERF`. The universal, profile-aware baseline is the SSOT in [`framework/agents/card-schema.md`](../../../agents/card-schema.md). Detect each card's profile (epic / child / standalone) per that module, then run three stages. (Epic-parent cards are excluded from the per-card loop — skip 1b-i/ii for them; the validator at 1b-iii still checks their EPIC column.)
|
|
137
137
|
|
|
138
138
|
**1b-i — HALT on non-derivable fields (ask the user).** Verify the fields that cannot be safely synthesized are present and non-empty:
|
|
139
|
-
- `
|
|
139
|
+
- `acceptance_criteria` (>=1), `files_likely_touched` (>=1 file), and `scope`.
|
|
140
|
+
- **`requirements` is conditional, NOT an unconditional HALT** (card-schema.md consumer contract): it HALTs here **only when `acceptance_criteria` OR `scope` is itself empty** (a genuinely thin card). When `acceptance_criteria` AND `scope` are both present and non-empty, a missing `requirements` block is **faithfully derivable** and is back-filled in 1b-ii — do NOT HALT or skip the card for it. (This is the common `/prd` shape: complete AC + scope + scope_boundaries but no top-level `requirements` — skipping it would destroy the epic.)
|
|
140
141
|
- `scope_boundaries` is **NOT** in this set (it is conditional — legitimately omitted for standalone cards with no siblings; see card-schema.md).
|
|
141
142
|
If any card fails: log the specific missing fields in `## Issues & Flags`, ask the user to fill them in before proceeding with that card, and continue pre-flight for any remaining valid cards. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (non-derivable fields cannot be fabricated — SC-4 "missing fields → SKIP+report, never fabricate") → skip that card from the batch and log `AUTO: card-baseline → skipped <CARD-ID> (missing <fields>)`; continue with the valid cards. NEVER invent requirements/AC/scope.**
|
|
142
143
|
|
|
143
144
|
**1b-ii — Back-fill deterministically-computable fields (compute → persist → log).** For each non-epic card missing a derivable field, compute it and **write it back to the card on disk in `$MAIN/${paths.backlog_dir}`** (the main repo path resolved at Phase 0 step 1 — NOT the worktree copy; same F-040 discipline `new2` uses for follow-ups):
|
|
144
145
|
- `review_profile` absent → compute via `prd-card-writer.md § Rule C` (the SSOT), write it.
|
|
145
146
|
- `owner_agent` absent/empty/`claude` → default `coder`, write it. (Never back-fill an epic's `""`.)
|
|
147
|
+
- `requirements` absent **AND** `acceptance_criteria` + `scope` both present and non-empty → **faithfully derive** it (card-schema.md consumer contract, BACK-FILL sub-kind b): write a `requirements` list that is a concrete restatement/decomposition of the card's existing AC + scope. **Faithful, not generative** — restate only what the AC and scope already commit to; NEVER invent new scope, constraints, or behaviour. (If `acceptance_criteria` OR `scope` is also empty, this is NOT derivable — it was already HALTed at 1b-i; never reach here.) Log it as `requirements=<N items, derived from AC+scope>`.
|
|
146
148
|
- **Check-and-skip (idempotent):** write ONLY when the field is genuinely absent. A card that already has the field is untouched — so a pre-flight re-entry after compaction never double-writes.
|
|
147
|
-
- Log each write on its own line in `## Issues & Flags`: `[BACKFILL] <CARD-ID>: <field>=<value> (Rule C / coder-default)`.
|
|
149
|
+
- Log each write on its own line in `## Issues & Flags`: `[BACKFILL] <CARD-ID>: <field>=<value> (Rule C / coder-default / AC+scope-derived)`.
|
|
148
150
|
- **Commit discipline:** after all cards are processed, if ANY back-fill was written, commit them together in `$MAIN` with the COMMIT_LOCK + doc-freshness precautions of `merge-cleanup.md` Phase 6b (clear stale `COMMIT_LOCK`; `git add` only the back-filled card YAMLs; if the doc-freshness hook blocks, stage `ssot-registry.md`; ≤2 retries): `git commit -m "chore(backlog): normalise card baseline [BACKFILL]"`. Do NOT leave `$MAIN` dirty for the batch duration — an uncommitted backlog edit would surface as noise in the Phase 3/4 git-diff gates.
|
|
149
151
|
- `canonical_docs` / `links.prd` absent on a standalone/non-PRD card → **WARN** only (log, do not block, do not back-fill).
|
|
150
152
|
|
|
@@ -316,14 +316,21 @@ returns when the batch is done. It returns:
|
|
|
316
316
|
`/new` on the hard-case follow-ups (vs leaving them tracked). In autonomous mode it is
|
|
317
317
|
`{ eligible:N, offered:false, ran_new:false }`.
|
|
318
318
|
Do NOT re-summarise the cards — the workflow already did.
|
|
319
|
-
6. **Process hygiene —
|
|
320
|
-
finder calls drive `codex app-server
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
319
|
+
6. **Process hygiene — end this batch's Codex broker, then sweep stragglers (NON-BLOCKING, two lines).**
|
|
320
|
+
The batch's per-card Codex finder calls drive a `codex app-server` broker (one per worktree cwd,
|
|
321
|
+
shared across the batch), whose broker spawns the `~/.codex/config.toml` MCP servers (Playwright, …)
|
|
322
|
+
as children; the Codex plugin only ends that broker on Claude Code `SessionEnd`, so without this it
|
|
323
|
+
and its MCP children keep burning CPU for the rest of the session. The workflow sandbox cannot run
|
|
324
|
+
Bash, so BOTH lines MUST run here in the main context, after the workflow returns:
|
|
324
325
|
```bash
|
|
326
|
+
# 1. Proactive, cwd-scoped teardown FIRST — end the broker bound to THIS batch's worktree.
|
|
327
|
+
# `worktreePath` is the workflow return's worktreePath (preflight); the broker matches by --cwd /
|
|
328
|
+
# state-hash, so this works even though the workflow already merged + removed the worktree dir.
|
|
329
|
+
npx baldart teardown-codex-broker --cwd "<worktreePath from the workflow result>" 2>/dev/null || true
|
|
330
|
+
# 2. Then the cumulative orphan sweep (second line) — mop up any MCP already orphaned to init.
|
|
325
331
|
npx baldart reap-orphans 2>/dev/null || true
|
|
326
332
|
```
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
333
|
+
The teardown is **scoped strictly by cwd** — it NEVER touches a broker serving another worktree or the
|
|
334
|
+
user's interactive Codex session (a different cwd). `reap-orphans` reaps ONLY orphaned MCP servers
|
|
335
|
+
(ppid 1 ⇒ broker dead) and NEVER kills a live broker. Non-blocking — any error / "nothing to do" is
|
|
336
|
+
fine; fold both one-line summaries into the record (`codex_broker_teardown`, `codex_mcp_reaped`).
|
|
@@ -183,6 +183,16 @@ Database-specific (apply the subset matching `stack.database`):
|
|
|
183
183
|
|
|
184
184
|
Card structure:
|
|
185
185
|
- files_likely_touched missing entries or conflicting across cards
|
|
186
|
+
- **per-AC seam-file closure**: for EACH acceptance_criteria entry, the render/seam file it
|
|
187
|
+
mechanically touches MUST be in files_likely_touched — an AC that adds a required prop, changes a
|
|
188
|
+
shared component's public signature, or wires a value into a page but does NOT list that page /
|
|
189
|
+
render-site / view-model file is a HIGH gap (it forces a MAY-EDIT stall or an unplanned fix-coder
|
|
190
|
+
in /new, and lets a coder dodge the AC by making the prop optional). Also flag any file named in a
|
|
191
|
+
card's requirements / acceptance_criteria / integration_points that is absent from
|
|
192
|
+
files_likely_touched (the ⊆ coverage rule).
|
|
193
|
+
- **shared multi-mode function consumers**: if a card changes a shared list/query/selector/filter fn
|
|
194
|
+
used in >1 mode/call-site, every consumer call-site must be owned by some card's
|
|
195
|
+
files_likely_touched — flag any unowned consumer (the "hidden-still-shown-in-the-other-mode" class).
|
|
186
196
|
- areas field incomplete
|
|
187
197
|
- git_strategy set to TBD
|
|
188
198
|
- acceptance_criteria not binary testable
|
|
@@ -764,6 +764,18 @@ Instead, run the ISA scan and present findings for validation.
|
|
|
764
764
|
utilities, API route indexes
|
|
765
765
|
- SHARED STATE: stores, contexts, providers that manage related data,
|
|
766
766
|
session/cache keys
|
|
767
|
+
- SHARED MULTI-MODE FUNCTIONS (consumer enumeration): when this feature changes
|
|
768
|
+
the behavior of a shared list/query/selector/filter/mapper function that is
|
|
769
|
+
called from MORE THAN ONE site or in MORE THAN ONE mode (e.g. a `listProducts`
|
|
770
|
+
used both in a "picker" mode AND a "catalog" mode), enumerate EVERY call-site /
|
|
771
|
+
consumer of that function as its OWN touchpoint — not just the definition site
|
|
772
|
+
or the most obvious caller. A filter/flag added at the definition that one
|
|
773
|
+
consumer relies on is NOT automatically honoured where another consumer calls
|
|
774
|
+
the same function in a different mode. (This is the gap that let an orders-wizard
|
|
775
|
+
catalog picker keep showing hidden products: the exclusion was modeled only at
|
|
776
|
+
the picker-mode call, while the live catalog-mode consumer page was never
|
|
777
|
+
enumerated → it surfaced as a HIGH only at the final review.) Grep the function
|
|
778
|
+
name across the codebase and list each distinct caller file:line.
|
|
767
779
|
|
|
768
780
|
For each touchpoint found, report:
|
|
769
781
|
- Category (from above)
|
|
@@ -33,7 +33,34 @@ Mark task 5 as `in_progress`.
|
|
|
33
33
|
- **Consistency WARN (soft)**: a `feature`/`enhancement` card with `review_profile: light` may violate Rule C. Log `"[REVIEW-PROFILE-WARN] Card <ID> is feature/enhancement but review_profile: light — verify against Rule C in prd-card-writer.md."` **Do NOT auto-bump the field** — `review_profile` is computed by `prd-card-writer` (Rule C); see `prd-card-writer.md` for the criteria. This is a soft gate: log the warning and surface it for human review.
|
|
34
34
|
- **Epics**: `review_profile` MUST be `skip` (trackers have no code work). Any other value = WARN, normalize to `skip`.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
d. **`requirements` presence (BLOCKING — PO1)** — for each CHILD/STANDALONE card whose
|
|
37
|
+
`acceptance_criteria` AND `scope` are both non-empty, `requirements` MUST be present and
|
|
38
|
+
non-empty. A freshly authored `/prd` card with derivable material but no `requirements` is an
|
|
39
|
+
authoring miss that shifts a measured ~3–4M-token back-fill into `/new`'s orchestrator context
|
|
40
|
+
(see `card-schema.md` § "Authoring completeness invariants"). `/prd` fixes it now for free.
|
|
41
|
+
Message: `"Card <ID> has acceptance_criteria + scope but no requirements — emit requirements
|
|
42
|
+
(faithful restatement of AC + scope) before commit."` (The `/new` conditional back-fill remains
|
|
43
|
+
only as the safety net for legacy / externally-authored cards.)
|
|
44
|
+
|
|
45
|
+
e. **files_likely_touched coverage (BLOCKING — P1/P2 closure)** — two sub-checks:
|
|
46
|
+
- **⊆ coverage (P2):** every file path named in a card's `requirements` /
|
|
47
|
+
`acceptance_criteria` / `integration_points` MUST also appear in that card's
|
|
48
|
+
`files_likely_touched`. An AC that adds a required prop / changes a shared component signature
|
|
49
|
+
/ wires a value into a page but does NOT list the render-site / page / view-model file is a
|
|
50
|
+
BLOCKER (it forces a MAY-EDIT stall or an unplanned fix-coder in `/new`, and lets a coder dodge
|
|
51
|
+
the AC by making the prop optional). Message: `"Card <ID>: file <path> named in <field> is
|
|
52
|
+
missing from files_likely_touched."`
|
|
53
|
+
- **ISA-touchpoint ownership (P1 AC-descent):** every touchpoint in the state file
|
|
54
|
+
`## Integration Surface Map` — including every consumer enumerated for a SHARED MULTI-MODE
|
|
55
|
+
function (discovery dimension 9) — MUST be owned by some card's `files_likely_touched`. A PRD
|
|
56
|
+
surface / ISA file owned by NO card is a BLOCKER (this is the gap that otherwise surfaces only
|
|
57
|
+
at `/new`'s expensive final review). Message: `"ISA touchpoint <path> (<category>) is owned by
|
|
58
|
+
no card — assign it to a card's files_likely_touched."`
|
|
59
|
+
Both are SEMANTIC (path extraction from prose / ISA needs interpretation) — bias toward flagging.
|
|
60
|
+
|
|
61
|
+
**Gate**: every card has `owner_agent` AND `review_profile` in their enums, `requirements` present when
|
|
62
|
+
derivable (d), and full files_likely_touched coverage (e). WARN entries from checks (b)/(c) are noted in
|
|
63
|
+
the audit log; user may accept them after review. The (d)/(e) BLOCKERs must be fixed before commit.
|
|
37
64
|
|
|
38
65
|
1. **Field Name Audit (runs after item 0 — only when item 0 passes for a given card; skipped for cards blocked in item 0)** — for each generated card:
|
|
39
66
|
|
|
@@ -100,6 +100,14 @@ Output: `{ path: string, branch: string, port: number }`
|
|
|
100
100
|
Input: `{ worktreePath: string, checksAlreadyPassed?: boolean }`
|
|
101
101
|
Output: `{ merged: boolean, developVerified: boolean, prNumber: number|null, mergeCommit: string, strategy: "pr"|"local-push" }`
|
|
102
102
|
|
|
103
|
+
**Invocation (via the Skill tool — the `/new` Phase 6 merge gate, and any orchestrator):**
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Skill({ skill: "worktree-manager", args: "/mw worktreePath=<absolute path> checksAlreadyPassed=true" })
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The skill name is **`worktree-manager`** (the skill that owns the `/mw` command) — callers must NOT guess `new` / `new:new` / `mw`. The `/mw` flow parses `worktreePath=` (the merge target; when absent it falls back to reading the registry to identify the active worktree) and `checksAlreadyPassed=` from the args string. Pass ONLY those two keys — the branch and the card IDs for the commit message are resolved from the worktree/registry, never from invented payload keys.
|
|
110
|
+
|
|
103
111
|
## Programmatic API — docs mode (for /prd orchestrator)
|
|
104
112
|
|
|
105
113
|
The docs mode is the **lean** counterpart to `/nw` and `/mw`. It is intentionally
|
|
@@ -448,6 +448,25 @@ raw.forEach((f, i) => { f.finding_id = `${f.source || 'src'}#${i}:${f.finding_id
|
|
|
448
448
|
phase('Verify')
|
|
449
449
|
const classified = (await parallelCapped(raw.map((f) => () => verifyFinding(f)), MAX_PARALLEL)).filter(Boolean)
|
|
450
450
|
|
|
451
|
+
// O5(b) — deterministic build-claim reconciliation. A VERIFIED BLOCKER/HIGH asserting a build/compile/
|
|
452
|
+
// type/symbol FAILURE is self-contradicted when THIS same review's qa-sentinel build/tsc gate PASSed:
|
|
453
|
+
// the build is green, so the claim cannot be a real build-breaker. Auto-demote it to
|
|
454
|
+
// NEEDS_MANUAL_CONFIRMATION (never silently drop — it still surfaces to the human) instead of letting it
|
|
455
|
+
// gate the merge or spawn a fixer on a phantom. Skipped when the build gate is itself FAIL or absent.
|
|
456
|
+
const BUILD_CLAIM_RE = /\b(build|compil|tsc|type[-\s]?error|does\s*not\s*compile|exported\s+member|missing\s+(symbol|export|function|import)|cannot\s+find\s+(name|module))\b/i
|
|
457
|
+
const buildGatePass = gateTable.some((g) => /build|tsc|type|compile/i.test(g.gate) && g.status === 'PASS')
|
|
458
|
+
const buildGateFail = gateTable.some((g) => /build|tsc|type|compile/i.test(g.gate) && g.status === 'FAIL')
|
|
459
|
+
if (buildGatePass && !buildGateFail) {
|
|
460
|
+
for (const f of classified) {
|
|
461
|
+
if (f.classification === 'VERIFIED' && (f.severity === 'BLOCKER' || f.severity === 'HIGH') &&
|
|
462
|
+
BUILD_CLAIM_RE.test(`${f.title || ''} ${f.evidence || ''} ${f.minimal_fix_direction || ''}`)) {
|
|
463
|
+
f.classification = 'NEEDS_MANUAL_CONFIRMATION'
|
|
464
|
+
f.reconciliation = 'auto-demoted: build/compile failure claim contradicted by a green build/tsc gate'
|
|
465
|
+
log(`Final review: auto-demoted [${f.finding_id}] ${f.severity} (build-failure claim) → NEEDS_MANUAL_CONFIRMATION — the qa build/tsc gate PASSed, contradicting it.`)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
451
470
|
// Route a finding to the specialist that OWNS its domain (never a generic code-reviewer for
|
|
452
471
|
// doc/api/security). Kept in sync with new2-resolve.js normDomain() routing buckets.
|
|
453
472
|
function domainVerifier(domain) {
|
|
@@ -460,7 +479,16 @@ function domainVerifier(domain) {
|
|
|
460
479
|
}
|
|
461
480
|
|
|
462
481
|
async function verifyFinding(f) {
|
|
463
|
-
|
|
482
|
+
// O5(a) — out-of-lane preValidated guard. A NARROW specialist (doc-reviewer / api-perf-cost-auditor)
|
|
483
|
+
// FP-checks ONLY its own lane in the finding pass; its `preValidated` (and self-reported confidence) do
|
|
484
|
+
// NOT authoritatively cover a finding it emitted OUTSIDE that lane (e.g. the perf auditor asserting a
|
|
485
|
+
// `domain: code` build/symbol failure). When the source is such a specialist and the finding's domain
|
|
486
|
+
// routes to a DIFFERENT verifier, strip the short-circuit so the correct domain specialist validates it.
|
|
487
|
+
// 'codex' / 'code-reviewer' are broad code reviewers (lane = all code domains) → unchanged. This is what
|
|
488
|
+
// let a phantom perf "BLOCKER" (a symbol it claimed missing, which existed) reach the skill un-verified.
|
|
489
|
+
const NARROW_SPECIALISTS = new Set(['doc-reviewer', 'api-perf-cost-auditor'])
|
|
490
|
+
const outOfLane = NARROW_SPECIALISTS.has(f.source) && domainVerifier(f.domain) !== f.source
|
|
491
|
+
if (!outOfLane && (f.preValidated || (typeof f.confidence === 'number' && f.confidence >= 80))) {
|
|
464
492
|
return { ...f, classification: 'VERIFIED' }
|
|
465
493
|
}
|
|
466
494
|
const verifier = domainVerifier(f.domain)
|
|
@@ -630,6 +658,13 @@ if (applyFixes && editableFiles.length) {
|
|
|
630
658
|
log(`Final Fix phase done: ${fixesApplied.length} fixed, ${docFixesApplied.length} doc-applied, ${residual.length} residual${checksFailed ? ', post-fix checks FAILED' : ''}.`)
|
|
631
659
|
}
|
|
632
660
|
|
|
661
|
+
// B2 — terminal completion sentinel. The skill's F.1.5 "workflow-killed recovery" branch treats the
|
|
662
|
+
// ABSENCE of this line (with the run journal's terminal status ≠ completed) as "killed mid-flight".
|
|
663
|
+
// Emitting it ONLY here — after every phase finished and the additive fields are assembled — makes it an
|
|
664
|
+
// unambiguous clean-finish marker. NOTE: when applyFixes, the fixes are applied to the WORKTREE but NOT
|
|
665
|
+
// committed (the orchestrator commits in F.5), so a run killed before this line leaves them as
|
|
666
|
+
// uncommitted worktree diffs that the recovery branch re-validates + commits off-context.
|
|
667
|
+
log(`NEW-FINAL-REVIEW COMPLETE — engine=${codexEngine}, findings=${findings.length}, ${applyFixes ? `fixes=${fixesApplied.length}+${docFixesApplied.length} (UNCOMMITTED in worktree), residual=${residual.length}` : 'review-only'}, gates=${gateTable.map((g) => g.gate + ':' + g.status).join(',') || 'none'}.`)
|
|
633
668
|
const base = { codexEngine, findings, noActionFindings, gateTable, summary }
|
|
634
669
|
// Additive ONLY when the Fix phase ran — otherwise byte-identical to today's shape (new2 untouched).
|
|
635
670
|
return applyFixes ? { ...base, fixesApplied, docFixesApplied, residual } : base
|
|
@@ -271,7 +271,7 @@ try {
|
|
|
271
271
|
`• Worktree (setup.md step 4): the ENTIRE build sequence (worktree add → install → env copy → port → registry → baseline) is the SSOT script \`scripts/setup-worktree.sh\` — the SAME script /nw and classic /new run. Do NOT hand-roll the bash (a re-authored copy is the duplication CLAUDE.md forbids, and the script cannot fabricate a baseline or stall). Resolve the branch from the lead/parent card's git_strategy.branch (fallback feat/<PARENT-ID|lead>-<slug>) + a kebab slug, then derive the git-authoritative idempotency pre-check (existing worktree for the branch → reset the half-built orphan before running, never re-create into a fail-loud collision). Resolve the script: \`SETUP_SH="$(ls .framework/framework/.claude/skills/worktree-manager/scripts/setup-worktree.sh .claude/skills/worktree-manager/scripts/setup-worktree.sh 2>/dev/null | head -1)"\`. Run it (foreground — you ARE the background context here): \`bash "$SETUP_SH" --main ${MAIN} --trunk ${TRUNK} --branch <branch> --slug <slug> --cards "${cardIds.join(',')}" --group-parent "<PARENT|>" --env-files "${ENV_FILES.join(',')}" --manifest /tmp/new2-wt-${firstCard}.txt\`. It installs non-interactively, copies stack.env_files (FILES plain cp / DIRS cp -R / missing FILE → WARN, never abort; never .env.example, never bulk-copy untracked), allocates a locked port, writes the registry, and runs the baseline under timeout 600 (tsc/lint report-but-continue, build STOP-on-fail). Read the manifest block (status/baseline/worktree_path/port/created_at/baseline_log): map baseline → your baseline field (pass|fail|timeout) and baseline_log → baselineLog. E2: a \`baseline:fail\`/\`status:error\` build break is NOT yours to fix (role boundary — the coder repairs it); just return it precisely. Then, INDEPENDENTLY of the manifest, VERIFY the worktree on disk and return EVIDENCE (the workflow E2.5 gate string-matches it, never the script's word): set worktreeVerified:true ONLY after running \`git -C ${MAIN} worktree list --porcelain\` (the worktree path MUST appear) AND \`test -d <wt>/node_modules\` AND confirming the branch; put the LITERAL stdout into worktreeEvidence{ worktreeListPorcelain, artifactsLs:\`ls -la <wt>/node_modules <wt>/.next 2>/dev/null | head\`, baselineLogTail }. If the script is ABSENT (older subtree), fall back to running the sequence inline per setup.md step 4 prose. NEVER report worktreeVerified:true without actually running the verify commands.\n` +
|
|
272
272
|
codexResolveBullet +
|
|
273
273
|
g3Bullet +
|
|
274
|
-
`• G4 card-field validation (setup.md 1b/1c): card missing
|
|
274
|
+
`• G4 card-field validation (setup.md 1b/1c, card-schema.md consumer contract): card missing acceptance_criteria/files_likely_touched/scope → EXCLUDE (excluded[] + reason). \`requirements\` is CONDITIONAL, not an auto-exclude: if it is missing BUT acceptance_criteria + scope are both present and non-empty, FAITHFULLY DERIVE it (a concrete restatement/decomposition of the existing AC + scope — faithful, never inventing new scope) and WRITE it back to the card YAML on disk in ${MAIN}/${paths.backlog_dir || 'backlog'} (F-040: main repo, not the worktree copy; the card artifact is NOT a source/doc file, so this is allowed despite the role boundary), logging \`[BACKFILL] <id>: requirements=<N items, AC+scope-derived>\`. Only EXCLUDE for missing requirements when acceptance_criteria OR scope is ALSO empty (a genuinely thin card). Never HALT for one bad card.\n` +
|
|
275
275
|
`• G5 depends-on: a card whose depends_on names a non-DONE card NOT in this batch → EXCLUDE it AND every in-batch card that transitively depends on it.\n` +
|
|
276
276
|
`• cardGraph (REQUIRED, F-021): for every runnable card return { id, dependsOn:[IN-BATCH deps only], ownerAgent (the card's owner_agent; G25 unknown→'coder'), reviewProfile (the card's review_profile; default 'balanced'), policyDeferredACs, alreadyCommitted, alreadyCommittedSha, isEpic (implement.md §6b epic guard: id ends '-00' OR filename ends '-epic.yml' OR group.is_epic:true OR review_profile 'skip' with no requirements), filesLikelyTouched (verbatim from the YAML) }.\n` +
|
|
277
277
|
`• B1/F-026 idempotency (per card, AFTER the worktree exists): set alreadyCommitted:true (+ alreadyCommittedSha) IFF ALL hold: (a) a commit referencing the card id exists in ${TRUNK}..HEAD of the worktree; (b) the card's validation_commands re-run GREEN right now; (c) NO open follow-up card for it exists in ${paths.backlog_dir || 'backlog'}. On a FRESH worktree ${TRUNK}..HEAD is empty → all false, zero extra work.\n` +
|
|
@@ -55,7 +55,7 @@ Legend: **R** = required, present **and non-empty** · **E** = required key, val
|
|
|
55
55
|
| `context` | R | R | R | |
|
|
56
56
|
| `scope` | R | R | R | **never omittable** — encodes intent |
|
|
57
57
|
| `scope_boundaries` | R | C | C | "Omit for standalone cards with no siblings" (`prd-card-writer.md`) |
|
|
58
|
-
| `requirements` | C | R | R | EPIC tracks via AC-EPIC
|
|
58
|
+
| `requirements` | C | R | R | EPIC tracks via AC-EPIC; CHILD/STANDALONE: faithfully derivable from AC+scope when both present — see consumer contract |
|
|
59
59
|
| `acceptance_criteria` | R (AC-EPIC) | R | R | |
|
|
60
60
|
| `definition_of_done` | R | R | R | |
|
|
61
61
|
| `depends_on` / `blocks` | E (`[]`) | E | E | |
|
|
@@ -81,16 +81,52 @@ The three classes are deliberately distinct so a thin card is fixed where it can
|
|
|
81
81
|
blocks where intent is genuinely missing:
|
|
82
82
|
|
|
83
83
|
- **HALT (non-derivable — ask the user):** a **R** field that cannot be safely synthesized.
|
|
84
|
-
The canonical HALT set is `
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
The canonical HALT set is `acceptance_criteria`, `files_likely_touched`, and `scope`
|
|
85
|
+
(CHILD/STANDALONE). `scope_boundaries` is **NOT** in the HALT set — it is **C**.
|
|
86
|
+
`requirements` is a **conditional** member: it HALTs **only when its deriving material is
|
|
87
|
+
itself absent** (`acceptance_criteria` empty OR `scope` empty) — a genuinely thin card. When
|
|
88
|
+
`acceptance_criteria` AND `scope` are both present and non-empty, a missing `requirements`
|
|
89
|
+
block is *recoverable by faithful derivation* (next bullet, sub-kind b), not a HALT — a
|
|
90
|
+
well-specified card must never be skipped just because the writer omitted a derivable block.
|
|
91
|
+
- **BACK-FILL (recoverable without asking — compute/derive, persist, log):** two sub-kinds.
|
|
92
|
+
*(a) Deterministically computable:* `review_profile` (compute via `prd-card-writer.md § Rule C`)
|
|
93
|
+
and `owner_agent` (default `coder` for a non-epic; **never** back-fill an epic's `""`).
|
|
94
|
+
*(b) Faithful derivation (bounded synthesis):* `requirements` — **only when**
|
|
95
|
+
`acceptance_criteria` AND `scope` are both present and non-empty — synthesize the
|
|
96
|
+
`requirements` list as a concrete restatement/decomposition of the existing AC + scope. This
|
|
97
|
+
is **faithful, not generative**: restate what the AC and scope already commit to; NEVER invent
|
|
98
|
+
new scope, constraints, or behaviour (the same procedure a card author performs by hand). For
|
|
99
|
+
either sub-kind, write the value to the card on disk in the **main repo** with a
|
|
100
|
+
`[BACKFILL] <id>: <field>=<value>` log. **Check-and-skip**: only write when the field is
|
|
101
|
+
genuinely absent (idempotent across pre-flight re-entry).
|
|
91
102
|
- **WARN (expected but tolerable absent):** `canonical_docs` / `links.prd` on a STANDALONE /
|
|
92
103
|
non-PRD card. Log, do not block.
|
|
93
104
|
|
|
105
|
+
## Authoring completeness invariants (write-side — keep `/new` from paying for it)
|
|
106
|
+
|
|
107
|
+
These bind the card AUTHOR (prd-card-writer) so a card never reaches `/new` with a gap `/new`
|
|
108
|
+
must then patch in its expensive orchestrator context. Two recurring leaks, each with a measured
|
|
109
|
+
downstream cost in a real batch:
|
|
110
|
+
|
|
111
|
+
- **Per-AC seam-file closure (P2).** Every `acceptance_criteria` entry must have, in
|
|
112
|
+
`files_likely_touched`, the **render/seam file it mechanically touches** — not only the
|
|
113
|
+
definition site. An AC that **adds a required prop, changes a shared component's public
|
|
114
|
+
signature, or wires a new value into a page** MUST list that component's **render-site / page
|
|
115
|
+
file** (e.g. an AC "Step-2 row shows a `notOrderable` flag" must list the `page.tsx` /
|
|
116
|
+
view-model that builds the row, not just the data fn). A coder may NOT satisfy such an AC by
|
|
117
|
+
making the new prop **optional** to dodge an unlisted file — so if the file is missing the card
|
|
118
|
+
is under-specified. The MAY-EDIT list `/new` derives from `files_likely_touched` is only as
|
|
119
|
+
complete as this closure; a missing seam file caused MAY-EDIT stalls + unplanned fix-coders.
|
|
120
|
+
**Coverage rule:** every file named in a card's `requirements` / `acceptance_criteria` /
|
|
121
|
+
`integration_points` MUST also appear in `files_likely_touched` (⊆ check).
|
|
122
|
+
- **`requirements` emitted at write time (PO1).** For a CHILD / STANDALONE card whose
|
|
123
|
+
`acceptance_criteria` AND `scope` are present, **emit `requirements` at authoring time** (the
|
|
124
|
+
faithful restatement/decomposition of AC + scope — same synthesis the `/new` BACK-FILL would do).
|
|
125
|
+
Do NOT ship the field absent and rely on the `/new` conditional back-fill above: that back-fill
|
|
126
|
+
runs in the orchestrator's accumulated context (a measured ~3–4M-token in-context tax per batch
|
|
127
|
+
when most cards omit it). The back-fill stays as the safety net for legacy cards; a freshly
|
|
128
|
+
authored `/prd` card must not trigger it.
|
|
129
|
+
|
|
94
130
|
A back-fill never rescues a card missing a HALT field: a card without `scope` blocks regardless
|
|
95
131
|
of how many derivable fields were filled.
|
|
96
132
|
|
|
@@ -80,6 +80,16 @@ Before picking a tier, classify the query:
|
|
|
80
80
|
Repeated grep on a large barrel burns the turn budget and risks the agent
|
|
81
81
|
returning *before* it synthesizes its report.
|
|
82
82
|
- **Git**: no hard budget.
|
|
83
|
+
- **Tool default — prefer `rg` over GNU `grep` for source searches.** `rg` searches
|
|
84
|
+
NUL-containing files as text by default; GNU `grep` silently switches to **binary
|
|
85
|
+
mode** on the first NUL byte (common in large generated / bundled / data files) and
|
|
86
|
+
then prints nothing and exits 1 — *indistinguishable from a genuine no-match*. So:
|
|
87
|
+
treat an empty `grep` result + exit 1 **on a file you EXPECT to match** as a
|
|
88
|
+
binary-mode trap, not "symbol absent" — re-run with `rg` (or `grep -a` to force
|
|
89
|
+
text mode) before concluding anything, and never spawn a fix to "add a missing
|
|
90
|
+
symbol" off a bare GNU-grep miss on a large file. (A measured `/new` run burned
|
|
91
|
+
~8–12M tokens with three coders independently re-discovering one NUL byte in a
|
|
92
|
+
single multi-hundred-KB `data.ts`.)
|
|
83
93
|
|
|
84
94
|
## Fallback rules
|
|
85
95
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `baldart teardown-codex-broker --cwd <path>` — proactively end the Codex
|
|
3
|
+
* app-server broker bound to a SPECIFIC workspace cwd (since v4.62.0).
|
|
4
|
+
*
|
|
5
|
+
* The reactive companion `baldart reap-orphans` only mops up MCP servers AFTER
|
|
6
|
+
* their broker has already died (ppid 1). This command is the PROACTIVE, cwd-
|
|
7
|
+
* scoped teardown: at the END of a `/new` batch (or an abort), end the broker
|
|
8
|
+
* the batch's worktree created so its Playwright/Figma/… MCP children stop
|
|
9
|
+
* burning CPU for the rest of the session — instead of waiting for the plugin's
|
|
10
|
+
* SessionEnd hook (which may be hours away) or the next reap sweep.
|
|
11
|
+
*
|
|
12
|
+
* SCOPE & SAFETY: scoped strictly by `--cwd`. It ends ONLY the broker whose
|
|
13
|
+
* `--cwd` matches; a broker serving another worktree, or the user's interactive
|
|
14
|
+
* Codex session on the main repo (a DIFFERENT cwd), is never touched. The SSOT
|
|
15
|
+
* for the teardown logic is `src/utils/codex-orphans.js` (`tearDownCodexBroker`);
|
|
16
|
+
* this command only frames it. Always exits 0 — hygiene, never a blocker.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const UI = require('../utils/ui');
|
|
20
|
+
const CodexOrphans = require('../utils/codex-orphans');
|
|
21
|
+
|
|
22
|
+
async function teardownCodexBroker(opts = {}) {
|
|
23
|
+
const json = !!opts.json;
|
|
24
|
+
const cwd = opts.cwd || process.cwd();
|
|
25
|
+
|
|
26
|
+
const result = {
|
|
27
|
+
schema: 'baldart.teardown-codex-broker/1',
|
|
28
|
+
cwd,
|
|
29
|
+
method: 'none',
|
|
30
|
+
brokerPid: null,
|
|
31
|
+
killed: 0,
|
|
32
|
+
failed: 0,
|
|
33
|
+
failures: [],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const r = await CodexOrphans.tearDownCodexBroker({ cwd });
|
|
38
|
+
result.method = r.method;
|
|
39
|
+
result.brokerPid = r.brokerPid;
|
|
40
|
+
result.killed = (r.killed || []).length;
|
|
41
|
+
result.failed = (r.failed || []).length;
|
|
42
|
+
result.failures = r.failed || [];
|
|
43
|
+
if (r.error) result.error = r.error;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
result.error = (err && err.message) || String(err);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (json) {
|
|
49
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (result.error) {
|
|
54
|
+
UI.warning(`Codex broker teardown skipped (error: ${result.error}).`);
|
|
55
|
+
} else if (result.method === 'none') {
|
|
56
|
+
UI.success('Codex broker teardown: no broker bound to this cwd — nothing to do.');
|
|
57
|
+
} else if (result.method === 'graceful') {
|
|
58
|
+
UI.success(`Codex broker teardown: gracefully shut down the broker for this cwd (pid ${result.brokerPid ?? 'n/a'}) + its MCP children.`);
|
|
59
|
+
} else {
|
|
60
|
+
UI.success(`Codex broker teardown: signalled the broker subtree for this cwd (broker pid ${result.brokerPid ?? 'n/a'}, killed ${result.killed} process(es)).`);
|
|
61
|
+
if (result.failed > 0) {
|
|
62
|
+
UI.warning(`${result.failed} could not be killed:`);
|
|
63
|
+
result.failures.forEach((f) => console.log(` pid ${f.pid}: ${f.error}`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = teardownCodexBroker;
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
const { execSync } = require('child_process');
|
|
43
|
+
const path = require('path');
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const os = require('os');
|
|
43
46
|
|
|
44
47
|
// Command signatures that identify an MCP server. When such a process is
|
|
45
48
|
// orphaned (ppid 1) it is safe to reap (its stdio parent is gone).
|
|
@@ -172,11 +175,175 @@ function reapOrphans(orphans = [], procs = listProcesses()) {
|
|
|
172
175
|
return { killed, failed };
|
|
173
176
|
}
|
|
174
177
|
|
|
178
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
// PROACTIVE, cwd-SCOPED broker teardown (since v4.62.0)
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
//
|
|
182
|
+
// WHY (distinct from reapOrphans above)
|
|
183
|
+
// -------------------------------------
|
|
184
|
+
// reapOrphans is REACTIVE: it can only mop up MCP servers AFTER their broker has
|
|
185
|
+
// already died and the OS reparented them to init (ppid 1). It deliberately never
|
|
186
|
+
// ends a live broker (a detached broker at ppid 1 is indistinguishable from a
|
|
187
|
+
// leaked one by ppid alone).
|
|
188
|
+
//
|
|
189
|
+
// But the Codex plugin ALREADY ships a clean per-cwd broker teardown — it just
|
|
190
|
+
// only fires on Claude Code `SessionEnd` (session-lifecycle-hook.mjs). A `/new`
|
|
191
|
+
// run that ends — or is KILLED mid-final-review — long before the Claude session
|
|
192
|
+
// ends therefore leaves its broker (and the Playwright/Figma/… MCP grandchildren)
|
|
193
|
+
// alive for the rest of the session, burning CPU. `tearDownCodexBroker` closes
|
|
194
|
+
// that gap: at /new run-end / abort, end the broker bound to THAT worktree's cwd.
|
|
195
|
+
//
|
|
196
|
+
// SAFETY: scoped strictly by cwd. The broker is keyed per workspace-root, so a
|
|
197
|
+
// worktree's broker has no legitimate concurrent user — but a broker for a
|
|
198
|
+
// DIFFERENT cwd (another worktree, or the user's interactive session on the main
|
|
199
|
+
// repo) is NEVER matched. We never blanket-kill brokers.
|
|
200
|
+
|
|
201
|
+
// Broker-process command signatures (the spawned app-server broker + its server).
|
|
202
|
+
const CODEX_BROKER_SIGNATURES = [
|
|
203
|
+
/app-server-broker\.mjs/,
|
|
204
|
+
/codex\s+app-server/,
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
// Best-effort locate the installed Codex plugin `scripts/` dir (for Tier 1).
|
|
208
|
+
function resolveCodexPluginScriptsDir() {
|
|
209
|
+
const home = process.env.HOME || os.homedir();
|
|
210
|
+
const base = path.join(home, '.claude', 'plugins');
|
|
211
|
+
const direct = [
|
|
212
|
+
path.join(base, 'marketplaces', 'openai-codex', 'plugins', 'codex', 'scripts'),
|
|
213
|
+
];
|
|
214
|
+
for (const c of direct) {
|
|
215
|
+
try { if (fs.existsSync(path.join(c, 'lib', 'broker-lifecycle.mjs'))) return c; } catch (_) { /* ignore */ }
|
|
216
|
+
}
|
|
217
|
+
// Shallow scan: <plugins>/{marketplaces,cache}/<marketplace>/(plugins/)?codex/scripts
|
|
218
|
+
for (const sub of ['marketplaces', 'cache']) {
|
|
219
|
+
const d = path.join(base, sub);
|
|
220
|
+
let entries;
|
|
221
|
+
try { entries = fs.readdirSync(d); } catch (_) { continue; }
|
|
222
|
+
for (const mk of entries) {
|
|
223
|
+
for (const cand of [
|
|
224
|
+
path.join(d, mk, 'plugins', 'codex', 'scripts'),
|
|
225
|
+
path.join(d, mk, 'codex', 'scripts'),
|
|
226
|
+
]) {
|
|
227
|
+
try { if (fs.existsSync(path.join(cand, 'lib', 'broker-lifecycle.mjs'))) return cand; } catch (_) { /* ignore */ }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Extract the `--cwd <value>` token from a broker ps command line.
|
|
235
|
+
function brokerCwdOf(command) {
|
|
236
|
+
const m = String(command).match(/--cwd\s+(\S+)/);
|
|
237
|
+
return m ? m[1] : null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Canonicalize a path for comparison (resolve symlinks where possible).
|
|
241
|
+
function canonical(p) {
|
|
242
|
+
if (!p) return null;
|
|
243
|
+
try { return fs.realpathSync(p); } catch (_) { return path.resolve(p).replace(/\/+$/, ''); }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Proactively tear down the Codex app-server broker bound to a SPECIFIC `cwd`
|
|
248
|
+
* (typically a /new batch worktree), freeing its MCP-server children. Scoped by
|
|
249
|
+
* cwd — never touches a broker serving another worktree or the user's
|
|
250
|
+
* interactive session on the main repo. Never throws (hygiene, not a blocker).
|
|
251
|
+
*
|
|
252
|
+
* Tier 1 (graceful) — reuse the Codex plugin's OWN lib: loadBrokerSession(cwd) →
|
|
253
|
+
* sendBrokerShutdown(endpoint) (the broker closes its app-client + MCP children)
|
|
254
|
+
* → teardownBrokerSession(...) (unlink socket/pid/log + SIGTERM the broker pid)
|
|
255
|
+
* → clearBrokerSession(cwd). This is exactly the plugin's own stale-broker path.
|
|
256
|
+
* Tier 2 (fallback) — when the plugin lib is absent/unimportable OR there is no
|
|
257
|
+
* broker.json for this cwd but a broker process still lingers: ps-match the
|
|
258
|
+
* broker whose command carries `--cwd <cwd>` + a codex-app-server signature,
|
|
259
|
+
* SIGTERM its process group, then SIGKILL survivors in its subtree (reusing
|
|
260
|
+
* listProcesses/collectTree).
|
|
261
|
+
*
|
|
262
|
+
* @param {{ cwd: string, pluginScriptsDir?: string }} opts
|
|
263
|
+
* @returns {Promise<{ method:'graceful'|'signal'|'none', brokerPid:number|null,
|
|
264
|
+
* killed:number[], failed:Array<{pid:number,error:string}>, error?:string }>}
|
|
265
|
+
*/
|
|
266
|
+
async function tearDownCodexBroker({ cwd, pluginScriptsDir } = {}) {
|
|
267
|
+
const none = { method: 'none', brokerPid: null, killed: [], failed: [] };
|
|
268
|
+
if (!cwd || process.platform === 'win32') return none;
|
|
269
|
+
|
|
270
|
+
// ---- Tier 1: graceful, via the plugin's own broker-lifecycle lib -----------
|
|
271
|
+
try {
|
|
272
|
+
const dir = pluginScriptsDir || resolveCodexPluginScriptsDir();
|
|
273
|
+
if (dir) {
|
|
274
|
+
const libPath = path.join(dir, 'lib', 'broker-lifecycle.mjs');
|
|
275
|
+
if (fs.existsSync(libPath)) {
|
|
276
|
+
const lib = await import(require('url').pathToFileURL(libPath).href);
|
|
277
|
+
if (typeof lib.loadBrokerSession === 'function') {
|
|
278
|
+
const session = lib.loadBrokerSession(cwd);
|
|
279
|
+
if (session && (session.endpoint || session.pid)) {
|
|
280
|
+
if (session.endpoint && typeof lib.sendBrokerShutdown === 'function') {
|
|
281
|
+
try { await lib.sendBrokerShutdown(session.endpoint); } catch (_) { /* ignore */ }
|
|
282
|
+
}
|
|
283
|
+
if (typeof lib.teardownBrokerSession === 'function') {
|
|
284
|
+
try {
|
|
285
|
+
lib.teardownBrokerSession({
|
|
286
|
+
endpoint: session.endpoint ?? null,
|
|
287
|
+
pidFile: session.pidFile ?? null,
|
|
288
|
+
logFile: session.logFile ?? null,
|
|
289
|
+
sessionDir: session.sessionDir ?? null,
|
|
290
|
+
pid: session.pid ?? null,
|
|
291
|
+
killProcess: (p) => { try { process.kill(p, 'SIGTERM'); } catch (_) { /* ignore */ } },
|
|
292
|
+
});
|
|
293
|
+
} catch (_) { /* ignore */ }
|
|
294
|
+
}
|
|
295
|
+
if (typeof lib.clearBrokerSession === 'function') {
|
|
296
|
+
try { lib.clearBrokerSession(cwd); } catch (_) { /* ignore */ }
|
|
297
|
+
}
|
|
298
|
+
return { method: 'graceful', brokerPid: session.pid ?? null, killed: session.pid ? [session.pid] : [], failed: [] };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (_) {
|
|
304
|
+
// Tier 1 unavailable (plugin moved, import failed, …) → fall through to Tier 2.
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---- Tier 2: signal the cwd-matched broker subtree -------------------------
|
|
308
|
+
try {
|
|
309
|
+
const target = canonical(cwd);
|
|
310
|
+
const procs = listProcesses();
|
|
311
|
+
const brokers = procs.filter((p) =>
|
|
312
|
+
matchesAny(CODEX_BROKER_SIGNATURES, p.command) && canonical(brokerCwdOf(p.command)) === target);
|
|
313
|
+
if (!brokers.length) return none;
|
|
314
|
+
const self = process.pid;
|
|
315
|
+
const killed = [];
|
|
316
|
+
const failed = [];
|
|
317
|
+
for (const b of brokers) {
|
|
318
|
+
// SIGTERM the broker's process group first (a detached broker is its own
|
|
319
|
+
// group leader) so it can run its shutdown handler; fall back to the bare pid.
|
|
320
|
+
try { process.kill(-b.pid, 'SIGTERM'); } catch (_) {
|
|
321
|
+
try { process.kill(b.pid, 'SIGTERM'); } catch (_) { /* ignore */ }
|
|
322
|
+
}
|
|
323
|
+
// Then SIGKILL any survivors in its subtree (force path — Tier 1 already failed/absent).
|
|
324
|
+
for (const pid of collectTree(b.pid, procs)) {
|
|
325
|
+
if (pid === self || !Number.isInteger(pid) || pid <= 1) continue;
|
|
326
|
+
try { process.kill(pid, 'SIGKILL'); killed.push(pid); }
|
|
327
|
+
catch (err) {
|
|
328
|
+
if (err && err.code === 'ESRCH') killed.push(pid);
|
|
329
|
+
else failed.push({ pid, error: (err && err.message) || String(err) });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { method: 'signal', brokerPid: brokers[0].pid, killed, failed };
|
|
334
|
+
} catch (err) {
|
|
335
|
+
return { method: 'none', brokerPid: null, killed: [], failed: [], error: (err && err.message) || String(err) };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
175
339
|
module.exports = {
|
|
176
340
|
MCP_SIGNATURES,
|
|
177
341
|
CODEX_RUNTIME_SIGNATURES,
|
|
342
|
+
CODEX_BROKER_SIGNATURES,
|
|
178
343
|
listProcesses,
|
|
179
344
|
detectOrphans,
|
|
180
345
|
collectTree,
|
|
181
346
|
reapOrphans,
|
|
347
|
+
tearDownCodexBroker,
|
|
348
|
+
resolveCodexPluginScriptsDir,
|
|
182
349
|
};
|