Skip to content

BoringStack full-stack build loop (Phase C) + tooling alignment#83

Open
agjs wants to merge 43 commits into
mainfrom
feat/boringstack-build-loop
Open

BoringStack full-stack build loop (Phase C) + tooling alignment#83
agjs wants to merge 43 commits into
mainfrom
feat/boringstack-build-loop

Conversation

@agjs

@agjs agjs commented Jul 12, 2026

Copy link
Copy Markdown
Owner

What

The BoringStack full-stack build loop: for web apps, the harness scaffolds/drives BoringStack (Bun+Elysia API, Vite+React UI, Postgres/Drizzle) one resource at a time — the harness runs BoringStack's own generators + deterministic wiring, the model fills domain logic + tests, evaluated by BoringStack's own validate/check. Supersedes the UI-only web scaffold that couldn't converge.

New: packages/core/src/loop/boringstack/{plan-resources,wire-resource,generate,gate,refine-prompt,build,exec,case}.ts (+ tests, 37 pass), driven by the existing greenfield engine (runGreenfield). Entry: TSFORGE_BORINGSTACK=1 bun packages/core/scripts/headless-build.ts "<goal>" react <clone>.

Built via subagent-driven-development (fresh subagent per task + two-stage review + a final whole-branch review); all review findings fixed.

Proven

  • Gate proven GREEN live on a real BoringStack clone: a full slice (API resource + wiring + db:push + UI feature + generate:api) passes BoringStack's own validate/checkAPI_EXIT=0, UI_EXIT=0, ROOT_EXIT=0.
  • Gate runs the way a developer runs it: bun install + host validate (not a throwaway container — deps live in per-app volumes), using BoringStack's pinned tooling (bun run format, never bunx-latest — a version drift false-failed the gate).
  • Full bun run validate green.

Also in this PR

  • Tooling aligned with BoringStack (prettier 3.9.4, eslint 10.6.0, typescript-eslint 8.62.1) + .github/dependabot.yml (dev-toolchain grouped) — so a version drift never silently breaks a gated build.
  • Removed the self-harness UI-only web corpus (evaluate-web) and retargeted the campaign to the spec corpus (rest of self-harness intact).

Pending (honest)

  • End-to-end model build: a live smoke ran the full harness pipeline (plan→generate→wire) but crashed at generate:api because the polluted test clone's api was unhealthy — a clean end-to-end proof needs a fresh, healthy clone.
  • Per-project isolation (dynamic compose project name / host ports / gate image / DATABASE_URL) — hardcoded-for-one-project today; needed for concurrent multi-project builds.
  • Full removal of the legacy UI-only scaffold subsystem (scaffold tools, web-templates, web-gate, staged-build, --web) — core-woven (~25 files); planned as a follow-up (see docs/superpowers/plans/2026-07-12-boringstack-build-loop.md Task 8).
  • Driver robustness: generate:api api-readiness poll; a resume wrapper for the single-invocation headless path.

Do not merge without review — flagging the pending items above.

agjs added 30 commits July 12, 2026 12:25
…ll-stack build loop against a scaffolded clone; type onEvent as Reporter
…idate, not docker-run) + use pinned bun-run-format not bunx-latest

The single docker-run gate had no toolchain (tsc: not found — deps live in per-app volumes). Run the composed gate on the host after bun install, with DATABASE_URL pointed at the published localhost Postgres. And the generator formats via BoringStack's OWN pinned prettier (bun run format), never bunx-latest, which caused a union-formatting drift that failed the gate.
… typescript-eslint 8.62.1) + add dependabot

BoringStack is the strict reference the harness gates against; keep tsforge's shared toolchain on par so a version drift never silently breaks a build (a bunx-latest prettier mismatch already caused a union-formatting false-fail). Reformatted 11 files under prettier 3.9.4. Added .github/dependabot.yml (weekly; dev-toolchain grouped so lint/format/type deps move as one unit) + github-actions updates.
…e) so generate:api syncs the OpenAPI client — fixes root 'OpenAPI drift' gate failure
…-web)

