Skip to content

Commit 5b2f43d

Browse files
fix(engine): fourteen defects found by running v0.21.0 on a live 30B, plus frontend assemblers
Everything here was found by driving the harness against a real local stack — oMLX serving Qwen3-Coder-30B — rather than by reading the code. Each fix names the measurement that produced it, and each carries a regression test that fails without it. ── The gates were not gating ───────────────────────────────────────────────── Executable acceptance criteria were discarded before they could run. collapseToWorker rebuilt the surviving task without Criteria, so whenever the splitter's tasks collapsed — the common shape on a small model — every criterion the model authored was dropped. Measured: the 30B emitted correct criteria with `go test ./...` as the verify command, and the run reached the reviewer with nothing to check, indistinguishable from a task that passed. A green test command that ran no tests counted as verification. classifySmoke checked sr.OK before the no-tests check, and `go test ./...` on a tree with no _test.go files exits ZERO — so the gate's own documented contract ("a 'nothing to run' exit is NoTests, never Green") held only for runners that fail on empty. Measured: a run told "both parts must have tests" wrote none and finished ✔ with qa_gate green. A forged criteria header could switch the criteria gate off. A worker that merely echoed "## Acceptance criteria" suppressed the review-time gate, and with nothing run CriteriaUnverifiedInOutput stayed false — the value that ALLOWS the reviewer fast path. The section's provenance stamp is now required. Criteria that could never run. Told only in the abstract to write a runnable verify command, the 30B wrote `go test -v ./... | grep -E '(TestX|PASS|FAIL)'`; the sanitizer refuses shell metacharacters, so every criterion on that board came back UNVERIFIED. The splitter now gets the concrete ✓/✗ example, and its own prompt budget — it is paid once per split, as the composer is paid once per run. ── The board could deadlock, and correction aimed at the wrong files ───────── runWave is synchronous, so nothing is in flight at the top of the scheduler loop — yet a task left in in_progress/in_review still made AgentWorkRemaining report work in progress, and no path re-dispatches those columns. Measured: one stranded task, four dependents frozen behind it, ~9 minutes of correct work discarded and reported as a failure with the edit already on disk and compiling. Orphans are re-queued, bounded by the existing attempt ceiling. When the QA gate stayed red, the synthesized verdict carried the literal string "qa_gate red" — and the corrective rewrite scopes its fix by mining file paths out of those failure strings, so a verdict naming no file fell back to "whatever finished most recently". Measured: three correction rounds scoped to pkg/tasks while every compiler error was in cmd/server/main.go, which none of them was allowed to touch. A dev server was accepted as a verification. `npm run` is whitelisted because `npm run lint` and `npm run typecheck` are exactly what the gate should run; `npm run dev` shares that prefix and never returns. Measured: a splitter wrote "npm run dev passes" as a tester task's acceptance, which is not a proposition that can pass. Refusing the non-terminating scripts is what makes it safe to admit `npm run`/`yarn run`/`pnpm run` as verify commands at all — so "the project builds" can now be proven instead of reported UNVERIFIED. ── Routing was configured, displayed, and then not used ───────────────────── runner.HasRole was wired only inside the escalation branch, so on any install without a model_escalation ladder — nearly all of them — every feature that asks "is this agent registered?" answered no. normalizeExecRole applied execute.default_role only to an EMPTY role, but Task.Normalize fills empty roles with "worker" first, so the chosen language specialist was displayed in the composition and never used. Tasks now route to the specialist that owns their files, which is also what lets a mixed Go + React board run on two specialists at once. doctor printed "agents inherit stack/global LLM" unconditionally — false as soon as model_roles pins a role — and validated nothing, so a typo'd model passed at 100/100 and failed mid-run at the reviewer. Both routing keys are now shown, every model they name is checked against what the endpoint serves, and an unserved one is a readiness finding (measured: 100 → 86). `slmcode agent list` showed 20 of 59 agents: block-defined agents were registered by the orchestrator and invisible to the command that lists them. ── Greenfield work could not start, and duplicated itself when it did ─────── Greenfield Go and web paths were dropped: isGreenfieldCreatePath accepted main.py but not main.go, and knew no cmd/, pkg/ or web/. Measured: a full-stack request finished 0 of 7 tasks with nothing on disk, because every planned path was discarded and a worker forbidden to invent paths had nothing it was allowed to write. Fixing it made the same request produce a compiling Go backend and a React frontend. The fix itself is upstream's — see the rebase note below; what remains here are the tests that pin the behaviour. Two prompt few-shot examples were being copied as real work: the splitter planned "add Sum to calc.go" verbatim from its own example on a greenfield request, and the composer's handoff carried "verify with go test ./..." into a React run for the same reason. Both are now shapes, not content. Duplicate workers on one file each cost a full wave and opened a file the previous one had rewritten. Same-file worker tasks now merge — on the primary target or the whole set — with dependencies rewritten onto the survivor. Measured: one query went from six tasks to three. Workers wrote against types they had never read. Rule 1 said "ws_read a file before editing it" — nothing about USING one. Measured: a worker scoped to cmd/server/main.go used task.Title/CreatedAt/UpdatedAt on a Task a sibling task had defined with three other fields. ── Frontend assemblers ────────────────────────────────────────────────────── Two packs that build React UI by INSTALLING components with the library's own CLI and wiring them up, instead of writing them from scratch: shadcn/ui and Untitled UI. Hand-writing a dialog with focus traps is where a 7-32B model spends its runway; imports, props and layout is what it is good at. Both ship enabled — nothing to install, nothing to apply. The method is chosen from evidence and announced: the request wins first (name a library, or say "from scratch"), then the project's own markers, then greenfield defaults to assembling; an existing app with no markers keeps writing by hand. Each pack carries an assembly reviewer that rejects a component hand-rolled when the registry already had it, and per-library gates. Both CLIs were driven for real to write the agents: shadcn's `init` needs an explicit -b or it stops on an interactive menu even with -y, and Untitled UI matches names fuzzily and wrongly (`buttons` installs app-store-buttons, and it does not error). Shell access is scoped rather than opened. npx stays an executor; five subcommands of two named packages are allowed, matched structurally so `npx shadcn add`, `npx --yes shadcn@latest add` and the legacy shadcn-ui name all work. An `add` naming a URL, an @registry or a local path is refused — both CLIs accept those where a component name goes, and they resolve to a registry nobody reviewed. Also fixed: `npx tsc --noEmit` was refused (the packs' own typecheck and qa_gate — those tools live in node_modules, never on PATH), and a bare `npx tsc` prefix also admitted `npx tsc-evil`, since matching is HasPrefix. ── Verification ───────────────────────────────────────────────────────────── Local gate green on the frozen tree: gofmt, vet, golangci-lint (0 issues), the full unit suite, -race, go mod tidy, the Studio UI (lint/typecheck/test/build), govulncheck (no vulnerabilities), and coverage at 67.6% against a 63.0% floor. Live end-to-end against oMLX + Qwen3-Coder-30B: a Go bug fix with criteria verified 3 passed / 0 failed / 0 unverified, worktree isolation leaving the origin untouched and committing no harness state, per-role model routing, and the shadcn assembler installing a component with the CLI and wiring it into a page that typechecks. ── Rebased onto 32 upstream commits ───────────────────────────────────────── Three conflicts, resolved in upstream's favour where it had built the better mechanism: - Greenfield paths. Upstream added a state-aware rule — isGreenfieldRoot plus looksLikeSourceTarget — that is permissive in a genuinely empty repository and conservative once there is code in it. That is strictly better than the unconditional allowlist here, which would have blessed a claimed-but-absent cmd/server/main.go in an ESTABLISHED repo. The additions to isGreenfieldCreatePath are dropped; the tests that cover the behaviour stay and pass against upstream's rule. - Per-task specialist routing. Upstream staffs each task at board level (routeBoardToSpecialists), which sets task.Role. specializeExecRole only ever replaces a GENERIC role, so it defers to that and fills in where board-level staffing did not run — which is also what the frontend assembler hooks into. - The QA-gate verdict. Upstream's integration-failure branch and the real failure payload are both kept: the guard decides WHETHER to re-enter the tester path, and the payload decides what it is told when it does. Gate green on the rebased tree: lint, the full unit suite, -race, and coverage at 69.0% against a 63.0% floor.
1 parent 0f66e88 commit 5b2f43d

61 files changed

Lines changed: 4166 additions & 75 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Fork it. Break it. Point it at whatever LLM you have. 🚀
4949
| 📐 **Context budgeted in tokens, not bytes** | The pack budget is derived from the model's real context window minus system prompt, tool schemas and response reserve. Assembly is byte-deterministic with a stable prefix so local KV-cache prefixes actually hit. A tree-sitter-free repo map ranks files by PageRank over a symbol reference graph. |
5050
| 🧠 **It gets better at your repo** | Four memory layers, failure fingerprinting, and a repair-rule store: a given failure mode costs an LLM round-trip **once**. A Thompson-sampling bandit learns which harness settings work for your model family and language. All of it is plain JSON under `.slmcode/`, and deleting it is supported. |
5151
| 🛡️ **Gates that fail closed** | Disk state is authoritative — a claimed edit that is not on disk does not pass. Truncated reviewer JSON fails closed. The QA gate cannot report green when tests fail. A HITL gate with a human attached blocks instead of expiring into an auto-approval. |
52+
| 🎨 **Frontends get assembled, not hand-written** | Ask for React UI and SLMCode installs reviewed components with the library's own CLI — `npx shadcn@latest add button dialog` — then wires and styles them. Hand-writing a dialog with focus traps is where a 7–32B model burns its runway; imports, props and layout is what it is good at. Both **shadcn/ui** and **Untitled UI** ship enabled, with a reviewer that rejects a component you hand-rolled when the registry already had it. Say *from scratch* to opt out. |
5253
| 🖥️ **Two front ends** | A non-blocking terminal REPL (Esc to interrupt and redirect mid-run, `/` fuzzy command picker, real unified diffs, interactive `slmcode apply`) and **Studio**, an offline React SPA with a live SSE feed, a pending-change review UI and run traces. |
5354

5455
---

‎cmd/slmcode/cmd_agent.go‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/spf13/cobra"
1010

1111
"github.com/UnicoLab/slmcode/pkg/agents"
12+
"github.com/UnicoLab/slmcode/pkg/blocks"
1213
"github.com/UnicoLab/slmcode/pkg/cli"
1314
"github.com/UnicoLab/slmcode/pkg/config"
1415
)
@@ -150,6 +151,14 @@ func agentList(cmd *cobra.Command, args []string) error {
150151
return err
151152
}
152153
custom, _ := cli.LoadProjectCustoms(ws.Config.AgentsDir())
154+
// Block-defined agents too, exactly as the orchestrator registers them.
155+
// Without this the list shows only the ~20 built-ins and whatever is on
156+
// disk under .slmcode/agents, while a run happily dispatches to go-worker,
157+
// react-tester or shadcn-worker — agents this command told the operator do
158+
// not exist. `slmcode agent list` is the roster; a roster that omits every
159+
// language specialist and both frontend assemblers is how an operator
160+
// concludes a feature is missing when it is merely undisplayed.
161+
custom = append(custom, blockAgentSpecs(ws.Config.Root, custom)...)
153162
cli.Header("Agents")
154163
if ws.Config.ActiveStack != "" {
155164
cli.KeyVal("active_stack", ws.Config.ActiveStack)
@@ -187,3 +196,31 @@ func applyAgentFields(c *agents.CustomSpec, fields map[string]string) {
187196
}
188197
}
189198
}
199+
200+
// blockAgentSpecs returns the agent blocks the registry would register, minus
201+
// any id already present on disk — on-disk definitions win, the same precedence
202+
// the factory applies via ExtraCustoms.
203+
func blockAgentSpecs(root string, have []agents.CustomSpec) []agents.CustomSpec {
204+
reg, err := blocks.Load(root)
205+
if err != nil || reg == nil {
206+
return nil
207+
}
208+
seen := map[string]bool{}
209+
for _, c := range have {
210+
seen[strings.ToLower(strings.TrimSpace(c.ID))] = true
211+
}
212+
var out []agents.CustomSpec
213+
for _, ab := range reg.Agents {
214+
spec := ab.Spec
215+
if err := agents.NormalizeCustom(&spec); err != nil {
216+
continue
217+
}
218+
id := strings.ToLower(strings.TrimSpace(spec.ID))
219+
if id == "" || seen[id] {
220+
continue
221+
}
222+
seen[id] = true
223+
out = append(out, spec)
224+
}
225+
return out
226+
}

