feat(playbot-lane): onboard Playbot 0.94.0 native lane via fused threads:launch contract - #9
Merged
Merged
Conversation
…94.0
Playbot 0.94.0 removed the threads:openThread and db:workspaceThreads:open
IPC channels. "Open a thread" was restructured: the operation now flows
through threads:launch, which the app mints the thread id for and returns
the persisted thread ({workspace, thread, activate, createdWorkspace}),
with activation split into threads:setActiveThread. This inverts every
assumption the legacy lane relied on (caller-minted chat-N-N id, undefined
result, single-channel open).
Make the thread-open wire contract release-aware while keeping the abstract
operation and evidence key stable at threads:openThread:
- releaseCompatibilityShape now parameterizes ipcChannelStrings and carries a
threadOpen descriptor; the 0.94.0 seed uses the launch/setActive surface and
the app-minted-id, non-undefined-result contract.
- MUTATION_WIRE_CHANNELS decouples the IPC-channel allowlist from the abstract
operation keys so threads:launch is invokable; gatedInvoke takes an explicit
wireChannel so the evidence gate still keys off threads:openThread.
- mutationOpenThread dispatches to the legacy path (unchanged) or a new launch
path that consumes the app-minted result.thread.id via validateThreadLaunchResult
and re-asserts the persisted row.
- The Phase-1 smoke records the real wire channel and app-minted id in the
evidence body so the 0.94.0 overlay honestly reflects the launch contract.
The static IPC surface and payload shapes were verified read-only against the
live 0.94.0 app.asar. doctor --json on 0.94.0 now passes release_compatibility,
both DB-schema dims, and ipc_channel_strings_static honestly; native-enabled
still awaits the Phase-1 disposable smoke as designed.
Tests and the fixture bundle extend to the 0.94.0 launch contract.
…+ record evidence 0.94.0 removed the standalone workspace:create channel and fused workspace creation into threads:launch with a new-workspace destination (one call creates the workspace and opens its first thread). The prior static IPC scan had false-positived workspace:create by matching it as a substring of the workspace:created event. Extend the release-aware adapt to the fused create+open contract: - Add a release-aware workspaceCreate descriptor (legacy standalone workspace:create vs 0.94.0 fused threads:launch), parallel to threadOpen. mutationWorkspaceCreate dispatches to the legacy or fused-launch path. - The fused path creates the workspace via threads:launch(new-workspace), reconstructs the workspace result from the authoritative DB row (unchanged downstream contract), and returns the fused thread id. The abstract evidence keys stay workspace:create and threads:openThread; the smoke adopts the fused thread rather than opening a second one and records BOTH guarantees (workspace created + thread opened) from the single launch, each body annotated with the real wire channel and the fusion. - The fused launch provisions its worktree root asynchronously after returning, so add waitForWorkspaceProvisioned to poll until workspace_roots lands before asserting workspace identity and branch. Harden the static IPC surface check against substring false-positives: - scanFileForNeedles gains an exactToken mode requiring each needle to be bounded by non-channel-token bytes, so an event string can never satisfy a command-channel needle. The doctor's ipc_channel_strings_static uses it; the preload-bridge substring scan is unchanged. Streaming boundary handling keeps cross-chunk matches correct. The 0.94.0 IPC surface, fused-launch payload/result, and async provisioning were verified read-only against the live app.asar and then proven end to end by the Phase-1 disposable smoke: doctor reports native-enabled and the smoke recorded signed evidence for every 0.94.0 mutation plus write-denial confinement (0.92.0/0.93.1 evidence preserved). The smoke targets only the registered disposable project, never MAIN. Tests cover the workspace-create contract, the fused launch result validation, and the exact-token accept/reject behavior.
…ioned path, dedupe payload
…and smoke evidence
jokim1
added a commit
that referenced
this pull request
Aug 25, 2026
…ads:launch contract (#9) * feat(playbot-lane): release-aware thread-open contract for Playbot 0.94.0 Playbot 0.94.0 removed the threads:openThread and db:workspaceThreads:open IPC channels. "Open a thread" was restructured: the operation now flows through threads:launch, which the app mints the thread id for and returns the persisted thread ({workspace, thread, activate, createdWorkspace}), with activation split into threads:setActiveThread. This inverts every assumption the legacy lane relied on (caller-minted chat-N-N id, undefined result, single-channel open). Make the thread-open wire contract release-aware while keeping the abstract operation and evidence key stable at threads:openThread: - releaseCompatibilityShape now parameterizes ipcChannelStrings and carries a threadOpen descriptor; the 0.94.0 seed uses the launch/setActive surface and the app-minted-id, non-undefined-result contract. - MUTATION_WIRE_CHANNELS decouples the IPC-channel allowlist from the abstract operation keys so threads:launch is invokable; gatedInvoke takes an explicit wireChannel so the evidence gate still keys off threads:openThread. - mutationOpenThread dispatches to the legacy path (unchanged) or a new launch path that consumes the app-minted result.thread.id via validateThreadLaunchResult and re-asserts the persisted row. - The Phase-1 smoke records the real wire channel and app-minted id in the evidence body so the 0.94.0 overlay honestly reflects the launch contract. The static IPC surface and payload shapes were verified read-only against the live 0.94.0 app.asar. doctor --json on 0.94.0 now passes release_compatibility, both DB-schema dims, and ipc_channel_strings_static honestly; native-enabled still awaits the Phase-1 disposable smoke as designed. Tests and the fixture bundle extend to the 0.94.0 launch contract. * feat(playbot-lane): fuse 0.94.0 workspace-create into threads:launch + record evidence 0.94.0 removed the standalone workspace:create channel and fused workspace creation into threads:launch with a new-workspace destination (one call creates the workspace and opens its first thread). The prior static IPC scan had false-positived workspace:create by matching it as a substring of the workspace:created event. Extend the release-aware adapt to the fused create+open contract: - Add a release-aware workspaceCreate descriptor (legacy standalone workspace:create vs 0.94.0 fused threads:launch), parallel to threadOpen. mutationWorkspaceCreate dispatches to the legacy or fused-launch path. - The fused path creates the workspace via threads:launch(new-workspace), reconstructs the workspace result from the authoritative DB row (unchanged downstream contract), and returns the fused thread id. The abstract evidence keys stay workspace:create and threads:openThread; the smoke adopts the fused thread rather than opening a second one and records BOTH guarantees (workspace created + thread opened) from the single launch, each body annotated with the real wire channel and the fusion. - The fused launch provisions its worktree root asynchronously after returning, so add waitForWorkspaceProvisioned to poll until workspace_roots lands before asserting workspace identity and branch. Harden the static IPC surface check against substring false-positives: - scanFileForNeedles gains an exactToken mode requiring each needle to be bounded by non-channel-token bytes, so an event string can never satisfy a command-channel needle. The doctor's ipc_channel_strings_static uses it; the preload-bridge substring scan is unchanged. Streaming boundary handling keeps cross-chunk matches correct. The 0.94.0 IPC surface, fused-launch payload/result, and async provisioning were verified read-only against the live app.asar and then proven end to end by the Phase-1 disposable smoke: doctor reports native-enabled and the smoke recorded signed evidence for every 0.94.0 mutation plus write-denial confinement (0.92.0/0.93.1 evidence preserved). The smoke targets only the registered disposable project, never MAIN. Tests cover the workspace-create contract, the fused launch result validation, and the exact-token accept/reject behavior. * no-mistakes(review): reject explicit launch thread id, require provisioned path, dedupe payload * no-mistakes(review): restore smoke-attested lanes script, defer fixes to follow-up * no-mistakes(document): document 0.94.0 fused threads:launch contract and smoke evidence
jokim1
added a commit
that referenced
this pull request
Aug 25, 2026
…ads:launch contract (#9) * feat(playbot-lane): release-aware thread-open contract for Playbot 0.94.0 Playbot 0.94.0 removed the threads:openThread and db:workspaceThreads:open IPC channels. "Open a thread" was restructured: the operation now flows through threads:launch, which the app mints the thread id for and returns the persisted thread ({workspace, thread, activate, createdWorkspace}), with activation split into threads:setActiveThread. This inverts every assumption the legacy lane relied on (caller-minted chat-N-N id, undefined result, single-channel open). Make the thread-open wire contract release-aware while keeping the abstract operation and evidence key stable at threads:openThread: - releaseCompatibilityShape now parameterizes ipcChannelStrings and carries a threadOpen descriptor; the 0.94.0 seed uses the launch/setActive surface and the app-minted-id, non-undefined-result contract. - MUTATION_WIRE_CHANNELS decouples the IPC-channel allowlist from the abstract operation keys so threads:launch is invokable; gatedInvoke takes an explicit wireChannel so the evidence gate still keys off threads:openThread. - mutationOpenThread dispatches to the legacy path (unchanged) or a new launch path that consumes the app-minted result.thread.id via validateThreadLaunchResult and re-asserts the persisted row. - The Phase-1 smoke records the real wire channel and app-minted id in the evidence body so the 0.94.0 overlay honestly reflects the launch contract. The static IPC surface and payload shapes were verified read-only against the live 0.94.0 app.asar. doctor --json on 0.94.0 now passes release_compatibility, both DB-schema dims, and ipc_channel_strings_static honestly; native-enabled still awaits the Phase-1 disposable smoke as designed. Tests and the fixture bundle extend to the 0.94.0 launch contract. * feat(playbot-lane): fuse 0.94.0 workspace-create into threads:launch + record evidence 0.94.0 removed the standalone workspace:create channel and fused workspace creation into threads:launch with a new-workspace destination (one call creates the workspace and opens its first thread). The prior static IPC scan had false-positived workspace:create by matching it as a substring of the workspace:created event. Extend the release-aware adapt to the fused create+open contract: - Add a release-aware workspaceCreate descriptor (legacy standalone workspace:create vs 0.94.0 fused threads:launch), parallel to threadOpen. mutationWorkspaceCreate dispatches to the legacy or fused-launch path. - The fused path creates the workspace via threads:launch(new-workspace), reconstructs the workspace result from the authoritative DB row (unchanged downstream contract), and returns the fused thread id. The abstract evidence keys stay workspace:create and threads:openThread; the smoke adopts the fused thread rather than opening a second one and records BOTH guarantees (workspace created + thread opened) from the single launch, each body annotated with the real wire channel and the fusion. - The fused launch provisions its worktree root asynchronously after returning, so add waitForWorkspaceProvisioned to poll until workspace_roots lands before asserting workspace identity and branch. Harden the static IPC surface check against substring false-positives: - scanFileForNeedles gains an exactToken mode requiring each needle to be bounded by non-channel-token bytes, so an event string can never satisfy a command-channel needle. The doctor's ipc_channel_strings_static uses it; the preload-bridge substring scan is unchanged. Streaming boundary handling keeps cross-chunk matches correct. The 0.94.0 IPC surface, fused-launch payload/result, and async provisioning were verified read-only against the live app.asar and then proven end to end by the Phase-1 disposable smoke: doctor reports native-enabled and the smoke recorded signed evidence for every 0.94.0 mutation plus write-denial confinement (0.92.0/0.93.1 evidence preserved). The smoke targets only the registered disposable project, never MAIN. Tests cover the workspace-create contract, the fused launch result validation, and the exact-token accept/reject behavior. * no-mistakes(review): reject explicit launch thread id, require provisioned path, dedupe payload * no-mistakes(review): restore smoke-attested lanes script, defer fixes to follow-up * no-mistakes(document): document 0.94.0 fused threads:launch contract and smoke evidence
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.
Intent
Onboard Playbot 0.94.0 to the native lane so bin/fm-playbot-lanes.mjs doctor reaches native-enabled and native dispatch is un-gated, matching the 0.93.1 onboard precedent (commit 66edbb5).
0.94.0 removed two IPC channels the lane depended on, so this is a release-aware adaptation, not just a manifest seed entry. Captain approved ADAPT (over downgrading or staying courier-primary); firstmate then chose to extend the adapt to the fused create+open contract and to harden the static IPC check in the same PR.
Deliberate decisions a diff reviewer would not otherwise know:
Everything was verified read-only against the live 0.94.0 app.asar and then proven end to end by the Phase-1 disposable smoke (operator-run, targets only the registered disposable project, never MAIN): doctor reports native-enabled and the smoke recorded signed evidence for every 0.94.0 mutation plus write-denial confinement, preserving 0.92.0/0.93.1 evidence. Tests cover the workspace-create and thread-open contracts, the fused launch result validation, and the exact-token accept/reject behavior. The committed evidence files under docs/verification/playbot-mutation-evidence/ are smoke-produced signed artifacts, not hand-written.
What Changed
bin/fm-playbot-lanes.mjswith a release-aware adaptation: 0.94.0 removed thethreads:openThread,db:workspaceThreads:open, andworkspace:createIPC channels, so thread-open now routes throughthreads:launch(existing-workspace, app-minted thread id) and workspace creation is fused intothreads:launch(new-workspace), which creates the workspace and opens its first thread in one call. The abstract operations and evidence keys are unchanged; evidence bodies record the real wire channel and fusion annotations, andwaitForWorkspaceProvisionedpolls for the asynchronously provisioned worktree root.scanFileForNeedleswith anexactTokenmode used by the doctor'sipc_channel_strings_staticcheck, so an event string (e.g.workspace:created) can no longer satisfy a command-channel needle by substring; the preload-bridge substring scan is unchanged.2026-08-20T16-17-04-891Z) covering every 0.94.0 mutation operation plus write-denial confinement while preserving 0.92.0/0.93.1 evidence, and extended the hermetic test suite with the 0.94.0 thread-open/workspace-create contract, fused-launch result validation, and exact-token accept/reject cases. Three known cleanups (explicit--thread-idrejection on the launch path, non-empty provisioned-path check, launch-payload dedupe) were intentionally reverted to keep the script byte-identical to the smoke-attested version and are deferred to a follow-up that re-runs the smoke, as noted in the pipeline review.Risk Assessment
✅ Low: The final tree is byte-identical to the round-1 reviewed head whose evidence chain I verified end-to-end (record hashes match overlay pointers, overlay matches the signed receipt, and the receipt's lanesSha256 again matches the committed script, restoring the doctor's path to native-enabled), with the three minor code findings explicitly deferred by the user to a follow-up smoke-backed PR.
Testing
Ran the full hermetic lanes suite (green, including the new 0.94.0 contract and exact-token tests), then demonstrated the intent live: the target-commit doctor and ready commands against the installed Playbot 0.94.0 report native-enabled with mutations un-gated while the base-commit doctor fails closed on the same app; verified the live app.asar wire-channel claims via exact-token vs substring scans (workspace:create exists only inside workspace:created); and independently verified the committed smoke evidence chain — receipt signature, overlay hash, and all 21 record hashes across 0.92.0/0.93.1/0.94.0 — plus the fused-launch and write-denial annotations in the 0.94.0 records. No visual artifacts because the surface is a CLI; JSON transcripts are the end-user surface. No live mutation/smoke was run (operator-only, disposable-project-only by design).
Evidence: Live doctor on Playbot 0.94.0 (target commit): native-enabled
"appVersion": "0.94.0", "operatingState": "native-enabled", "readOnlyReady": true, "ready": true, "mutationsEnabled": true; mutation_evidence_integrity verified all 21 entries across 0.92.0/0.93.1/0.94.0 with zero refusalsEvidence: Base-commit doctor on the same live app: fails closed
exit 2 — "operatingState": "phase1-evidence-required", "mutationsEnabled": false, reason: release absent from compatibility manifestEvidence: Ready verdict (native capability) on live 0.94.0
{ "capability": "native", "ready": true, "operatingState": "native-enabled", "mutationsEnabled": true, "reason": null }Evidence: Live app.asar IPC scan: exact-token vs substring
0.94.0 channel surface (threads:launch, threads:setActiveThread, send, stop, archiveThread, workspace:archive, workspace:delete) all pass exact-token; workspace:create passes substring but FAILS exact-token (only workspace:created exists); threads:openThread and db:workspaceThreads:open absentEvidence: Evidence signature + hash verification (all releases)
ssh-keygen: Good "firstmate-playbot-smoke" signature for jokim1 with ED25519 key; receipt overlaySha256 matches overlay; all 21 record contentSha256 values verify (0.92.0, 0.93.1, 0.94.0 incl. confinement)Evidence: Hermetic lanes test suite transcript
fm-playbot-lanes: all tests passed (incl. 0.94.0 release-aware contracts, fused-launch validation, exact-token accept/reject)Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-playbot-lanes.mjs:1770- On 0.94.0, mutationOpenThreadLaunch silently ignores request.id: the payload only carries destination/thread/activate, soopen-thread --thread-id <id>(bin/fm-playbot-lanes.mjs:4019 passes args['thread-id']) accepts an explicit caller-minted id it cannot honor and returns a different app-minted id without any error. The launch path should throw when request.id is provided, since the 0.94.0 contract makes a caller-chosen id unsatisfiable.bin/fm-playbot-lanes.mjs:1661- The fused 0.94.0 create asserts only the workspace:create evidence gate (gatedInvoke('workspace:create', ..., wireChannel: 'threads:launch')), yet the same wire call also performs the thread-open operation. If threads:openThread evidence were individually refused (e.g. one corrupt record) while workspace:create verified, CLIcreatewould still open a thread without that gate. Edge case: both records come from the same smoke, and native dispatch separately requires all operations, so impact is marginal. This matches the intent's single-fused-operation modeling; flagging the gating asymmetry for awareness.bin/fm-playbot-lanes.mjs:1562- waitForWorkspaceProvisioned returns once project_root_id, archive_state, and branch match, but never checks that workspace_roots.path is non-empty, and the smoke immediately consumes workspaceRow.path as worktreePath for the confinement probe. If the app ever committed the root row with a null path (LEFT JOIN would surface it), downstream would proceed with a null worktree path. Adding a non-empty path condition to the poll would make the 'fully provisioned' guarantee complete.bin/fm-playbot-lanes.mjs:1772- The thread/activate payload block ({ title, approvalMode, planMode, ephemeral } plus activate: request.activate !== false) is duplicated between mutationWorkspaceCreateFusedLaunch (bin/fm-playbot-lanes.mjs:1651-1657) and mutationOpenThreadLaunch (bin/fm-playbot-lanes.mjs:1772-1778). Extracting a small shared launchThreadPayload helper would prevent the two launch payload shapes from drifting apart in future release adaptations.🔧 Fix: reject explicit launch thread id, require provisioned path, dedupe payload
1 error still open:
docs/verification/playbot-mutation-evidence/publications/2026-08-20T16-17-04-891Z/overlay.v1.json.receipt.json:16- The review-fix commit d1303fc edited bin/fm-playbot-lanes.mjs after the Phase-1 smoke, invalidating the committed 0.94.0 evidence attestation: the receipt pins lanesSha256=30975d99... (the a4809d4 script), but the file now hashes to ee3f73b4..., and verifyEvidenceAttestation (bin/fm-playbot-lanes.mjs:345-348) enforces that binding in every default loadCompatibilityManifest call. At this head the overlay is refused ('evidence receipt lanes binary digest mismatch'), all mutation evidence reverts to PHASE1-EVIDENCE-REQUIRED, doctor reports phase1-evidence-required, and native dispatch is re-gated — contradicting the intent's required outcome: 'doctor reaches native-enabled and native dispatch is un-gated'. The hermetic tests will NOT catch this because fixture receipts are minted at test time with the current file hash. Resolution requires operator action, since evidence must be smoke-produced and signed (intent: 'smoke-produced signed artifacts, not hand-written'): re-run the Phase-1 disposable smoke against the fixed script to publish a fresh receipt (verifyPriorEvidenceForSmoke uses ignoreLanesSha256, so 0.92.0/0.93.1 evidence is preserved), or drop the fix commit and ship the exact smoked script.🔧 Fix: restore smoke-attested lanes script, defer fixes to follow-up
1 info still open:
bin/fm-playbot-lanes.mjs:1766- Deliberate deferral on record: commit 28007c6 restores bin/fm-playbot-lanes.mjs byte-identical to the smoke-attested a4809d4 version (sha256 30975d99... matches the receipt's lanesSha256, so evidence attestation verifies and doctor can reach native-enabled). As a consequence, the three round-1 findings (mutationOpenThreadLaunch silently ignoring an explicit --thread-id on 0.94.0, waitForWorkspaceProvisioned not requiring a non-empty workspace_roots.path, and the duplicated launch thread/activate payload block) are intentionally back in the code, per the user's explicit instruction to defer them to a follow-up PR that re-runs the Phase-1 smoke and regenerates evidence bound to the fixed script. No action in this PR.✅ **Test** - passed
✅ No issues found.
bash tests/fm-playbot-lanes.test.sh(hermetic suite incl. new 0.94.0 thread-open/workspace-create contract tests, validateThreadLaunchResult accept/reject, exactToken scan accept/reject) — all passnode bin/fm-playbot-lanes.mjs doctor --jsonread-only against live Playbot 0.94.0 → operatingState native-enabled, mutationsEnabled true, exit 0base-commit (66edbb5) doctor against the same live app → fails closed with 'release absent from compatibility manifest', mutationsEnabled false, exit 2node bin/fm-playbot-lanes.mjs ready --json --capability native→ ready true, native-enabledexact-token vs substringscanFileForNeedlesagainst the live /Applications/Playbot.app app.asar: 0.94.0 channel surface all present; workspace:create matches only by substring (workspace:created event) and is rejected exact-token; threads:openThread and db:workspaceThreads:open absentssh-keygen -Y verifyof the 2026-08-20T16-17-04-891Z publication receipt against allowed_signers (good firstmate-playbot-smoke signature) plus receipt overlaySha256 matchindependent sha256 verification of all 21 evidence records (0.92.0, 0.93.1, 0.94.0 mutation ops + confinement) against the published overlaymanual inspection of 0.94.0 workspace:create/threads:openThread/confinement evidence records for the fused-launch annotations (wireChannel, fused, fusedThreadId, fusedWith, idSource, write-denial)bin/fm-playbot-lanes.mjs:3745- bin/fm-playbot-lanes.mjs --help (the designated owner of exact CLI flags) still listsopen-thread [--thread-id <native-id>], which is silently ignored on 0.94.0; fixing it requires editing the smoke-attested script (breaking the evidence receipt's lanesSha256) and re-running the operator smoke, so it should land with the follow-up already deferred by commit 28007c6 (which also restores d1303fc's explicit rejection of a caller-chosen launch thread id). The operator doc now carries the correct behavior in the meantime.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.