Removes web-build evaluation system and all web: task references from self-harness:
- Delete evaluate-web.ts and self-harness-web.test.ts
- Remove runOneWeb(), webTimeoutMs option, and web: task dispatch from evaluate.ts
- Remove web-corpus exports (runWebTaskOnce, WEB_SLOW_THRESHOLD, WEB_RUN_TIMEOUT_MS, IWebEvaluateOptions, IWebRunOutcome) from index.ts
- Simplify split.ts: remove web: namespace validation logic (webSlugs parameter)
- Update self-harness.ts: remove BENCHMARK_CATALOG import and webSlugs argument

Keeps the rest of self-harness fully intact: Algorithm-1 loop, overlay, mining, proposer, campaign, all spec/code corpus functionality.

All tests green: bun run typecheck, bunx eslint packages, bun test packages/core/tests/self-harness-*.test.ts (56 pass)
… to the spec corpus (auth/fix-regression proof; boringstack corpus is the future successor)
…the live-smoke 'fetch failed' race after resource hot-reload)
… retry (skip codegen if the resource exists — a feature can now be re-attempted to fix gate failures instead of crashing on 'directory already exists')
…he next attempt's prompt (IFeature.lastError) so the model fixes real errors instead of rebuilding blind; dedupe toCamelCase in refine-prompt
…rompt (canonical app.handle(Request) route-test, ApiErrors not throw, TS strict rules) — targets the Elysia-typing failures that stuck the live smoke
…d the dead UI-only web-build path: scaffoldWeb/buildStaged/web-gate/delegation) — Task 8 chunk C1
…ive REPL (keep boringstack/astro scaffold + normal session) — Task 8 chunk C3
…caffold_web/ui/routes tools

Surgically remove the web-only staged-build machinery and its three scaffold tools:
- Delete staged-build.ts and the two-phase build infrastructure (designBuild/implementBuild/buildStaged/generatePlan)
- Remove scaffold_web/ui/routes tool definitions and dispatch handlers
- Remove SetupWebFn type and setupWeb callback threading
- Update session.ts: remove scaffoldWeb/scaffoldUi config, remove 4 public methods
- Update agent.constants.ts: remove 3 tool names, specs, and definitions
- Update execute-tool.ts: remove 3 handler entries
- Update turn.ts, tool-context.ts, cli/repl.ts: remove setup callbacks
- Clean up 3 test files (staged-build.test.ts, staged-gate.test.ts, scaffold-routes-idempotent.test.ts)
- Remove or update 9 test cases that asserted scaffold-tool behavior

All 4 validation gates pass: typecheck, eslint, e2e:pty, test suite (2206 pass)
…lateral-deleted them; the tools survive as general research tools)
…er build BACKEND

pull_conventions was gated behind flags.webTools(), so boringstack builds
(TSFORGE_BORINGSTACK, not TSFORGE_WEB) never got the conventions tool — a real
convergence bug. toolsFor now takes offerConventions (default false); a build
backend that ships a convention library opts in via ISessionConfig.pullConventions.
headless-build sets it true. Web being on no longer drags the tool in.
…th PULL

The 'HOW TO WRITE THIS RIGHT (boringstack)' guide push fired on ANY gated build
that tripped a mapped rule — so a plain CLI build tripping no-restricted-syntax
got boringstack/web-flavored advice (DOM/query narrowing) labeled boringstack.
Now gated on ILoopState.conventionsEnabled, set from the same ISessionConfig
.pullConventions signal that offers the pull_conventions tool: push + pull
activate together, a plain build gets neither. New test locks in the withheld
case.
…before gate

Two blockers found in a live build (Bookmark resource, stuck 0/3):

1. Test-helper catch-22 — new:resource generates the Drizzle table but nothing
   added it to tests/helpers/db.ts's schema re-export. The no-direct-db-in-tests
   rule forbids importing the table from the schema, but the sanctioned helper
   didn't expose it either, so ANY test referencing the table was trapped (the
   model diagnosed this exactly and had no legal fix). Adds a 4th deterministic
   wiring edit (wireTestHelperFile) — idempotent, existence-guarded.

2. Prettier thrashing — the model burned attempts guessing prettier's exact
   line-break formatting, which is deterministic and 100% auto-fixable. implement
   now runs 'bun run format' (pinned, never bunx-latest) over both apps after the
   model writes and before the gate — a dev gets format-on-save, so should the
   model. Best-effort; the gate stays the source of truth. Neither change relaxes
   the gate.