‎cmd/slmcode/cmd_util.go‎

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"path/filepath"
99
"regexp"
1010
"runtime"
11+
"sort"
1112
"strconv"
1213
"strings"
1314
"time"
@@ -494,9 +495,10 @@ func runDoctor() error {
494495
if len(pinned) > 0 {
495496
fmt.Println(cli.Warn(fmt.Sprintf("agents pinning LLM (override stack): %s", strings.Join(pinned, ", "))))
496497
fmt.Println(cli.Dim(" tip: slmcode stack apply <name> --clear-agent-llm or slmcode agent clear-llm <id>"))
497-
} else {
498+
} else if len(ws.Config.ModelRoles) == 0 && len(ws.Config.ModelEscalation) == 0 {
498499
fmt.Println(cli.Success("agents inherit stack/global LLM"))
499500
}
501+
reportModelRouting(ws.Config)
500502
// .slmcode/auth.json holds provider API keys; `slmcode commit` runs
501503
// `git add -A`, so an un-ignored .slmcode is a real leak path.
502504
if gs := gitignoreStatus(ws.Config.Root, ws.Config.SlmDir()); gs["ok"] != true {
@@ -832,3 +834,71 @@ func providerRejectedAuth(check readiness.Check) bool {
832834
code, _ := strconv.Atoi(m[1])
833835
return code == 401 || code == 403
834836
}
837+
838+
// reportModelRouting shows model_roles / model_escalation and checks that every
839+
// model they name is actually served.
840+
//
841+
// Two things were invisible before. "agents inherit stack/global LLM" was
842+
// printed unconditionally, which is simply false once model_roles pins a role —
843+
// the one line an operator reads to learn where their models go said the
844+
// opposite of the truth. And nothing validated the names: a typo in a pinned
845+
// model passed doctor at 100/100 and then failed mid-run, at the reviewer, after
846+
// minutes of real work. The model list is already fetched to answer "is the
847+
// main model available"; checking three more names against it costs nothing.
848+
func reportModelRouting(cfg *config.Config) {
849+
if cfg == nil || (len(cfg.ModelRoles) == 0 && len(cfg.ModelEscalation) == 0) {
850+
return
851+
}
852+
// Every distinct model the routing config names, in a stable order.
853+
wanted := map[string][]string{} // model → the roles/rungs that name it
854+
roles := make([]string, 0, len(cfg.ModelRoles))
855+
for role := range cfg.ModelRoles {
856+
roles = append(roles, role)
857+
}
858+
sort.Strings(roles)
859+
for _, role := range roles {
860+
m := strings.TrimSpace(cfg.ModelRoles[role])
861+
if m != "" {
862+
wanted[m] = append(wanted[m], "@"+role)
863+
}
864+
}
865+
for i, m := range cfg.ModelEscalation {
866+
if m = strings.TrimSpace(m); m != "" {
867+
wanted[m] = append(wanted[m], fmt.Sprintf("escalation rung %d", i+1))
868+
}
869+
}
870+
if len(wanted) == 0 {
871+
return
872+
}
873+
for _, role := range roles {
874+
cli.KeyVal("model_role", "@"+role+" → "+cfg.ModelRoles[role])
875+
}
876+
if len(cfg.ModelEscalation) > 0 {
877+
cli.KeyVal("model_escalation", strings.Join(cfg.ModelEscalation, " → "))
878+
}
879+
880+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
881+
defer cancel()
882+
served, err := models.Fetch(ctx, cfg)
883+
if err != nil || len(served) == 0 {
884+
fmt.Println(cli.Dim(" routed models not verified — the endpoint did not list its models"))
885+
return
886+
}
887+
have := make(map[string]bool, len(served))
888+
for _, m := range served {
889+
have[strings.ToLower(strings.TrimSpace(m))] = true
890+
}
891+
var missing []string
892+
for m, users := range wanted {
893+
if !have[strings.ToLower(m)] {
894+
missing = append(missing, fmt.Sprintf("%s (%s)", m, strings.Join(users, ", ")))
895+
}
896+
}
897+
sort.Strings(missing)
898+
if len(missing) > 0 {
899+
fmt.Println(cli.Warn("routed model not served: " + strings.Join(missing, "; ")))
900+
fmt.Println(cli.Dim(" the run will fail when it reaches that role — fix the name or load the model"))
901+
return
902+
}
903+
fmt.Println(cli.Success(fmt.Sprintf("every routed model is served (%d)", len(wanted))))
904+
}

‎docs/blocks.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ shareable: true # marketplace-ready flag (default: true)
6060
6161
## Predefined Language Packs (Builtin)
6262
63-
SLMCode ships **thirteen** language packs. Each one is a `pack` block composing a pipeline, a
63+
SLMCode ships **fifteen** packs. Each one is a `pack` block composing a pipeline, a
6464
quality block and language-aware agents; `slmcode init` picks one automatically (see
6565
[Detection](#detection-how-a-pack-is-chosen)).
6666

@@ -79,9 +79,18 @@ quality block and language-aware agents; `slmcode init` picks one automatically
7979
| 🐘 `php` | php | `php-worker` `php-tester` | `vendor/bin/phpunit --colors=never` | `vendor/bin/phpunit --colors=never` |
8080
| 🕊️ `swift` | swift | `swift-worker` `swift-tester` | `swift build` | `swift test` |
8181
| ⚙️ `cpp` | cpp | `cpp-worker` `cpp-tester` | `cmake --build build` | `ctest --test-dir build --output-on-failure` |
82+
| 🧩 `shadcn` | typescript | `shadcn-worker` `shadcn-reviewer` `react-tester` | `npx tsc --noEmit` | `npx tsc --noEmit` |
83+
| 🎨 `untitledui` | typescript | `untitledui-worker` `untitledui-reviewer` `react-tester` | `npx tsc --noEmit` | `npx tsc --noEmit` |
8284

8385
`slmcode blocks list` prints the live set; this table is a snapshot of it.
8486

87+
!!! tip "shadcn and untitledui are *methods*, not languages"
88+
The last two build React UI by **installing** components with the library's own
89+
CLI and wiring them up, rather than writing them by hand. You do not have to
90+
apply them: both are chosen automatically from the request and the project, and
91+
their agents ship registered. Applying one just pins the choice for good.
92+
See [Frontend: assemble or write](frontend.md).
93+
8594
Every pack also pins skills (`pin_skills: true`) — always `atomic-coding`, `specialist-worker`
8695
and `specialist-tester`, plus language-specific ones: `go` adds `go-table-tests` and
8796
`go-concurrency`, `typescript` adds `typescript-strict`, `react` adds `react-hooks` and

‎docs/changelog.md‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,147 @@ the failures felt like noise rather than progress:
592592
they are and not to repeat the last one; and the same unresolved defect
593593
reopens its existing ticket instead of stacking a new one every gate run,
594594
which is what made the board look like it was losing ground.
595+
### Fixed — found by running v0.21.0 on a live 30B
596+
597+
Everything in this subsection was found by driving the harness against a real
598+
local stack — oMLX serving Qwen3-Coder-30B — rather than by reading the code.
599+
Each item names the measurement that produced it.
600+
601+
### Fixed — the harness
602+
603+
- **Executable acceptance criteria were discarded before they could run.**
604+
`collapseToWorker` rebuilt the surviving task without `Criteria`, so whenever
605+
the splitter's tasks collapsed — the common shape on a small model — every
606+
criterion the model authored was dropped. Measured: the 30B emitted correct
607+
criteria with `go test ./...` as the verify command, and the run reached the
608+
reviewer with nothing to check, indistinguishable from a task that passed.
609+
- **Harness state followed the sandbox under `--isolate worktree`.** Memory,
610+
the derived graph and the metrics row take a project root and join
611+
`<root>/.slmcode/…` themselves, so they never saw `StateDir`. The isolated
612+
run wrote them into the throwaway worktree: an orphan directory survived every
613+
run (a late write re-created the directory git had just removed), and
614+
`git add -A` swept nine `.slmcode/**` files into the commit merged onto the
615+
operator's branch. New `Config.StateRoot()`; the isolated commit now carries
616+
only source.
617+
- **A green test command that ran no tests counted as verification.**
618+
`classifySmoke` checked `sr.OK` before the no-tests check, and `go test ./...`
619+
on a tree with no `_test.go` files exits **zero** — so the gate's own
620+
documented contract ("a 'nothing to run' exit is NoTests, never Green") held
621+
only for runners that fail on empty. Measured: a run told "both parts must
622+
have tests" wrote none and finished ✔ with `qa_gate green`.
623+
- **The board could deadlock on an abandoned task.** `runWave` is synchronous,
624+
so nothing is in flight at the top of the scheduler loop — yet a task left in
625+
`in_progress`/`in_review` still made `AgentWorkRemaining` report work in
626+
progress, and no path re-dispatches those columns. Measured: one stranded
627+
task, four dependents frozen behind it, ~9 minutes of correct work discarded
628+
and reported as a failure with the edit already on disk and compiling.
629+
Orphans are now re-queued, bounded by the existing attempt ceiling.
630+
- **The composer could drop the coordinator.** `applyBudgetClass` is applied to
631+
the heuristic composition only, so the class could budget `coord` and the
632+
composer silently omit it — 3 live runs out of 3. Coordination is restored
633+
because @coordinator acts on the *board*, which does not exist yet when the
634+
composer decides.
635+
- **Per-file specialist routing and the model ladder were both dead by
636+
default.** `runner.HasRole` was only wired inside the escalation branch, so
637+
on any install without a `model_escalation` ladder — nearly all of them —
638+
every feature that asks "is this agent registered?" answered no.
639+
- **A task's role never reached its language specialist.** `normalizeExecRole`
640+
applied `execute.default_role` only to an *empty* role, but `Task.Normalize`
641+
fills empty roles with `worker` first, so the chosen specialist was displayed
642+
in the composition and never used. Tasks now route to the specialist that owns
643+
their files, which is also what makes a mixed Go + React board run on two
644+
specialists at once.
645+
- **Duplicate workers on one file.** Two tasks scoped to the same file cannot
646+
share a wave (concurrent workers share one tree), so each cost a full wave and
647+
the second opened a file the first had rewritten. Same-file worker tasks now
648+
merge, with dependencies rewritten onto the survivor.
649+
- **Greenfield Go and web paths were dropped.** `isGreenfieldCreatePath`
650+
accepted `main.py` but not `main.go`, and knew no `cmd/`, `pkg/` or `web/`.
651+
Measured: a full-stack request finished **0 of 7 tasks with nothing on disk**,
652+
because every planned path was discarded and a worker forbidden to invent
653+
paths had nothing it was allowed to write. After the fix the same request
654+
produced a compiling Go backend and a React frontend.
655+
- **Two prompt few-shot examples were being copied as real work.** The splitter
656+
planned "add Sum to calc.go" — verbatim from its own example — on a greenfield
657+
request, and the composer's handoff carried `verify with go test ./...` into a
658+
React run for the same reason. Both examples are now shapes, not content.
659+
- **A refusal never said what it refused.** `"npx" can execute arbitrary code`
660+
is unanswerable when npx runs a different program every invocation; refusals
661+
now quote the command.
662+
- **`slmcode agent list` showed 20 of 59 agents.** Block-defined agents — every
663+
language specialist and both frontend assemblers — were registered by the
664+
orchestrator and invisible to the command that lists the roster.
665+
666+
- **`npx tsc --noEmit` was refused.** The bare `tsc` and `eslint` were already
667+
builtin-safe, but their npx forms were not — and in a JS/TS project those
668+
tools live in `node_modules`, never on PATH, so the npx spelling is the only
669+
one that runs. It is the react and shadcn packs' own typecheck and `qa_gate`.
670+
Measured: a live assembler run was refused for its own smoke command.
671+
- **A whitelisted prefix matched past a word boundary.** `npx tsc` also admitted
672+
`npx tsc-evil` — any package whose name merely starts the same way — because
673+
matching is a plain prefix test. The builtin list already carries trailing
674+
spaces (`tsc `, `eslint `, `find `) for exactly this; the npx entries now do
675+
too, and the same hole is closed in the `react` and `typescript` quality
676+
packs.
677+
- **A forged criteria header could switch the criteria gate off.** A worker that
678+
merely echoed `## Acceptance criteria` — plausible, since the reviewer
679+
contract in its own prompt names that heading — suppressed the review-time
680+
gate, and with nothing run `CriteriaUnverifiedInOutput` stayed false, which is
681+
the value that ALLOWS the reviewer fast path. The section's provenance stamp
682+
is now required, so a genuine section is still recognized (and its commands
683+
still not re-run) while a typed one is not.
684+
- **`model_roles` and `model_escalation` were invisible to `doctor`.** It
685+
printed "agents inherit stack/global LLM" unconditionally — false as soon as a
686+
role is pinned — and validated nothing, so a typo'd model passed at 100/100
687+
and failed mid-run at the reviewer. Both are now shown, every model they name
688+
is checked against what the endpoint serves, and an unserved one is a
689+
readiness finding (measured: 100 → 86) rather than a surprise.
690+
- **Criteria that could never run.** Told only in the abstract to write a
691+
runnable verify command, a live 30B wrote
692+
`go test -v ./... | grep -E '(TestX|PASS|FAIL)'`; the sanitizer refuses shell
693+
metacharacters, so every criterion on that board came back UNVERIFIED. The
694+
splitter now gets the concrete ✓/✗ example, and its own prompt budget — it is
695+
paid once per split, like the composer is paid once per run.
696+
697+
- **Correction rounds aimed at the wrong files.** When the QA gate stayed red,
698+
the synthesized tester verdict carried the literal string `"qa_gate red"` —
699+
and the corrective rewrite scopes its fix by mining file paths out of those
700+
failure strings, so a verdict naming no file fell back to "whatever finished
701+
most recently". Measured on a greenfield Go build: three correction rounds
702+
scoped to `pkg/tasks` while every compiler error was in `cmd/server/main.go`,
703+
which none of them was allowed to touch. The gate's own output is now carried
704+
through, so the fix is scoped at the file that is actually broken.
705+
- **Workers wrote against types they had never read.** Rule 1 said "ws_read a
706+
file before editing it" — nothing about *using* one. Measured: a worker
707+
scoped to `cmd/server/main.go` used `task.Title`, `task.CreatedAt` and
708+
`task.UpdatedAt` on a `Task` that a sibling task had defined with three other
709+
fields, and the package would not compile. Reads were never scope-limited;
710+
the worker simply had no instruction to use them for cross-file APIs.
711+
712+
### Added — frontend assemblers
713+
714+
- **`shadcn` and `untitledui` packs: build React UI by INSTALLING components.**
715+
Hand-writing a dialog with focus traps is where a 7–32B model spends its
716+
runway; installing a reviewed one and wiring it up is imports, props and
717+
layout. Both ship enabled — nothing to install, nothing to apply — with a
718+
worker, an assembly reviewer that rejects a component you hand-rolled when the
719+
registry already had it, a pipeline and quality gates.
720+
- **The method is chosen from evidence and announced.** Your request wins first
721+
(name a library, or say *from scratch*), then the project's own markers, then
722+
greenfield defaults to assembling; an existing app with no markers keeps
723+
writing by hand. The run prints which and why.
724+
- **Scoped shell access.** `npx` stays an executor; five subcommands of two
725+
named packages are allowed, matched structurally so `npx shadcn add`,
726+
`npx --yes shadcn@latest add` and the legacy `shadcn-ui` name all work. An
727+
`add` naming a URL, an `@registry`, or a local path is refused — both CLIs
728+
accept those in the same position as a component name, and they resolve to a
729+
registry nobody reviewed.
730+
- The assembler agents carry each CLI's real contract, verified by running them:
731+
shadcn's `init` needs an explicit `-b` or it stops on an interactive menu even
732+
with `-y`; Untitled UI matches names fuzzily and wrongly (`buttons` installs
733+
`app-store-buttons`, and it does not error).
734+
- See [Frontend: assemble or write](frontend.md).
735+
595736
## v0.21.0 — 2026-08-27
596737

597738
Adapts the structural ideas from [zeroshot](https://github.com/the-open-engine/zeroshot)'s

0 commit comments

Comments
 (0)