The working-session stack: host topology, faculties consolidation, thread admission + supervision, plugin threads, root authority, the ui_* producer threads + autoresearch loop - #348
Merged
Conversation
The engine self-mints `instanceId = ueid('bp_')` and stamps it on every
trace. With an ACP ingress landing later, the host owns session identity
policy (mints on `session/new`, passes a client's old id on `session/load`),
so the engine must ACCEPT a host-supplied session id — it never mints one
and never returns ids. No rename: the two ids are separate axes and both
live on the wire.
- `TraceBase` gains `sessionId: string` alongside `instanceId`; a new
exported `TraceBaseSchema` is the one home for the trace wire's common
shape, so per-kind validators derive from it instead of hand-mirroring.
- `behavioral()` gains `{ sessionId?: string }`; the frozen return object
is unchanged. Session id is computed once at factory time
(`options?.sessionId ?? instanceId`) and stamped on every trace next to
`instanceId`; `resumePendingThreadsForSelectedEvent` stamps it on
interrupt traces too.
- The frontier faculty speaks the same wire with no drift: replay/explore/
verify inputs accept `sessionId` (AJV `nullable`, defaulting to the
`instanceId`), and every synthetic trace it builds (frontier, selection,
deadlock) carries both ids.
- `serve.ts`/`trace-consumer.ts` are pass-through only — no construction
sites existed in production code; only spec fixtures gained the field.
- `skills/behavioral/references/behavioral.md` factory signature updated.
Validation: `bun --bun tsc --noEmit` plus the behavioral, frontier
faculty, and cli serve/trace-consumer suites (211 specs).
The direction was dropped from the plan; these comment-only references
survived the sweep. No behavior change — doc lines and one doc-comment
re-wrap only.
- behavioral.ts / behavioral.types.ts / skill reference: session identity
is owned by "the host layer", no protocol names
- controller.types.ts: Transport seam examples now generic ("native IPC")
- store/faculty.ts: swap-backings list drops the Tauri engine example
Validation: bun --bun tsc --noEmit clean; comment-only changes, no
executable surface touched.
Slice 1 of the attach-or-start lifecycle: the running instance owns <home>/instance.pid. acquireInstanceLock takes the lock when the home is free, blocks with the live holder pid when one runs, and reaps a stale pidfile (pid no longer alive) before acquiring. Injectable home + pid; release removes the pidfile for the SIGINT/SIGTERM terminate path.
Slice 2: the host gains its single listener, <home>/instance.sock — a Bun.serve with the unix option whose WebSocket carrier speaks the same JSON-RPC lane as stdio (one text frame = one message). The host-side dispatcher and egress wiring are extracted from serve.ts (dispatchToRuntime, wireRuntimeEgress) and reused, one protocol one dispatcher multiple carriers. Redacted traces and ui_* selections fan out to every client; socket file removed on close, stale file reaped at start. Transport-shaped clients exercise the real server over ws+unix in the specs.
Slice 3: a single-file readline TUI (src/cli/tui.ts, no framework) with exactly three primitives: emit (egress-only, Bun.color-resolved kind severity colors that never touch the wire or a prompt), prompt (a raw line resolves to a tui_command ingress event), and select (a numbered choice resolves to tui_select). The tui_* family is ingress-only with AJV detail schemas at the top of tui.ts; the root guard pack now derives its guard entries from TUI_DETAIL_SCHEMAS alongside the controller wire, so malformed tui_* details block visibly. Lines are consumed through an explicit queue (terminal:false readline drops buffered lines between questions).
Slice 4: bare `behavioral` (no subcommand) is the attach-or-start entry. A live instance (pidfile held) is attached to over <home>/instance.sock with "attached to running instance <id>"; a free or stale home starts the foreground instance — engine + socket host + TUI in one process, the shell supervising. The instance own TUI rides the socket like every other client (no in-process fast path); SIGINT/SIGTERM terminate the engine and remove the pidfile and socket (the daemon-door discipline). The router gains a lazy `default` entry; --help and subcommands still never load the composition graph. Two-process specs drive the real lifecycle across Bun.spawn children: attach notice, cross-process trigger, no second instance, stale-pidfile reap, SIGTERM cleanup. File IO is Bun-native (Bun.file/Bun.write/Bun.file().delete()); node:fs only where Bun has no equivalent (mkdir, socket-file existence).
Slice 5: the host single listener now serves the bundled controller GUI at /.behavioral/connect.js alongside the WebSocket upgrade — the browser carrier and the TUI carrier are two clients of one Bun.serve (Carriers/H). bundleController is promoted from the controller fixture to src/controller/ (one home for the bundle; the fixture re-exports it). In prod the AOT bundle is built once and cached; --dev (start-time only, attachers cannot flip a running instance) rebuilds per request for source editing. Engine and wire are identical in both modes. The router default forwards argv so the bin entry recognizes the flag.
Slice 1 of the mcp-into-shell handoff (.prompts/mcp-into-shell-security-faculty.md): the lightweight HTTP JSON-RPC client that replaces the @modelcontextprotocol SDK transport layer. - `send({ url, method, params, id, getAuthToken, fetch })` — one stateless HTTP POST per call; envelope is `{ jsonrpc, id, method, params }`. - Protocol-agnostic: no MCP knowledge, no `_meta`, no protocol versions — the MCP layering lives in the thread pack (later slice), not the client. - Auth is a seam: injectable async `getAuthToken`; a vended token rides a bearer header, absent means no header. The client does not know OAuth. - Errors-as-data: HTTP non-OK, JSON-RPC error payloads, malformed bodies, and network failures return `{ ok: false, error: { code, message } }` — no throw crosses a transport/protocol failure. - `fetch` injectable; specs script it to exercise the real request/decode paths (spec: shell/tests/rpc.client.spec.ts, 8 specs).
Slice 2 of the mcp-into-shell handoff: remote tool execution as a third
op beside `run` and `shell` — transport-shaped, not MCP-shaped.
- `ShellRpcOpInput { op: 'rpc', url, method, params?, timeoutMs? }` joins
the op-discriminated `ShellCallInput` union (anyOf branch, strict AJV).
- `runOp` dispatches `rpc` to the Slice-1 client; `getAuthToken` is an
injectable module seam returning undefined (the security faculty's
credential round-trip lands in Slice 4).
- Cancel rides the existing `active` map: an rpc `Execution` carries an
AbortController; `shell_cancel` aborts the in-flight fetch (first stop
wins — an abort is never mislabeled a remote error).
- Errors-as-data at both layers: remote JSON-RPC errors and HTTP non-OK
surface as `code: 'error'` + `remoteCode` (the remote discriminant,
for retry policy); cancel/timeout as their own statuses.
- Specs through the real faculty process boundary against a local
JSON-RPC HTTP server (rpc-op.spec.ts, 6 specs); the client gains a
signal seam with an abort spec (rpc.client.spec.ts, 9 specs).
…y faculty Slice 3 of the mcp-into-shell handoff: credentials/OAuth extracted from the mcp faculty into `src/faculties/security/` — cross-cutting and stateful, per the Direction/A ruling. - Wire kinds `credential_request` / `credential_result` / `credential_cancel` join the faculty event registry (schemas + validators in faculties.types.ts; the uniform WorkerResultDetail envelope; `credential_result` is the lane's only inbound kind — the pump re-enters results only). - The process vends fail-closed: broker env-data first (MCP_BROKER_URL + boot secret), then the keychain floor; absent -> error data naming the server. `credential_cancel` marks an in-flight vend (first writer wins). - keychain-oauth-provider MOVES from mcp/ to security/ and drops the SDK: plain types in security/types.ts (the issuer-stamp storage pattern, OAuthClientInformationContext, the discovery blob), a plain IssuerMismatchError and selectClientAuthMethod replace the SDK imports. tokens(ctx) now enforces the issuer binding its contract documented (a mismatched blob is treated as absent). - The keychain floor is `vendKeychainToken` (one home, shared with the mcp faculty's floor); broker env keys move to security/types.ts, re-exported by mcp/types.ts until deprecation. MINIMAL: expiry detection deferred. - Specs: provider suite (issuer binding, floor vend, auth selection) and the wire suite through the real process boundary (broker vends, absent-credential errors, cancel, space echo). 21 pass; tsc clean.
…binding lane
Extends the Slice 3 skeleton with the out-of-band binding lane, modeled on
the you.com MCP server's host-supplied override pattern: host-only parameters
ride `detail.ctx` beside `input` (the model-facing arguments), never inside
them — the thread pack that discovers the authorization server stamps the
resolved `issuer`; the op never does.
- `credential_request` events gain optional `detail.ctx` (`{ issuer?: string }`)
— loose at the shared wire gate (faculties.types.ts), strict at the
security faculty's boundary (`SecurityRequestContextSchema`); a ctx that
fails its shape is error data like any invalid input.
- The keychain floor (`vendKeychainToken`) takes the ctx issuer: a blob
stamped for a different AS is treated as absent, never vended to the wrong
server (the provider's issuer-match rule, now shared via `issuerMatches`).
Unstamped legacy blobs and ctx-less reads keep the most-recently-saved
contract.
- Resolves the Slice 3 open question: the floor's issuer binding now has a
comparison base (the caller's resolved AS issuer) instead of none.
Specs: ctx-carrying requests validate and vend over the wire; a ctx failing
its boundary is error data; the floor binds on ctx.issuer (mismatch ->
absent, match -> vended, unstamped -> legacy-accepted). tsc clean, 114
targeted specs pass.
…o security
Slice 4 of the mcp-into-shell handoff: the rpc op's credential round-trip is
thread-orchestrated — the op never knows OAuth and never talks to the
security faculty directly.
- `ShellRpcOpInput` gains the declarative auth seam: `auth: true` without a
token short-circuits as typed `credential_required` (no unauthenticated
remote call, first-writer loop bound) echoing the originating request —
the replay capture payload, mirroring the mcp spine's
authorization_required. The replayed call carries the vended bearer in
`authToken` (thread-injected, never model input; the redaction floor now
scrubs `authToken` values from traces).
- `shell/rpc-auth.threads.ts` — the vend-and-replay spine: the requestor
transforms a first-attempt credential_required into
`credential_request { serverUrl }` with the original call riding
`ctx.echo` (the you.com MCP host-supplied out-of-band lane); the replayer
joins the vended `credential_result` and replays the shell_request with
the bearer merged in. The security faculty echoes `ctx.echo` verbatim on
the result — the join rides one round-trip, no store capture needed.
- bProgram wires the security faculty (default-on, `security` override for
env-data hosts), routes credential_request/cancel, and mounts the seam
pack when shell+security are both on; `Faculty` gains 'security'.
- MINIMAL: a failed vend stays inert (the caller already holds the typed
credential_required error; the failure is visible in traces). No retry
policy — the generic retry thread pattern covers it later.
- Specs: op short-circuit + bearer-injection through the real process
boundary; the spine's requestor/replayer/loop-bound/absent-credential
gates against the real engine; the full composition end-to-end — real
loopback broker, real JSON-RPC endpoint, vend, replay, bearer observed.
tsc clean; full suite 675 green.
Slice 5 of the mcp-into-shell handoff: the MCP layering lives in `shell/remote-mcp.threads.ts` over the generic `rpc` op, and the mcp faculty — with the `@modelcontextprotocol/*` dependency — is gone. - Request stamping: every pack-issued rpc op carries the reserved `_meta` envelope (`io.modelcontextprotocol/protocolVersion` + `clientInfo` + `clientCapabilities`) and the `MCP-Protocol-Version` header (the op gains `headers`); results never carry the request-envelope keys. - Discovery: `remote_mcp_discover` -> `server/discover` -> `tools/list` -> the store registry (`remote-mcp` collection, keyed by server url, alongside skills/plugins) + `remote_mcp_discovered` surfacing. - Execution: `remote_mcp_call` -> `tools/call` -> `remote_mcp_call_result`. - MRTR (the 2026-07-28 contract): an `input_required` result (reserved `inputRequests`/`requestState`, at-least-one) surfaces `remote_mcp_elicitation`; the host answers with `remote_mcp_elicitation_response` and the pack retries on a FRESH request id with the answers + a byte-exact `requestState` echo, capped as a typed `round_cap` error. - Retry: deadline/network/5xx re-requests the op with the attempt advanced, bounded; `retry-after` rides as data. - Trust boundary: AJV validates ONLY the four trusted response shapes (exported as `REMOTE_MCP_*_SCHEMA`); a failing shape silently no-matches the acting transform — fail-closed, visible as an unmatched event. - The join lane generalizes: `shell_request` gains optional `detail.ctx`, echoed verbatim on the result (both branches) — the you.com MCP `_meta` pattern. All pack joins (source call, attempt, MRTR round) ride the echo, stateless per transform. The security faculty echoes ctx on failed vends too, so absent credentials surface as typed errors instead of pending callers. - Deprecation: `src/faculties/mcp/` deleted; the mcp_* wire kinds removed from the registry; bProgram's default faculties are shell/store/security; the Faculty union drops 'mcp'; `@modelcontextprotocol/client` + `@modelcontextprotocol/server` removed from package.json; the skill reference moves to `references/remote-mcp.md`; AGENTS.md and plan.md updated. - Specs: the pack against the real engine (stamping, discovery chain, execution, MRTR round-trip + cap, bounded retry, failed-vend surfacing), wire-kind parity in the registry spec, and the composition end-to-end — discovery registering tools through real routing against a loopback JSON-RPC server. tsc clean; full suite 671 green.
The id axis deserves a CSPRNG id: CodeQL flags Math.random in ueid as
high severity at the engine's instanceId mint. Swap the trace-identity
mints — behavioral() and the frontier faculty's replay/explore input
defaults — to `bp_${randomUUIDv7()}`: UUID v7 is CSPRNG-backed and
monotonic (sortable), which suits the audit/trace-ordering axis.
ueid itself is untouched and keeps its other call sites — it remains
the pure correlation-id helper (protocol message ids, not security);
only the identity axis moves to the Bun built-in.
Spec: session-id.spec.ts adds a shape-only assertion that two minted
instanceIds differ and both carry the bp_ prefix.
attachTui learned the instance id from the first received trace, so on
a fresh idle instance the "attached to running instance <id>" notice
lagged indefinitely. The host now says hello first.
- socket-host: on every new client connection the host sends one
connection-scoped `hello` notification carrying the engine identity
(`{ method: 'hello', params: { instanceId, sessionId } }` — the
carrier's method/params envelope is the existing outbound contract;
type/detail map onto it) before any trace traffic. Not an engine
event: nothing enters the engine, nothing triggers a super-step.
`HelloDetailSchema` (AJV) is the one schema home at the boundary —
the host validates before it sends (fail closed) and attach clients
validate on receipt.
- bProgram exposes the engine identity (`identity`) and HostRuntime
carries it. behavioral()'s frozen return gains `instanceId` — the
per-process id the engine self-mints and no caller holds; session
ids stay accept-only per the Surface/B decision (never minted,
never returned).
- attach: onAttach resolves from the hello, the single home for the id
handshake; the trace-derived fallback is gone.
Specs: socket-host.spec proves the hello is per-connection, precedes
trace traffic, enters nothing into the engine, and fails closed on a
malformed identity; the two-process lifecycle spec proves an idle
instance's notice is immediate with the correct id and that a
subsequent attach prints exactly once.
The skill was pure delegation after the ICL conversion: its SKILL.md pointed at one reference and handed every domain elsewhere (skills/plugins to skill-conventions, git/shell to the shell faculty, HTML to the controller floors, TS LSP to a future faculty). That reference, remote-mcp.md, is fully duplicated by remote-mcp.threads.ts's self-describing surface — the @packageDocumentation header plus the vocabulary exported as data (REMOTE_MCP_EVENT_TYPES, the four REMOTE_MCP_*_SCHEMA gates, REMOTE_MCP_MAX_ROUNDS). The code is the authority and equally agent-readable; the skill added prose polish only. Also drops the three fleet-era dead links in skills/behavioral that targeted files already absent from behavioral-tools/references/ (mcp-client.md, html.md, frontier.md): SKILL.md and references/controller.md, references/frontier-analysis.md. Verified: nothing in code, config, or the docs references the skill; skill-conventions checked against the scan code and stands accurate.
Dynamic thread addition, structural layer. The op joins FrontierOp (replay/explore/verify) as an analysis operation: a proposed thread tuple is schema-checked against the engine's ThreadSchema home (exported ThreadSchema — derived, not mirrored), then verifyFrontiersRaw analyzes the proposal joined to the current thread set over a selection-trace prefix (deadlock + progress-spec livelock). The verdict returns as data — ok + echoed thread + status/findings/livelocks/report — the frontier stays analysis-shaped and never writes to the engine. The composition admission rides the existing route: an add_thread request routed through the frontier lane registers its id against the validated proposal (the id map is the authorization); the correlated frontier_request_result carries the verdict, and the pump admits under the re-entry law (addThread + step) iff both verdict legs are ok. Rejections are data — the requester reads the why from the verdict. MINIMAL: candidates admit immediately after validation (structural only); the systemOne blocking judge is the next slice.
The e2e proof of the candidate→live transition: an add_thread verdict that verifies admits under the re-entry law (addThread + step), so the admitted thread participates in the next super-step — its requests are candidates in the frontier and select like any other event, looping for a non-once thread. No new wiring: slice 1's admission write was already the re-entry law, so the transition was inherent; this test pins the observable behavior (the full path trigger → frontier_request → verdict → thread_added → live selections). Docs: plan.md Current State + Decision Log record the landed refinements — the verdict result IS the candidate record (no new engine trace kind; the composition cannot emit engine traces), the id map is the authorization against forged results, and the op input shape (thread strict via ThreadSchema, analysis context permissive).
…ming The shipped design default now lives at skills/behavioral/assets/DESIGN.md (the Agent Skills bundled-files location; it seeds <home>/DESIGN.md via the init story per the plan's Home default/G ruling). Restructured to the Google design.md format adopted this session: flat spec-named token groups (colors / typography / rounded / spacing — no geometry wrapper, no nested dark/light objects), every dual-mode color as a single light-dark() value, derived values (the notebook dot tint) via color-mix() over the primary token instead of hardcoded rgba, the iconography line removed, and components declared omitted via the spec-native omitted key. Usage annotations stay — guidance, not definitions. Rulings Default/J + Color conventions/K in plan.md; the maintenance contract lives in the skills-docs prompt (slice 5).
…g judge
The system-one faculty's thread pack (threads.ts): when systemOne is
wired, a validated add_thread candidate does not admit directly — its
admission is blocked while a system-one Decision judges the proposed
thread, and the Decision determines whether the block lifts.
Three threads. admission-issue: a thread_candidate event (the
composition's emission of a validated candidate) issues the correlated
system_one_request (<candidate>-judge) with the thread as the Decision
input (state: the thread; question: an admit/reject choice).
admission-gate: the blocking judge — waits for the candidate, blocks
every thread_admission while the Decision is in flight, lifts on the
approve-shaped result or on the rejection event (a rejection holds the
line through its own processing without poisoning later candidates),
then wraps to the next candidate. admission-verdict: the judge-
correlated result maps to thread_admission { id, admit: true } on an
explicit approve, thread_admission_rejected { id, admit: false } on
anything else — not-ok, malformed, non-admit: fail-closed, error data,
never a throw.
The judge answer is normalized in jq (every non-object on the answer
path coalesces to {}), so a hostile payload falls through to the reject
listener instead of crashing the transform. Concurrency: candidates
judge independently (id-correlated); the gate's block is the type-scoped
judging window, advisory when judgments overlap.
Specs run the pack against the real engine: the Decision request
carries the thread; the probe admission stays blocked while judging and
selects only after the lift; the reject holds the line — no admission
for the candidate, the rejection visible, the gate released after.
The Decision input and the judged outcome get their AJV schema homes in
the pack (JSONSchemaType, the shared ajv instance), derived from the
existing homes rather than mirrored:
- ADMISSION_INPUT_SCHEMA — the issued system_one_request input: state
{ lane, thread } with the thread validating against the engine's
ThreadSchema, questions.admission against the question union's
choiceQuestionSchema (now exported from system-one/schemas.ts — the
one question home). validateAdmissionInput keeps the issue thread's
jq honest: what it emits must be exactly this shape.
- ADMISSION_VERDICT_SCHEMA — the judged outcome { id, admit, reason? }.
validateAdmissionVerdict is the composition's admission-gate boundary
(slice 3 consumes only conforming verdicts). reason stays optional
and absent today: the choice answer carries no prose, so the mapping
emits none — a reason rides a future answer type, never a guess.
New specs: the issued input and both outcomes validate against their
homes; a hostile Decision answer (admission as a bare string) and a
faculty error result are error data — fail-closed rejection, the
candidate never admits, the program keeps judging.
… Decision Wiring slice: with systemOne wired, the composition mounts the admission judgment pack alongside the faculty guard and routes a both-legs-ok add_thread verdict to a thread_candidate event instead of admitting — the judged path. The gate's block holds while the Decision runs; the pump's write legs are the judged outcome events: thread_admission (a conforming ADMISSION_VERDICT with admit === true) admits the pending thread under the re-entry law; thread_admission_rejected (or anything malformed — validateAdmissionVerdict is the boundary) drops the id, the rejection visible in the traces. Without systemOne the structural direct admit stands, unchanged (the existing admission-path specs pin it). E2E specs through the composition with the real system-one faculty and the Decisions fixture (now with a pickChoice override so the canned server can answer reject): approve — the Decision saw the proposed thread, the verdict precedes the admission, thread_added fires, the admitted thread goes live; reject — the line holds, no admission, no thread_added, nothing live. Docs: plan.md Current State + a 2026-09-25 Decision Log entry (the block is the judging window, type-scoped — per-candidate correctness is id-correlation; a literal permanent hold would deadlock all future admissions since pure-data threads cannot parameterize blocks; the fail-closed verdict mapping; the known engine frontier — the super-step cascade recurses unboundedly on self-sustaining loops). AGENTS.md's faculty map notes the pack.
The five verified deltas against src/behavioral/behavioral.ts and friends:
- Package exports: `.` (defineConfig via src/main.ts), `./faculties`,
`./controller`, `./utils` — `./tools` is gone, the engine is in none of
them; the source-path import example drops the dead `UseAddThread` type.
- Hook surface: five members `{ addThread, trigger, useTrace, step,
instanceId }` — the curried `useAddThread(space?)` is gone; `space` rides
on the Thread. `step()` documented as the re-entry pump.
- Action channel: the canonical host is the composition
(src/cli/b-program.ts) under the in-process re-entry law — addThread alone
is inert, every re-entry pumps a super-step; the contentless kick and the
dead src/kernel/kernel.ts reference are removed.
- Transform: the engine now applies the contract in-engine (the jq worker +
evaluateTransform bridge); errors-as-data via transform_error; the
remote-mcp thread pack is the production consumer. The stale
"no production host" caveat is gone.
- Trace union table: all 12 TRACE_MESSAGE_KINDS, adding idle, thread_added,
transform_error, step with their payloads from behavioral.types.ts.
Validated with the shell faculty's validate-links recipe (missing: []).
The controller wire vocabulary is uniformly `ui_*` (controller.constants.ts): ui_render/ui_attrs/ui_dispatch_custom_event/ ui_navigate/ui_scale_check in, ui_event/ui_error/ui_form_submit/ ui_success/ui_snapshot/ui_scale_check_result out. Every table, example, and prose mention takes the rename. Restructured around the Controller as the one live surface: the retired SSR html tools and the no-Renderer claim are now an explicit statement, not a pair of live surfaces. Added the schema home the doc omitted — CONTROLLER_DETAIL_SCHEMAS (controller.schemas.ts), imported by the host threads, never the browser bundle — alongside the deterministic floors and the classifier ceiling. Dropped the dead behavioral-tools html.md link and the retired html-scale-check tool name; verified the WebSocket retry codes (1006/1012/1013, max 3) and queue-flush behavior still match controller.utils.ts. Validated with the shell faculty's validate-links recipe (missing: []).
Per the 2026-09-25 design.md ruling: references/design-spec.md is deleted (it self-declared a non-normative wayfinding consensus surface, not a reference) and its SKILL.md mentions go with it — the route-table row, the description's design-system-spec clause, and the "When to use" bullet. SKILL.md takes the current-surface rewrite: the description covers the runtime, faculties wire, controller `ui_*` protocol, frontier analysis, and eval — no SSR html tools (retired), no design-spec. The controller route row takes the ui_* rename. The companion-skills paragraph replaces the mcp faculty + dead behavioral-tools link with the remote-mcp thread pack over the shell faculty's rpc op. frontier-analysis.md light touch: the dead behavioral-tools frontier.md link is dropped; "three ops" becomes the four-op surface (replay, explore, verify, plus the landed add_thread admission op); the schema-home sentence reconciles the two homes — the wire event shape in faculties/faculties.types.ts, the per-op input schemas in faculties/frontier/faculty.ts; fleet-era frontier-verify/explore tool names become op phrasing. Validated: SKILL.md frontmatter parses under the scan's fence rules; validate-links missing: [] on both edited files.
Per the 2026-09-25 ruling: eval.md is now a shape guide for building an
eval harness — prescriptive on the DATA (the trace stream, event shapes,
correlation axes, the redacted/raw split), not on harness code. One pass;
the engine deltas fold into the reshape:
- Capture-side projections table (selection/thread_added/idle/
transform+transform_error/deadlock) — the 12-kind table stays in
behavioral.md; cross-referenced, never re-tabled.
- Identity + correlation: TraceBase { instanceId, sessionId }; correlate by
sessionId (defaults to instanceId), not timestamp.
- Space: the traceSpace derivation (trace-consumer.ts) — top-level, else
selection/interrupt's, else thread_added's, else root; per-space grading
is consumer-side filtering.
- Thread[] free via thread_added; divergence via
frontier_request { op: replay | explore | verify } — op contracts left to
frontier-analysis.md.
- The redacted/raw split: in-process useTrace subscribers are raw (the
canonical eval path, per-consumer catch); every remote carrier (stdio
serve, instance socket) is redacted-once via createTraceConsumer —
declared secrets, sensitive fields, credential shapes; redacted fields
are exactly the secrets.
- Wiring seams as shapes: HostRuntime = { trigger, useTrace, start,
terminate, identity } (serve.ts); engine never awaits listeners; sinks
sync when ordering is the contract; subscribe before start().
- Fleet-era tool names (frontier-explore/verify/replay), the dead
src/kernel/kernel.ts path, the "no root export" claim (the root exports
defineConfig), and the three-hook surface are all replaced with the
current code shapes.
Intake questions and grading-out-of-scope kept as harness scaffolding.
Validated with validate-links (missing: []).
Verification pass against the 2026-09-25 maintenance contract — the frontmatter already conforms (flat colors one-token-per-key, flat per-token typography map, rounded/spacing groups, no geometry wrapper, no nested dark/light objects; light-dark() dual-mode values; color-mix() derived dot tint; `omitted` declaring components; brand/surfaces_texture/ accessibility as extensions outside the spec groups; fence-sliced YAML parses as one object) — so no frontmatter changes. The desync was body-prose: the dark/light surface tables enumerated only 13 of the 29 `colors` tokens, and rounded/spacing never appeared in prose at all. §4.1/§4.2 now list every frontmatter token (roles as guidance, no invented contrast ratios beyond the audited pairs), and a §4.4 records the rounded/spacing scale. Mechanical check: every colors/rounded/spacing/ typography token from the frontmatter now appears in the body — 0 mismatches.
Retire "pack" as a collective noun repo-wide. The formal units stay exactly as they are — threads (the engine's composition unit), faculties (processes), plugins (the distribution unit carrying sh.behavioral/threads/) — and a co-shipped set of threads is now named by its threads: "the remote-mcp threads", "the admission judgment threads", "the ui_* producer threads" (formerly "the interface pack"). Every occurrence swept: AGENTS.md, comment headers/bodies and describe() labels across shell, system-one, and the composition (no behavior changes — comments and docs only), and the behavioral skill docs that had picked the term up from the code. Vocabulary ruled in plan.md (Vocabulary/L); tsc clean.
Pin the super-step cascade's observable behavior before any engine change, per the trampoline Decision Log: - the cascade sequence oracle: a self-sustaining request loop with a deterministic stopper; the exact trace-kind sequence, selection order, and step numbers recorded against the CURRENT recursive engine - the re-entrancy oracles: a nested step() (the pump-shaped addThread+step re-entry) and a nested trigger() from a selection listener mid-cascade — both pin that the nested super-step completes in order, with the trigger's ingress channel intact on the step trace and the candidate - the overflow spec (RED): the same loop driven past the ~8.6k recursion limit — today it fails with RangeError (Maximum call stack size exceeded) straight through step → selectNextEvent → nextStep → step; this is the failure the trampoline slice exists to fix
The 20k-tick overflow spec pinned the trampoline's contract: a self-sustaining request loop completes past the recursion limit. The trampoline direction is superseded by the pilot ruling (2026-09-25 session): the self-sustaining loop is a defect to guard at admission — mandatory livelock detection on the add_thread path — not a shape the engine must complete. The engine reverts to (and stays at) the pure recursive cascade; the characterization specs 1-3 stand unchanged. The retired spec's contract moves to the composition's admission boundary (the review-pack slice).
The pilot ruling: the self-sustaining request loop is the defect to guard, not
a shape the engine must complete — the trampoline direction is superseded and
the engine stays the pure recursive cascade. The guard moves to admission,
where frontier's dormant livelock detection lives.
The admission-review home (src/faculties/frontier/threads.ts):
- the composition enriches every add_thread proposal at the route seam with
ITS policy, never the requester's claim: progress = the derived *_result
kinds ("an external consumer observed a result"), maxDepth default 20k
with a clamped requester override. A self-sustaining no-progress cycle
verdicts failed (livelocks in the result) and never reaches the write.
- the structural admission is BP-native — the review pack (gate + verdict
threads, the judgment pack's mirror over the frontier verdict) maps
verdicts to thread_admission / thread_admission_rejected selections;
mode-exclusive with the system-one judgment pack. Rejection is data.
Specs: the looper never admits (livelock finding in the verdict); a
progress-cycle and external-release loops still admit (the guard is
progress-relative, not loop-hostile); truncated rejects fail-closed; the
default budget and the override both flow. Old looping-thread fixtures
re-pinned to the new contract.
Residual, recorded in plan.md: an admitted progress-cycle thread still
overflows the recursive cascade; the swallow-to-feedback_error slice is the
queued last line of defense.
The failure-path transform listeners (rpc-auth requestor; remote-mcp retry,
call-failure, discover-failure, vend-failure) gated on {id, ok} — every
shell/credential result matched, so their jq select() declined on successes
into empty-output transform_error traces: 8 stray errors on every clean
composition boot, invisible to the selection-level specs.
The gates now express the jq conditions in schema form, so a matched
listener always fires: ok const false + error + the ctx echo lane, with the
surface listeners leg-pinned (call vs discover/tools) and the requestor
pinned to the exact credential_required shape (url present, authToken
null-or-absent — the replay loop bound, now enforced at the gate). The
credential_required jq stays as defense in depth.
Pinned: a success and a direct caller's failed vend are trace-clean at the
thread level; a clean composition boot fires zero transform_errors (was 8 —
proven by stashing the fix). Remaining, by design: one genuine-failure
sibling decline (retry vs its surface — "retryable", remoteCode >= 500, is
not schema-expressible), and the elicitation siblings on genuine
input_required results.
…s divide by schema The remote-mcp residual from the runtime-supervision handoff: on genuine remote failures the retry-vs-surface listeners divided "is it retryable?" in jq (remoteCode >= 500), so one declining sibling emitted a transform_error trace per real failure — stray noise on the audit surface the supervision layer reads. The discriminant is computed once at the op level, where the numeric comparison lives: network failure, timeout, or remoteCode >= 500 → true; 4xx, JSON-RPC error codes, malformed responses, stops, and credential challenges → false. The thread siblings divide on the schema field — `retryable: true` under the attempt cap for the retry listener; non-retryable or exhausted for the surface listeners — and the jq numeric check is deleted. The division is total, so no listener ever declines: zero transform_errors on genuine failures, the retry still retries, the surface still surfaces.
…e BP
Slice 1 of the runtime-supervision layer: block-then-judge at runtime,
the admission pattern rotated. A supervisor thread waits on a watched
event type and counts selections — the counter is the thread's rule
position, so the breaker is pure data: `threshold` waitFor steps, a trip
rule that blocks the type and surfaces `supervision_tripped
{ type, count, threshold }`, and a hold rule that parks the block until
a `supervision_release` for that type (the wrap-back is the reset).
The block takes effect at the NEXT super-step, mid-cascade: the
cascade's own advance passes through the supervisor each selection, so
the breaker fires at the default threshold 4096 — under the ~8.6k
recursive-cascade overflow — before the stack dies. Type-scoped by
design (stops the event KIND, not one thread); the watch list is
composition config, no auto-discovery; systemOne adds no threads
itself, so there is no proposer/judge conflict.
Specs pin: a self-sustaining loop trips at the threshold, the cascade
stops, the trip surfaces conforming to its AJV home, and the rest of
the program keeps running (a fresh non-watched event selects while the
watched type stays blocked); a 20-iteration legitimate loop under the
default threshold runs clean.
…d, the verdict owns the block
Slice 2 of the runtime-supervision layer: on trip, the supervision
judgment asks the systemOne judge whether the blocked loop is
legitimate. `supervision-issue` transforms the surfaced trip into the
correlated `system_one_request` (`<type>-supervision`) carrying the
loop's identity as Decision input (state lane `supervision`: type,
count, threshold — one AJV home, SUPERVISION_INPUT_SCHEMA).
`supervision-verdict` maps the judge-correlated result back:
- lift (an explicit choice) → `supervision_release { type }` — the
supervisor's hold rule matches, the block lifts, and the wrap back to
the counter IS the reset (the program continues);
- everything else — a halt choice, a malformed answer (the not-const
oneOf falls through junk shapes) → `supervision_halted { type }`;
- judge unavailable (429-exhausted, timeout, crash — ok:false) →
`supervision_halted { type, reason }` — FAIL-VISIBLE, locked: the
block holds AND the unjudged halt surfaces with the judge-failure
reason. Never silent continuation, never an invisible halt.
The three listeners' gates are mutually exclusive (ok false / ok true
with choice `lift` / ok true with anything else), so exactly one acts
per judge result — zero transform_errors on any branch, the audit
surface stays clean (the slice-0 lesson carried into the new pack).
The composition takes a `supervision: { watch, threshold }` option and
mounts the breaker + judgment with systemOne only when the host names
watched types. Specs: all branches engine-level RED-first, the lift
and fail-visible branches proven through the real composition against
the real faculty process, and an opt-in live TypeSafe integration
(TYPESAFE_API_KEY) that runs the whole judgment lane against
api.typesafe.ai — the threads' own issued request, the real answer
mapped through the verdict. init.spec now restores the env vars it
borrows instead of deleting them (the leaked deletion was poisoning
the live key for later specs in the same process).
…try, the override ingress
Slice 3 of the runtime-supervision layer: recovery for a standing halt,
thread-orchestrated.
- `supervision-judge-retry` — an UNJUDGED halt (the reason's presence
marks the judge-unavailable branch; a judged halt carries no prose and
never re-issues — the judge spoke) re-issues the same Decision. The
attempt count rides the thread's rule position, the generator state —
the same idiom as the supervisor's counter: SUPERVISION_MAX_REISSUES
re-issue rules (default 2), then the thread parks on the next release
and the halt stands. A release — a later lift or an override — re-arms
the budget. No timer exists (the count-not-rate lock), so the
provider's own 429/529 transport retry with retry-after backoff is
the backoff; the thread bounds the re-asks.
- `supervision-override` — the host/TUI ingress
(`supervision_override { type }`, one AJV home) lifts the block for
the named type: the human decision path. The listener's detailSchema
is the trust boundary — a malformed override never matches, the
block holds.
The issue and the re-issue share one request-body jq home
(supervisionRequestJq — the question text never forks). The composition
mounts the recovery threads with the pack. Specs: engine-level — the
re-issue and later lift recovers end-to-end, the bound holds (initial
issue + exactly MAX re-issues, then the standing halt), a judged halt
never re-asks, the override lifts immediately, a malformed override
never matches; composition-level — a fixture that 429s exactly the first
judgment's transport attempts proves the retry re-asks and the second
judgment lifts the block, and the override lifts a permanently-halted
block through the real ingress.
The Open Questions entry resolves to LANDED (all four slices, per .prompts/runtime-supervision.md); the Decision Log gains the dated entry (the counting breaker, the schema-total verdict division, the composition option, the live TypeSafe integration, the MINIMAL notes); Current State records the landing. AGENTS.md's system-one boundary and the faculty-threads listing now name the supervision threads.
…ateSSR is retired
behavioral.types.ts:172's `@see {@link UseAddThread}` names a type that no
longer exists; the runtime member is `addThread` and its function type is
`AddThread` (behavioral.types.ts:663). controller.ts:10's module JSDoc
advertised a `createSSR` Rendering export that is not exported anywhere —
the SSR tool fleet is retired, so the line is removed. Comment-only, zero
behavior change; verified by `rg createSSR src/` (one hit, the stale doc
line itself) and tsc.
….2 namespace Per agent-plugins.org §8.2, client-owned files sit under the reverse-domain namespace directory: behavioral.sh → sh.behavioral. The plugin scan recipe's names-only discoverThreads now scans sh.behavioral/threads/ instead of the pre-1.0 top-level threads/, which breaks outright (zero shipped consumers — no warn-and-continue). The manifest shape is unchanged: threads stays an array of filenames, now sourced from the namespace dir; the spec pins both sides (a file under the namespace dir is discovered, the same file at top-level threads/ is not).
…is root-only isListeningFor's space rule was one-sided: an unstamped listener matched events in ANY space (omni), while a stamped listener matched only its own. Root/D's ruling needs the inverse — an admission without a space stamp targets the root space only, never implicitly all-spaces — so the rule is now symmetric: a listener matches an event iff both are unstamped (root), or both carry the same stamp. Omni becomes structurally inexpressible; a thread governing several spaces is admitted (or wired) per space explicitly, each mount stamped. Root is absence everywhere — the 'root' literal stays a store/trace display label (n_SPACE), never an engine stamp. Blast radius verified: no spec pinned the omni behavior (all space tests are stamped-both-sides), all current composition traffic is root, and the per-space loops are self-consistent (a thread's request bids its own space; useFaculty re-enters results as space-stamped once-threads; the frontier analysis shares this matcher). The root guard and supervision threads become root-only with everything else unstamped — a named-space composition wires its own per-space faculty set, which useFaculty already supports. 609 specs green.
…te, propose
The proposal threads (src/faculties/shell/plugin-threads.threads.ts),
mounted by the composition when the shell faculty is on. A
plugin_threads_proposal (host ingress — the explicit proposal act) issues
the bun-direct import script through the run op; the script's top-level
await import executes the plugin file's top level IN THE WORKER
subprocess — the only code-execution moment, once per content version —
and validates EVERY export against the engine ThreadSchema imported from
the engine schema home (ajv + ThreadSchema from the same module the
composition trusts, never hand-mirrored). The file content hash (sha256)
rides the result — the admission registry's re-arm key (slice 3).
The ctx.echo join (the credential-seam / remote-mcp pattern) maps the
correlated shell_request_result to plugin_threads_imported or the typed
plugin_threads_failed (shell-level and script-level failures both —
errors as data, never a crash). The imported batch peels one
plugin_threads_candidate per validated thread (the carry recursion:
pure-data threads cannot loop, so the queue rides the events), and each
candidate dispatches one frontier_request { op: add_thread } keyed by
its candidate id — the landed admission path (livelock guard, verdict,
the pending-id write) carries each candidate live. The proposal's target
space governs the mount (Root/D): a named space stamps the proposed
thread, a root target strips any author stamp — root-only, never omni,
on the now-symmetric space matching. The plugin-threads label stamps
the proposal-lane ops (trace annotation, no routing weight).
Specs: thread-level (dispatcher, join, carry, dispatch, failure shapes),
the script itself run for real (validated exports + skip warnings +
hash; typed import/read errors), and the composition vertical through
the real shell faculty worker — one add_thread per valid export, the
invalid export skipped with a warning, the candidate admitted live.
…isions, snapshot boots The admission registry under <home> (src/cli/plugin-thread-registry.ts) — host-local, the config.ts/traces pattern, never the space-scoped store — keyed (plugin, file, content hash, space). Admitted entries carry the validated thread SNAPSHOT (threads are pure data), rejected entries carry the reason. The whole file validates against AJV on read — the thread leg is the engine ThreadSchema home — and a malformed registry fails fast with its path (the load-config pattern), never silently ignored. Writes are synchronous by design (the trace-log sink's rule: the outcome legs fire-and-forget, an async writer could drop the last admission on exit). The composition wires it: boot mounts the admitted snapshots through the deferred thread lane (an unchanged admission never re-imports the plugin file — the snapshot is authoritative, surviving post-admission file mutations); the pump joins each plugin_threads_candidate's registry key to its add_thread id; a decided key never re-adjudicates — the route drops the request and surfaces plugin_threads_skipped (status + reason), never a silent re-judge. The outcome legs own the durable write: a structural verdict failure (livelock, invalid) records rejected + reason at the result leg (rejection is data — the pending id drops there); the thread_admission outcome records the admitted snapshot — same write in both modes, the judged admission (systemOne) rides the same legs. Hash keying re-arms: a plugin update is a new key, a candidate again — new thread code is never silently admitted. Root and a named space hold independent entries (Root/D); the registry mount is independent of the supervision option. Specs: the registry module (key independence, round-trip, fail-fast on malformed/invalid, boundary-checked writes) and the composition vertical — admit then fresh-boot mounts the OLD snapshot with no import; reject stays out visibly, a re-proposal of the same content surfaces the skip and never reaches the frontier analysis; a changed hash re-runs the import and proposes again (both hashes hold independent decisions). Full suite: 719 specs green.
All four slices are on the branch (2e477ac…b53bcef1): the stale JSDoc references, the sh.behavioral/threads namespace scan (§8.2), the plugin-thread proposal path (worker import, engine-ThreadSchema validation, one add_thread candidate per validated export, the proposal's space stamp governing the mount), and the <home> admission registry (snapshot boots, durable admitted/rejected keyed (plugin, file, hash, space), decided keys never re-adjudicate) — riding the pilot's in-session Root/D mechanics ruling that made engine space matching symmetric (unstamped listeners are root-only; omni is structurally inexpressible). The landed prompt is deleted from disk (session docs are untracked). AGENTS.md: the retired-fleet bullet and the faculty-threads listing name the proposal threads (shell/plugin-threads.threads.ts); the src/cli boundary names the admission registry (plugin-thread-registry.ts) and bProgram's snapshot boot mount. Docs-only, zero behavior.
…pace Direction/R (2026-09-25): root is root in the traditional architectural sense — visibility flows UP only. An unstamped (root) listener now matches candidates in every space across all four idioms (waitFor, block, interrupt, transform), while a space-stamped listener stays confined to its own space — the symmetric ruling's stamped half stands; its "unstamped is root-only" clause is superseded (ff0f145). The matcher is the ruled one line: listener.space === undefined ? true : space === listener.space. One edit beyond the prompt's one-line scope, required by the Decision Log's ruling that "the transform target's space stamp follows the source event": the re-entry stamp is now listener.space ?? selectedEvent.space. The prompt's "existing carry-through" note misread the old code, which stamped the target with the declaring thread's space. Identical for stamped listeners and root events; only a root transformer observing a space event changes — its target now re-enters in that space. Root requests still bid only in root (emissions are not observations) — pinned via the selection-order pin. space-matching.spec.ts rewritten to pin the new rule: omni waitFor/block/interrupt/transform, stamped confinement, the root-request pin, and the transform source-space stamp. Full suite: 758 pass, 0 fail. tsc clean.
…tcher pins The blast-radius proof for the root-sees-all matcher flip (0171243): no production changes — three composition/consumer specs pin the intended flipped consumers, plus the admission/livelock suites re-passed green (root threads' reachability spans spaces; no finding regresses). - The root guard (b-program): a named-space malformed ui_render is blocked — the root guard's unstamped block matches every space, closing the named-space validation gap with zero new wiring. - The supervision breaker: a space-stamped loop trips the root-mounted breaker and the block is GLOBAL — the same type in root is blocked too (accepted v1 bluntness; a space-stamped supervisor set confines — expressible, not built), while the rest of the program keeps running. - The plugin dispatcher: a space-stamped proposal event reaches the root dispatcher's unstamped waitFor (the join), and the proposal's declared space flows through to the add_thread target stamp. Full suite: 761 pass, 0 fail. tsc clean.
…on boundary - skills/behavioral/references/behavioral.md: a new "Space matching" section (root authority — visibility flows UP only; the unstamped listener sees every space across all four idioms; stamped listeners never escape their space; root requests bid only in root), the addThread prose points at it, and the transform section's target stamp now reads "the source event's space" (a stamped contract's equals the event's). Links verified — the validate-links recipe named by the slice prompt does not exist in this tree (skills/ holds only behavioral/), so the relative links were checked directly. - AGENTS.md: the system-one supervision boundary gains the global-trip clause — a root-mounted supervisor's block is global; a space-stamped supervisor set confines, expressible but not built. plan.md untouched per the slice rules — findings for the navigator are reported out-of-band.
…IGN.md scan
Slice 1 of the ui-threads handoff (.prompts/ui-threads.md): the boot scan
recipe through the shell faculty's run op reads the USER'S <home>/DESIGN.md
(the no-lock contract — the shipped asset is an init-copied seed, never read
at runtime), validates leniently per the design.md consumer table (unknown
groups/sections ride verbatim, spec-named groups validate by shape with a
drop-to-warning, duplicate section heading rejects the file with the
rejection riding the warnings), and lands the store tenant design/context
{ tokens, sections, warnings } — warnings-as-data, the catalog posture.
A missing DESIGN.md is not an error: no tenant, no warning-spam.
The thread set lives at src/cli/ui-threads.ts (composition territory —
Home/O), mounted by bProgram when shell + store + systemTwo are on; absent
systemTwo there is no generation lane and the threads don't mount (the
remote-mcp precedent). The skill's controller.md reference gains the
producer-threads section with the design tenant contract in the same commit.
Specs: engine-level thread contracts, the real-run recipe against temp
homes (user tokens vs shipped seed, the duplicate rejection, the missing
file), and the composition mount through real faculty processes — the temp
home rides the faculty env overrides because Bun.spawn children see STARTUP
env only (the carried TUI finding).
…-then-stamp Slice 2 of the ui-threads handoff: the preflight thread (Structural IA/E — fixed mechanism, thread-authored policy) in the admission-gate shape over the DOM fact. A render trigger (the b-trigger convention: a ui_event whose inner BPEvent has type render) derives its ui_scale_check request; the generate request — the thread-owned generation event, off the ui_* egress fan-out — is BLOCKED until the correlated ui_scale_check_result re-enters (joined by the echoed id: the controller wire carries no ctx), and the result is stamped INTO the generation request's ctx (block + transform in one sync point): the effective scale and target ride host-supplied ctx, never model-facing. Without a browser the hold stands — visible in the frontier as the parked preflight with its generate block (the deadlocked-ish hold, correct for the first pass). A foreign-echo result joins nothing and the hold survives it. The v1 ceilings are MINIMAL-noted in the module header: the triggering event's detail does not survive the scale-check round trip (the controller result carries no source reference), and concurrent triggers during an in-flight preflight drop (the thread's rule position is the window). Specs: engine-level (the request, the hold's frontier visibility, the stamp, the foreign echo) plus the end-to-end drive through the real serve dispatcher — ui_event ingress, the ui_scale_check egress notification, and the result re-entering the same seam.
…mTwo to ui_render Slice 3 of the ui-threads handoff. The ctx binding lane extends to the two faculties the pipeline joins through (the landed shell pattern, verbatim): store_request and system_two_request details carry an optional ctx, both workers echo it verbatim on results (ok and error branches) — the join lane the generation state (scale, target) round-trips through. The generation threads: the scale-stamped generate request fetches the design tenant (the store get carries the generate ctx as its ctx.echo); the tenant-bearing (or null-tenant) store result composes the systemTwo request — with a tenant the flattened token vocabulary (--design-* custom property names, never literal values) rides model-facing and the prose sections ride as system context; with NO tenant the request composes plain (the design lane is an optional input, never a gate). The model composes only the html fragment — id/target/swap are host-stamped; the draft must validate against CONTROLLER_DETAIL_SCHEMAS's ui_render schema before the thread requests it (validate-before-request, the catalog pattern — a non-conforming reply is held as data, visible in traces, never emitted). A tenant-bearing scan also compiles the custom-properties artifact (design/artifact, --design-<path>: <value>; verbatim values, from the TENANT only). Specs: the store + systemTwo ctx echoes through the real faculty processes; the engine-level lane contracts (the fetch, the two compositions, the conforming + held-as-data replies, the artifact); and the no-lock end-to-end through the real composition, the serve dispatcher, and the fixture Open Responses endpoint — the USER's two-token vocabulary rides the recorded model call and the reply renders as an egressed ui_render; with no DESIGN.md the pipeline still renders plain. MINIMAL notes carry the v1 ceilings (provider/modelId constants pending the loop's config seam; the artifact's store home pending the serving seam).
…rontier replay
Slice 4 of the ui-threads handoff (its prompt retired with this landing).
The iterate mechanism the initial thread set is refined by: an in-process
RAW capture consumer (src/cli/ui-capture.ts — the eval ruling's canonical
path: in-process = raw, a second useTrace subscriber coexisting with the
redacted lane untouched) writing ui-pipeline runs (a render ingress through
its ui_render) to a capture sink — the socket host wires the durable file
sink under <home>/captures (the TUI deliverable path; serve gains it on a
named need).
The Thread set rides thread_added in two lanes: the standing policy
threads, and the run's once-thread RE-ENTRIES position-tagged by message
count — the engine adds a transform's once-thread BEFORE tracing its
source's selection, so the capture buffers closed-world re-entries (cleared
at each closed-world selection, backfilled at run open; the space:
undefined key the replay input's strict thread shape rejects is
normalized — absent means root). The position tag is what makes prefix
replay faithful: uiReplayRequest(run, upTo) builds the frontier_request
{ op: replay } over the standing set plus exactly the re-entries that
existed at that point — replaying the full run re-derives the end state;
replaying the prefix up to the browser's scale reply re-derives the hold
(frontier idle, threads pending).
Specs: the scripted run through the real composition (the Thread set
round-trips, the messages span ingress → preflight → generation → render,
a superseded hold run is captured incomplete), the replay through the
real frontier lane, and the socket-host capture wiring. controller.md
gains the autoresearch section; eval.md's Thread[] section points at the
loop as its first concrete consumer; the full suite is green (796).
Iteration 1 of the autoresearch loop, driven by the capture evidence (all
four v1 ceilings confirmed in the data):
- the standing pipeline (one const scale-check id, one const generation
lane) DROPPED concurrent triggers and lost the trigger detail at the
scale-check round trip — the recorded model call carried the fixed
"Compose the HTML fragment..." string with no trace of the user's view
request;
- the capture attributed runs by time window, not lineage — interleaved
triggers mis-attributed (one run carried the other's completion tail)
and boot faculty traffic contaminated run windows.
The repair, branch (a) — the host leg:
- `uiPipelineThreads({ id, detail })` mints FIVE per-trigger once-threads
(scale-issue, scale-join, context-issue, generation-compose,
render-compose), labels `ui/pipeline:<id>/<leg>`, every correlation id
per-trigger (`<id>-scale`/`-tenant`/`-gen`/`-render`). The trigger's own
detail rides the generate request (`request`) into the systemTwo user
message (`View request: ...`) — the user's content, model-facing by
right. The joins stay pure data: the echoed id (controller wire) and
ctx.echo (store/systemTwo wires).
- b-program's pump leg: on a `render` INGRESS selection (uiMounted —
shell + store + systemTwo), addThreads a fresh `ui-<ueid>` set — the
admission-path precedent, synchronous mint, ThreadSchema backstop.
N triggers → N pipelines; the no-browser hold is per-trigger (the
scale-join once-thread parks on its transform listener).
- the capture is reworked to LINEAGE keying: runs keyed by the minted
pipeline id (parsed from mint labels, transform/faculty re-entry labels,
correlation ids, ctx lineage), many runs open at once, the pump's
subscriber-order warp (mint traces arrive before the ingress trace)
handled by lazy binding with ingress insertion at message 0; a run
closes only at its `ui_render` terminus. Unrelated faculty traffic no
longer lands in runs; the incomplete-run flush stays the parked
quiescence need (MINIMAL-noted).
- the standing set keeps the boot design scan, the tenant/artifact
compile, and the render gate (shape-only, draft-agnostic).
MINIMAL ceilings kept: the user message renders the trigger detail as
compact JSON (a structured content contract rides the loop's data); the
provider/modelId stay the fixed conventions (the config seam is the next
slice).
Validation: tsc clean; full suite 801 tests / 0 fail (30 in the ui-threads
spec, 4 in the capture spec — mint isolation, interleaved e2e, trigger
detail in the recorded fixture call, lineage-attributed captures, replay
full + prefix).
The loop's second repair: the generation endpoint left the thread-set
constants (`default`/`gpt-5.1`) — the recorded call pinned the model no
matter what the composition intended.
- `uiPipelineThreads` gains optional `provider`/`modelId` (defaults = the
conventions, back-compat: absent = today's behavior);
- `bProgram` gains `ui?: { provider?, modelId? }`, threaded through the
pump's mint into every per-trigger pipeline; a named provider must exist
in the systemTwo endpoint map (`useSystemTwo({ endpoints: { … } })`);
- the constants' MINIMAL notes retire (the seam landed).
Validation: tsc clean; the cli suites green (32 ui-threads spec tests —
named values reach the composed request at engine level and the recorded
model call through the real composition; the defaults pin via the existing
e2e, still 'gpt-5.1' when unset).
The loop's third repair (the pilot-approved branch (iii)): the compiled
--design-* artifact landed in the store but no page consumed it — zero
store reads of design/artifact across a whole captured session.
The seam is a new controller wire kind, not a host HTTP surface and not
a mutation of the model's fragment:
- `ui_style` joins CONTROLLER_INCOMING_MESSAGE_TYPES with its detail
schema in CONTROLLER_DETAIL_SCHEMAS (StyleMessage — id, target, css;
derived, never hand-mirrored).
- The pipeline's style-issue leg (the sixth minted once-thread) composes
the scoped style from the TENANT's tokens, jq-deterministic: the
`--design-*` declarations wrapped in an `@scope` block rooted on the
render target's b-target selector — `@scope ([b-target=...]) {
:scope { ... } }` — so the custom properties live on the target and
its subtree inherits them without leaking to the page (Baseline 2026:
Chrome/Edge 118+, Firefox 146+, Safari 26.4; older engines drop the
block silently — plain degradation, same as no tenant).
- The browser applies the css VERBATIM into one data-b-style-keyed
style element per target (idempotent replace, never stacks); the
controller stays a dumb applier and composes nothing.
- Emitted BEFORE the ui_render when a token-bearing tenant exists;
plain (no tenant / no tokens) emits nothing. No standing gate: the
css has no model in the loop (the render gate exists for MODEL
output); the leg's listener schema is the input gate.
- The capture's lineage alternations gain the -style suffix, so the
egress rides its run by pipeline id.
MINIMAL kept: the `=` selector match only (match variants ride a named
need); the store artifact remains the durable record for other
consumers.
Validation: tsc clean; full suite 808/0 — the real-Chrome controller
test proves the @scope application end-to-end (subtree resolves the
custom property, body outside the scope resolves nothing, one
idempotent style element), the composition e2e proves ui_style
egresses before ui_render with a tenant and never plain, the capture
proves the lineage routing.
…cold-spawn races The first CI run of the stack (PR #348) failed three tests; the diagnosis is reproduced and probe-instrumented in a Linux x64 Bun 1.4.2 container: - the progress-looper fixture was a LOOPING thread requesting a bare store_request_result — a progress event per the admission spec's *_result derivation. Its own request is the candidate that selects, re-arming the thread inside ONE cascade: an admitted livelock the engine's recursive super-step converts to a stack overflow. Where the overflow lands (absorbed by the trace-listener catch vs escaping the pump) decides pass/crash per platform — CI's deeper stacks crash, macOS's absorb. The fixture is now once — same verdict path (the request still selects progress inside its cycle), no runtime spin. - shell_cancel asserted seen === 1 after a fixed 100ms sleep — a cold runner's worker spawn misses the window. Poll to the server's arrival instead (bounded, fail-fast on a genuine no-show). - the two registry tests exceed bun's 5s default on slow runners (5.6s on a 4x-slow container; green on CI with margin) — explicit 20s timeouts for the process-heavy multi-boot choreography (waitForTraces' own 8s deadline still fails fast on a real break). Verified: the two spec files green locally and in the Linux x64 container; tsc and biome clean. The open branches (the engine yield valve, the unguarded default-faculty result kinds) are logged in the session plan for the pilot — deliberately not landed here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
.prompts/*handoffs (TDD, one commit per slice). plan.md (gitignored by design) is the navigator's decision log for the design chain behind these commits.Summary
TraceBasegainssessionIdbesideinstanceId(accept-only, host-supplied, defaults to the instanceId;behavioral()return unchanged) — the dormant audit axis<home>/instance.pid) + unix-socket attach lane (<home>/instance.sock) speaking the existing JSON-RPC vocabulary through the same dispatcher asserve(one protocol, one dispatcher, multiple carriers); barebehavioralis attach-or-start; the minimal TUI (three primitives,tui_*guard entries fromTUI_DETAIL_SCHEMAS); static GUI serving on the same listener +--dev(start-time only);bundleControllerpromoted tosrc/controller/rpcop; the security faculty (credential vending, issuer-bound OAuth overBun.secrets) with the credential seam's vend-and-replay threads; remote-mcp as threads over the rpc op (themcpfaculty is retired)add_threadop (structural verdict as data) + the end-to-end admission path (candidates go live under the re-entry law) + the systemOne admission judgment (the BP-native blocking judge — a candidate's admission waits on its Decision)bProgram({ supervision: { watch, threshold } })sh.behavioral/threads/namespace scan, the proposal path (worker import → validation → oneadd_threadper export), and the<home>admission registry (hash-keyed, durable, snapshot boots)<home>/DESIGN.md, the shipped asset is an init seed only), the scale preflight (block-then-stamp), the generation lane (systemTwo →ui_render, the custom-properties artifact), per-trigger minted pipelines, the generation config seam (bProgram({ ui: { provider, modelId } })), the scopedui_styleegress (an@scopeblock rooted on the render target — Baseline 2026, silent drop = plain degradation), and the loop's raw capture + frontier replayChanged Files
src/behavioral/— the sessionId wire, root-authority space matching, the cascade characterization specs, schemas + suitessrc/faculties/— frontier (add_thread op, sessionId on synthetic traces, livelock-at-admission), shell (rpc op, credential seam, remote-mcp threads, plugin threads), security (the credential faculty), system-one (the admission judgment + supervision threads), shared (faculties.threads.tsguard derivation,use-faculty.ts, instance-lock)src/cli/— tui, socket-host, attach, attach-or-start, serve (dispatcher extraction), b-program (the composition: supervision + registry + ui mounts), plugin-thread-registry, ui-threads, ui-capture, load-config, trace-consumer, cli + suitessrc/controller/— bundle-controller (promoted), theui_stylekind +StyleDetailSchema,#styleapplier, controller + schema suites (real-browser WebView specs)bin/behavioral.ts— the default command registrationskills/behavioral/— references (behavioral.md, controller.md, eval.md), the shipped DESIGN.md asset.github/,scripts/— CI hygieneKnown Failures / Drift
once), a cold-spawn race inshell_cancel(now polls), and the two registry tests' slow-runner timeouts (now 20s explicit)6e906970amendment), the unguarded default-faculty result kinds (mounting guards changes admission-replay verdicts), the style-target CSS-injection flag (pattern-constrain or escape)terminal: falsedrops buffered lines betweenquestion()calls (explicit queue);Bun.file().exists()returns false for socket files (socket checks usenode:fs existsSync)sessionIdis a dormant axis: landed and defaulted but currently degenerate (always equal to instanceId) — no consumer supplies a real one yetReview Notes / Residual Risks
bun --bun tsc --noEmitclean, biome clean; the last CI run of the branch is green@scope-basedui_stylerequires Baseline 2026 engines (Chrome/Edge 118+, Firefox 146+, Safari 26.4) — older engines drop the block silently (the plain-degradation lane), never break--dev) is Bun-WIP upstream; degrade path is full page reloads, and the flag is opt-in start-time onlygit log main..HEADis the slice ledger; every slice has its spec, and the docs landed in the same commits