agjs added 13 commits July 12, 2026 19:42
…ailures

Every 'stuck 0/3' this session traced to the SAME harness bug, not model
weakness: the gate ran BoringStack's whole api suite, and a pristine scaffold
already fails 5 base tests (email-template render + validateEnv placeholder
domain) that the model is FROZEN out of. So it was handed an impossible task —
turn a gate green that was red on arrival, on files it can't edit — then stamped
'stuck'.

Fix: capture the baseline failure set on the pristine scaffold before any model
work, then grade each feature DIFFERENTIALLY — a red gate passes iff every
failure is a pre-existing baseline failure (feature introduced none). Novel
failures are the only feedback the model sees, so it never chases base defects.
A red baseline is surfaced LOUDLY (the scaffold itself should be fixed). Empty
parse on a non-zero exit stays failed (conservative). Not a gate relaxation: the
full gate still runs on everything the model touches or newly breaks.

extractFailures/novelFailures are pure + unit-tested; differential gate behavior
covered in boringstack-build.test.ts.
…tier

A stuck build's residual was 5 padding-line-between-statements lint errors —
deterministic, auto-fixable by eslint --fix, but the auto-fix pass only ran
prettier (which doesn't touch that rule class). Now runs 'bun run format' THEN
'bun run lint:fix' per app before the gate, so no auto-fixable lint costs the
model an attempt. Genuine violations (no-non-null-assertion, missing audit) are
correctly left for the model to fix.
…ck build path

The UI-only Vite/React scaffold (scaffold_web/ui/routes, staged web gate, browser
smoke) was removed; web apps are now built on a real BoringStack clone. Updated the
Astro docs site + internal harness-subsystems.md to match:

- delete scaffold/web.mdx + drop it from the sidebar/DocMap/Capabilities nav; repoint
  all links to /scaffold/boringstack/
- document the build loop (plan → generate → deterministic wiring → model fills domain
  → prettier+eslint --fix auto-fix → baseline-aware differential gate → freeze) in
  loop/greenfield.mdx + loop/gate-floor.mdx
- drop removed tools from the agent tool table (add pull_conventions); rewrite the
  plan-mode staged-web section; remove dead flags (--web mode, TSFORGE_A11Y/SCREENSHOTS)
- rewrite the web-scaffold subsystem section in harness-subsystems.md to the boringstack
  build subsystem

Docs site builds clean (47 pages).
Wires the existing expert handoff (capabilities.expert = deepseek-4-pro) into the
boringstack build loop — the rung above the per-attempt feedback the path already
has. Adds an optional IGreenfieldDeps.rescue hook: run.ts calls it ONCE before
parking a stuck feature and gives the result a final evaluation; green ticks the
feature, anything else parks as before (generic greenfield leaves rescue unset —
fully backward compatible, existing stuck tests unchanged).

boringstackDeps.rescue hands the stuck file + its exact novel-failure errors to the
expert via the existing resolveExpertAsk/resolveStuckFile/runExpertHandoff, then
re-runs the auto-fix pass. Opt-in via TSFORGE_EXPERT_RESCUE (expert is a paid API);
off/unconfigured → no-op. This is the automated form of 'a stronger hand steps in
when the local model is stuck' — the escalation that would have given conv4/conv5's
Elysia-typing feature a real shot before parking.

Tests: run.ts escalation control flow (lands-green ticks / can't-help parks /
applied-but-still-red parks once, no loop) + boringstack no-op when disabled.
The resource planner prompt invited 'entities, services, API endpoints' as separate
items, so a bookmarks goal produced BOTH 'Bookmark' and 'BookmarkService' — a whole
redundant generation cycle (BoringStack's new:resource already builds every layer
from one entity). Two guards:

- prompt rewritten to 'one entity = one resource; there is no separate XService/XApi/
  XRoutes — the service and endpoints are part of the entity; list the smallest set'
- dedupeLayerVariants() structurally drops a <Entity>Service/Api/Routes/... resource
  when the base entity is also present (belt-and-suspenders for when the model splits
  anyway); a genuinely standalone suffixed id with no base entity is kept

This directly prevents the redundant cycle we watched waste ~15 min in build #5. Not a
constraint on how the model builds — just stops asking for work already covered.
…line capture

A pristine BoringStack scaffold fails its OWN gate on a fresh clone — not the model's
fault. BoringStack's rename step rewrites identifiers repo-wide, which leaves two
documented follow-ups undone: 'bun run regen' (resync OpenAPI client + ACL + doc
catalogs → else OpenAPI-drift RED) and 'bun run format' (re-prettier lines whose width
the rewrite changed → else format:check RED). The scaffold ran rename+setup but skipped
these, so every build opened with a red baseline warning.

driveBuild now runs regen + format after install, before baseline capture. Verified: a
fresh scaffold that was RED on the full composed gate (api validate && ui validate &&
check) now exits 0 — OpenAPI drift ✓, ACL drift ✓, format ✓. Pairs with the BoringStack
test-fixture fix (c2ab47e, separate repo). Best-effort: a residual defect still falls to
the differential gate rather than aborting.
…gate

Build #6 stuck at the JUDGE stage (gate green, but the service ignored description/
priority and close didn't set status) — yet the expert rescue no-op'd. It only looked
for a file PATH in the failure, and a judge critique is prose with none, so
resolveStuckFile found nothing.

New rescueFileFor(): gate stuck → the file named in the errors (as before); judge stuck
→ fall back to the resource's <camel>.service.ts, where the domain logic the judge
flags lives. So the expert now gets the service file + the judge's critique and can
implement the missing fields / state transition — exactly the case where a stronger
model should step in. Null only when neither resolves. Unit-tested (gate path, judge
fallback, no-file).
Complements BoringStack's parameterized compose host ports: the scaffold now assigns a
free host port to every binding (allocateHostPorts) and writes them to the clone's
compose/.env, so a scaffolded project boots ALONGSIDE the dev stack (or another project)
without colliding on 5432/7330/7331/… The compose project name is already per-project via
rename-project.sh; this closes the ports half. The handoff reports the api/ui/postgres
ports. allocatePort is injectable for deterministic tests; a repo tsforge didn't configure
is unchanged (compose defaults to the upstream ports). Proven: a fresh scaffold writes
free ports (e.g. postgres 52662 / api 52664 / ui 52665) into compose/.env.
… baseline

The differential-gate feedback always appended '; N baseline failure(s) hidden' —
confusing noise when the baseline is green (N=0). Now the suppression note only
appears when there's actually something hidden.
…xing them

Declaring params only to `_`-prefix and ignore them is dead weight when TS lets a
stub/impl omit unused TRAILING params entirely. Removed them from the evaluate/gate/
browser/judge impls and the exec test fakes (async () => … satisfies the type). The
`_`-params that remain elsewhere are LEADING/MIDDLE positionals that can't be omitted
(a later param is used) — the one legitimate use of the convention.
…ale describe

- capabilities: drop the removed 'vite (web)' from the scaffold blurb (wizard only
  offers boringstack/astro).
- wizard (boringstack only): two optional steps — admin email (validated) + admin
  password (masked, min 12). Blank = skip (sign up in-app instead).
- thread the creds answers → plan → applyScaffold, which writes SUPERUSER_EMAIL/
  SUPERUSER_PASSWORD to compose/.env BEFORE boot (co-located with the port write), so
  the migrate task's db:seed creates the admin on first boot. Password is secret:
  on disk, redacted in the summary.
- tests: creds land in compose/.env + password never in the summary; absent when not given.
Design for a product-first, interactive planning phase before the greenfield build
loop: human describes the product (+ mockups via vision), a config-routed planner role
(default deepseek-4-pro, swappable) PROPOSES the plan (domain model + feature slices +
UI intent + verification contract), human approves once, then autonomous per-slice
build carrying the approved plan as shared context. For greenfield BoringStack an
approved plan is a HARD precondition (reuses solo_spec_gate). Reviewer role deferred.
TDD task-by-task plan on feat/boringstack-build-loop: planner capability role (T1),
product-plan type + .specs I/O (T2), planner proposal (T3), approval gate (T4),
refinePrompt context threading (T5), build precondition + build-from-slices (T6),
interactive plan orchestration (T7), REPL/wizard/headless enforcement (T8).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant