Files
stack/agents/filbert/work/s6/BUILD.md
T
jason.woltjeandClaude Opus 5.5 9b670e27b2 docs(s6): row 41 slice 1 S6 round 1 candidate packet (filbert)
build.patch dc346027…4454, candidate-manifest 5b3a934d…27f3, base 915e00e5,
41 files, +4302/-55. Gate at 2855e628 in out/: every node suite and every
test-*.sh green except test-task's live recall (needs Docker; the base
fails the same two). The recorded PM launch waits on the Q14 cutover.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 19:56:01 -05:00

9.2 KiB
Raw Blame History

Row 41 (#1523): slice 1 S6, candidate packet, round 1

Author: Filbert. Reviewer: Darkwing. Brief: docs/plans/2026-10-04_slice-1.md, section "Slice 1 S6", blob 72d11de2. Plan: PLAN.md here (b6d2fe2b). Rulings: lead decisions 77 and 78. Base: 915e00e5 (base.txt). None of the 41 files changed between the base and 2855e628, where the gate ran. The candidate source is uncommitted. There are no pushes. No token, private binding or tracker host was read or written, and nothing touched the live mosaic-bus@mosaic-stack unit or ~/.mosaic-dev/bus/.

Files (files.txt, 41)

build.patch is git diff --cached --binary 915e00e5 over those files, +4302/−55. It applies cleanly to 2855e628 with git apply --index, and sha256sum -c candidate-manifest.sha256 passes in that tree.

Area Files What
Harness (new) packages/harness/ bundle, typed tools, gate, Pi extension, Claude Code gate and MCP server, runner, tests, README
Adapters adapters/claude/adapter.sh (new), adapters/pi/adapter.sh, adapters/README.md Claude Code adapter; Pi takes MOSAIC_EXTENSIONS as -e, and --no-approve
Sessions packages/seat/src/session.mjs, proc.mjs (new), tests, README spawn under unshare, registry, launch log, stop
Launcher and verbs packages/cli/src/launcher.mjs (new), host.mjs, cli.mjs, tests, README the launch socket in the trusted host; mosaic talk, stop, launches off|on|list; bus start --pm
Broker packages/bus/src/{broker,process,runtime}.mjs, tests/end-launch.test.mjs, README trusted IPC launch ops; Broker.endLaunch
Other docs/TOOLS.md, scripts/mosaic, scripts/agent-host-dev.sh verb reference; host seats pass --no-approve (the DEFERRED entry)

Against the brief

  • Bundles, Pi then Claude Code (packages/harness/README.md, "The bundle"): prompt, policy, typed tools and a manifest from one resolved instance. Each manifest names the S0 lines it relies on (reliesOn). Skills: resolution runs without a skills source, so bundles list none (README "Limits").
  • S0 lines (README table): Pi blocks in a tool_call handler, a throw blocks, a missing -e refuses to start, and a hang is bounded by the runner's wall clock plus the agent_end turn marker. Claude Code uses a command hook run as timeout -k 2 10 <gate> || exit 2 with hook timeout 20; --bare is never passed. --restricted is defence in depth only. Line 5 (bash) is the tool limit and is written as a limit.
  • The PM launches through the broker, within launch: the host's launch socket checks the instance list, that the instance isn't already running, the model family against launch.max (every running session counts, the PM's too), then the broker's own role.launch authorization. Every launch, refusal and end is a launch-log line and a broker event. mosaic launches off is Jason's one word (launch.revoke). mosaic stop <run> ends a session.
  • Founder credentials (REQ-CRED-2): the session environment is an allowlist (ENV_ALLOW). The runner exits 20 before claiming if a known founder variable reached it, or if a service the role needs has no usable role token (the broker's credential status, metadata only).
  • The PM moves off T3: mosaic bus start <business> --pm launches the business's launch.by instance without a window, and mosaic talk reaches it. The handover of Sage's T3 thread is the acceptance run below.

Changes from the plan

  1. The capability goes to the runner as one stdin line, not a 0600 file. It is written after the bind and is never on disk, in argv or in the environment.
  2. Verbs live in packages/cli, not packages/seat (lead decision 77). packages/seat keeps the session module and the moved /proc helpers (proc.mjs).
  3. --pm takes no argument. The PM is the business file's launch.by instance; its harness and model come from resolution.
  4. Runner exit 23 covers a role.claim with no answer as well as brokerRetries failed polls.
  5. The adapter runs as /bin/sh <adapter.sh>, because the repository keeps adapters 0644 and only the image sets the mode.

Broker surface

No socket verb, no event kind and no schema change. The new launch ops are trusted IPC from the host to the broker process: identity, authorizeLaunch, refuse, endLaunch, credentialStatus, beside the existing bindLaunch. bindLaunch records the run only after its checks, and a rebind of an ended run refuses with run-ended, also across a broker restart. The host gains op(message) and beforeClose(fn).

Process control

  • Early signals. The runner installs its SIGTERM and SIGINT handlers before anything else, because pid 1 of a PID namespace ignores a signal with no handler. Node's own startup still leaves a window, so the launcher and mosaic stop resend SIGTERM every second.
  • isRunner(pid) is cmdline[1] === RUNNER, which skips unshare's forked child before its exec (its argv still names the runner).
  • Host lost. A starting host reads the last host's sessions.json. Before the launch socket opens, for each entry of this business it SIGKILLs the session if it still runs, rebinds the run with the recorded identity and ends it as host-lost, which releases the role. A rebind that refuses with run-ended logs "was already ended". An unreadable or malformed sessions.json refuses the host start with exit 3.
  • PID namespace limits are in packages/harness/README.md, "Limits": same UID, shared broker socket and /tmp, network not confined, and a same-UID process can still ask something outside its tree (a systemd user manager, an existing tmux server) to run a command. The README no longer says the namespace blocks ptrace in general; it hides other sessions by pid.

--no-approve (DEFERRED: "Host seats launch Pi with --approve")

scripts/agent-host-dev.sh and adapters/pi/adapter.sh now pass --no-approve. Pi 0.85.1 (trust-manager.js) gates project .pi/ resources on trust: settings.json, extensions, skills, prompts, themes, SYSTEM.md, APPEND_SYSTEM.md. -e paths load in the "temporary" scope, which trust doesn't gate. A scratch probe against a workspace saved as trusted, with a mock Messages API, showed:

argv project SYSTEM.md explicit --skill generated prompt -e extension
host-seat, --approve loaded loaded loaded loaded
host-seat, --no-approve ignored loaded loaded loaded
adapters/pi/adapter.sh ignored loaded loaded loaded

The skill appears only when the session has a tool to read it; the probe's first run used --no-tools and showed no skill in any case, the --approve control included.

agent-host-dev.sh keeps --append-system-prompt, not the entry's "explicit system prompt", because its comment preserves Pi's built-in coding prompt on purpose, and --no-approve already closes the project SYSTEM.md. I didn't edit docs/plans/DEFERRED.md: it holds another seat's uncommitted changes. The entry can close when this lands. scripts/test-goal-native.py still passes --approve on purpose (it tests project extensions) and is untouched.

Gate

Run at 2855e628 with build.patch applied, in a detached worktree, sequentially, each output in out/ (out/summary.txt). TMPDIR on the scratch disk; DOCKER_HOST=unix:///nonexistent.sock, so nothing reaches Docker.

Suite Pass Fail
node: bus, business, cli, control-board 74, 60, 77, 124 0
node: conversation, discord, harness, ledger 152, 178, 45, 78 0
node: mosaic, queue, seat, tasks, webui 69, 148, 27, 51, 14 0
test-auth, conductor, config, discord 15, 17, 24, 66 0
test-extension-package, foundation, queue, release 18, 44, 27, 4 0
test-task 26 2

The two test-task failures are "user recall run succeeds (exit 1)" and "recalled user name". That check runs a live worker and needs Docker. The unpatched base fails the same two (out/base-test-task.txt, identical PASS/FAIL lines), so they are the environment, not this candidate.

An earlier gate over this candidate without the --no-approve edits had test-queue fail F1 once ("a plain commit -e whose guard ran before update-ref fails at its own HEAD update": only the stderr match). It passed in two patched reruns, in the base run, and in this gate. See the follow-ups below.

Acceptance run: waiting

The recorded run (the host started with --pm, mosaic talk asks the PM to launch a coder, mosaic agents shows both claims) waits on the Vikunja move to tasks.woltje.com (Q14), as Sage ruled: the integration commit and the acceptance run wait for that cutover; build and review continue. It will not run on Astra (R26) or against the live unit. The tracker host comes only from vars.tracker.baseUrl; no code, test or launcher config names one, and fixtures use 127.0.0.1 and example.test.

Follow-ups (not in this row)

  • packages/queue/tests/commit.test.mjs, pausedCommit, awaits the child's exit rather than close, so its stderr can be unread when test-queue F1 matches it.
  • scripts/test-task.sh (:514-518): the live user-recall check needs Docker and isn't skipped when Docker is unavailable.