ForgeKitFORGEKIT

Build Log

Alloy — Runner Bridge and First Real Claude Execution

July 19, 2026

Proved Alloy can initiate real Claude Code execution via a separate local runner process (verified twice via CLI/SDK capability spikes, real session IDs, real permission boundary), built the full runner/command/event pipeline end to end and typechecked clean, then pivoted before a live UI click to a screen-by-screen completion approach after the Charge/Ignite mockup gap surfaced live.

AlloyForgeKit OSRunnerExecution BridgeArchitecture
2
capability spikes run live (CLI + SDK) confirming existing-subscription auth, zero separate API billing
3
real permission-boundary attempts tested live before finding one that actually worked (disallowedTools, allowedTools+dontAsk both failed silently; PreToolUse hook via --settings verified working)
4
new authenticated API routes (register/claim/events/complete) — the full runner<->Alloy protocol
3
real bugs found and fixed live during end-to-end testing: proxy.ts blocking /api/runner/* entirely, bare dotenv/config missing .env.local, createWorkpiece redirecting to a /workpieces/[id] route that was never built

Timeline

Start
Synthesized a revised Charge from a detailed external chat review, then ran the Session Risk Forecast and a lightweight Arc-lifecycle grep-check against lib/actions.ts
Confirmed advancePhase() is hard forward-only (PHASE_ORDER.indexOf + Math.min clamp) — a real, evidence-based finding, not assumed from the chat's claim
+30m
Ran live capability spikes: CLI and SDK, both against a real read-only prompt in products/alloy
Both confirmed apiKeySource: none (existing subscription, no API billing) with real session IDs and structured stream-json events
+50m
Discovered the real permission-boundary gap: allowedTools/disallowedTools/permissionMode did not reliably block Bash or PowerShell
Found and verified the fix live — a PreToolUse hook loaded via --settings, modeled on this repo's own gate-check.mjs pattern, correctly blocked both with real permission_denials evidence
+1.5h
Built the schema additions (execution_commands, arc_event provenance/source_sequence), ExecutionProvider interface, CLI adapter, and standalone runner process
tsc clean throughout; migration generated and pushed to the real, already-provisioned Alloy Neon database
+2h
Built the 4 runner-facing API routes, runner-auth.ts, wired Ignite's real startExecution Server Action, added setPhase() to fix the forward-only phase gap
tsc clean; fresh Dependency Gate evidence written per new trigger
+2.5h
Started the real Alloy dev server (discovered already running on port 3911, reused rather than starting a duplicate) and the real runner process
Found and fixed 3 real bugs live: proxy.ts blocking /api/runner/* via Clerk, bare dotenv/config missing .env.local, createWorkpiece redirecting to a nonexistent /workpieces/[id] route
+3h
Zeb drove the browser walkthrough live: created a real Workpiece, started a real Arc, reached the real Charge form, advanced to the real Ignite screen with the runner listening
Runner registered and polled successfully; Ignite button rendered correctly
+3.2h
Zeb flagged that Charge/Ignite look nothing like their mockups (ChargeLayout.png/IgniteLayout.png) — surfaced a real, pre-existing gap: those mockups show a rich cockpit (Crucible, Anvil, Mechanism Rack, readiness %) that a PRIOR session deliberately did not build, because the underlying data doesn't exist yet and fabricating it would repeat a mistake Zeb had personally corrected before
Initially answered evasively; corrected after Zeb called it out directly
+3.4h
Zeb selected 'full mockup parity including fabricated numbers' from an offered menu; Claude paused instead of building it, naming the direct conflict with the app's own documented honesty standard, and asked Zeb to confirm in his own words rather than proceeding on a menu selection that may not have reflected real intent
Zeb clarified: wants systematic screen-by-screen completion (starting with Home), not a one-off Charge/Ignite patch
End
Zeb confirmed the capability-spike proof (real Claude execution, real session IDs, verified permission boundary) was sufficient to close this session without a final live UI click; pivoted the next session's focus to screen-by-screen UI completion
Runner process stopped cleanly; retro written

What shipped

products/alloy/lib/db/schema.ts — execution_commands table (durable, idempotent runner commands), arc_events gained source/sourceSequence/rawProviderPayload/occurredAt columns, executions gained cwd, arc_event_type gained 8 runner/provider event types

products/alloy/drizzle/migrations/0001_giant_lizard.sql — generated and pushed to the real Alloy Neon database (first real schema deploy for this app)

products/alloy/lib/runner-protocol.ts — ExecutionProvider interface, ProviderCapabilities, NormalizedProviderEvent — provider-neutral by construction

products/alloy/runner/providers/claude-cli.ts — the CLI adapter, using a verified PreToolUse-hook permission boundary (not CLI flags, which were tested and found unreliable)

products/alloy/runner/index.ts — the standalone runner process: registers with Alloy, polls for commands, runs them through the provider, posts real events back

products/alloy/runner/package.json + tsconfig.json — a separate, independently-runnable Node package, deliberately outside the root npm workspaces (matches the topology decision that Alloy and the runner never assume they're on the same machine)

products/alloy/lib/runner-auth.ts — shared-bearer-token auth for the runner<->Alloy boundary (not Clerk — the runner isn't a browser session)

products/alloy/app/api/runner/{register,claim,events,complete}/route.ts — the 4 authenticated API routes implementing the protocol

products/alloy/proxy.ts — fixed to exempt /api/runner/* from Clerk's auth.protect() (found live: without this, the runner could never reach Alloy at all)

products/alloy/lib/actions.ts — startExecution() (the real Ignite action), setPhase() (fixes the forward-only phase-transition gap found via direct grep), fixed createWorkpiece()'s redirect to a route that never existed

products/alloy/components/IgnitePanel.tsx + app/ignite/page.tsx — the real, minimal Ignite screen (button + execution status), intentionally not matching the full IgniteLayout.png mockup — see nextItems