Skip to content

The working-session stack: host topology, faculties consolidation, thread admission + supervision, plugin threads, root authority, the ui_* producer threads + autoresearch loop - #348

Merged
EdwardIrby merged 55 commits into
mainfrom
agent/working-session
Sep 26, 2026
Merged

EdwardIrby merged 55 commits into
mainfrom
agent/working-session

Conversation

@EdwardIrby

@EdwardIrby EdwardIrby commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Context

  • Day's working branch carrying the session-id wire, the full attach-or-start + TUI stack, and the day's engine/composition work — all built per the session's .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

  • Identity wire: TraceBase gains sessionId beside instanceId (accept-only, host-supplied, defaults to the instanceId; behavioral() return unchanged) — the dormant audit axis
  • Host topology: instance lock (<home>/instance.pid) + unix-socket attach lane (<home>/instance.sock) speaking the existing JSON-RPC vocabulary through the same dispatcher as serve (one protocol, one dispatcher, multiple carriers); bare behavioral is attach-or-start; the minimal TUI (three primitives, tui_* guard entries from TUI_DETAIL_SCHEMAS); static GUI serving on the same listener + --dev (start-time only); bundleController promoted to src/controller/
  • Remote consolidation into the shell faculty: the generic fetch-based JSON-RPC 2.0 client and its rpc op; the security faculty (credential vending, issuer-bound OAuth over Bun.secrets) with the credential seam's vend-and-replay threads; remote-mcp as threads over the rpc op (the mcp faculty is retired)
  • Thread admission (frontier-mediated): the add_thread op (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)
  • Runtime supervision: the counting circuit breaker + block-then-judge + recovery (bounded judge-retry, the host override ingress), mounted by bProgram({ supervision: { watch, threshold } })
  • Plugin threads (agent-plugins.org §8.2): the sh.behavioral/threads/ namespace scan, the proposal path (worker import → validation → one add_thread per export), and the <home> admission registry (hash-keyed, durable, snapshot boots)
  • Root authority: space matching is root-sees-all — an unstamped listener matches every space (all four idioms), a stamped listener stays confined to its own space; the root guard now validates every space's traffic
  • The ui_ producer threads + the autoresearch loop*: the DESIGN.md scan → store tenant (no-lock: the runtime reads the user's <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 scoped ui_style egress (an @scope block rooted on the render target — Baseline 2026, silent drop = plain degradation), and the loop's raw capture + frontier replay
  • Docs: the behavioral skill references rewritten to match the code; the shipped DESIGN.md; the "pack" vocabulary retired

Changed Files

  • src/behavioral/ — the sessionId wire, root-authority space matching, the cascade characterization specs, schemas + suites
  • src/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.ts guard 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 + suites
  • src/controller/ — bundle-controller (promoted), the ui_style kind + StyleDetailSchema, #style applier, controller + schema suites (real-browser WebView specs)
  • bin/behavioral.ts — the default command registration
  • skills/behavioral/ — references (behavioral.md, controller.md, eval.md), the shipped DESIGN.md asset
  • .github/, scripts/ — CI hygiene

Known Failures / Drift

  • The first CI run failed three tests (diagnosed, fixed in 731d268, verified in a Linux x64 Bun 1.4.2 container): the progress-looper fixture was an admitted self-sustainer (its own request self-selected, re-arming the thread inside one cascade — the recursive super-step overflowed; the fixture is now once), a cold-spawn race in shell_cancel (now polls), and the two registry tests' slow-runner timeouts (now 20s explicit)
  • Open branches (deliberately not landed, logged in the session plan): the engine yield valve (an admitted self-sustainer spins-but-terminable needs an engine change — a 6e906970 amendment), the unguarded default-faculty result kinds (mounting guards changes admission-replay verdicts), the style-target CSS-injection flag (pattern-constrain or escape)
  • Two Bun runtime findings shaped the code: readline with terminal: false drops buffered lines between question() calls (explicit queue); Bun.file().exists() returns false for socket files (socket checks use node:fs existsSync)
  • sessionId is a dormant axis: landed and defaulted but currently degenerate (always equal to instanceId) — no consumer supplies a real one yet

Review Notes / Residual Risks

  • 808 specs green, bun --bun tsc --noEmit clean, biome clean; the last CI run of the branch is green
  • The @scope-based ui_style requires Baseline 2026 engines (Chrome/Edge 118+, Firefox 146+, Safari 26.4) — older engines drop the block silently (the plain-degradation lane), never break
  • Fullstack dev server (--dev) is Bun-WIP upstream; degrade path is full page reloads, and the flag is opt-in start-time only
  • The PR carries 55 commits across the day's session slices — git log main..HEAD is the slice ledger; every slice has its spec, and the docs landed in the same commits

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.
Comment thread src/behavioral/behavioral.ts Fixed
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.
@EdwardIrby EdwardIrby changed the title session-id wire + attach-or-start TUI The working-session stack: host topology, faculties consolidation, thread admission + supervision, plugin threads, root authority, the ui_* producer threads + autoresearch loop Sep 26, 2026
@EdwardIrby
EdwardIrby merged commit 1345ef2 into main Sep 26, 2026
5 checks passed
@EdwardIrby
EdwardIrby deleted the agent/working-session branch September 26, 2026 08:15
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.

2 participants