feat(tuval)!: a command may only send, and a bare port name lands on its own program - #9221
Conversation
🚀 Preview deployed
|
|
governance: PASS @ 6fd0902 content:093d43fd9d8f — no contradiction, no weakening Governance verdict at 6fd0902 — PASS. Derivation. Questions the change decides.
Corpus half. 0372 itself. The lead "What this decides" paragraph is kept verbatim and a dated amendment note immediately follows it; the Consequences section replaces the two "open hole" bullets (#8898, #8858) with the ruled shape. Frontmatter stays Gate half. Evidence cited from issues (#8898 / #8858 ruling comments) is named as context only; the verdict rests on the record text and the diff. |
|
review-code: PASS @ 6fd0902 content:093d43fd9d8f — merge-ready review-code at 6fd0902 — PASS. CI at head: Acceptance criteria (#8898,
#8858 ( Fan-out.
Deviations table:
Verdict-written: 2026-09-15T07:53:46Z |
|
review-doc: PASS @ 6fd0902 content:093d43fd9d8f — merge-ready review-doc at 6fd0902 — PASS. Slice:
Conversation-authored: no. Bound to #8898 criterion 4 (in-tree half), which this slice discharges. Verdict-written: 2026-09-15T07:53:53Z |
|
routed-elsewhere: review-ui @ b0f86ee — no rendered delta: six ui-class files are authoring types, handlers and tests under apps/tuval/src/authoring/, none paints a surface The
No Text judgment of these files is |
|
ship: BASE-CONFLICTED — PR #9221 @ 6fd0902 → repair
Every gate at this head was clean before the pre-arm read: gate satisfied (review-code pass, review-doc pass, review-ui routed, governance pass), checks green (37 gating success, 6 skipped), evidence present, 0 unresolved threads. A rebase moves the merge-base blob every verdict's content digest covers, so the rebased head owes a full re-review and a fresh gate pass before any re-enqueue. Merge intent disarmed at site refuse (was not armed). |
6fd0902 to
473b777
Compare
|
governance: PASS @ 473b777 content:edd4d0e9a162 — no contradiction, no weakening Governance verdict at 473b777 — PASS. Scope. Questions the diff decides.
Corpus half. Gate half. This diff derives the governance namespace; whether it needs a code-owner approval is a separate question CODEOWNERS answers. |
…its own program `CommandEffect` is `SendEffect<SendTarget>`: `send`, and nothing else. The argument ADR 0372 made against `emit` runs one effect further, which is what the founder ruled on #8898 and #8858. `spawn` and `ask` read `ProcessSelf` for a `self` a spell call does not have, `reply` spends a correlation it was never handed, and `stop` ends a process it holds no claim on — so all four are undeclarable in a `commands` cell, refused by the checker at the line that wrote them. The target widens where the verb narrows. `send("pr", pr)` names a port and no process; `resolveOwnProcess` answers which process of the declaring program it lands on, by the ruled rule: exactly one live, that one; several with the caller's own `Scope.process` among them, the caller's; anything else, a typed refusal naming the program and the ids, which comes back as a spell reply rather than a silent no-op. The read is `ProcessTable`, which `Kernel` already names, so the composition root owes nothing new. The explicit `send({process, port}, …)` form is untouched. `COMMAND_HANDLERS` is one key, `CommandEffectServices` one service, and the worked `pr-review` example declares `run: (pr) => send("pr", pr)` — the shape #8716 R16.1 always asked for. Ruling: #8898 (comment) Fixes #8898 Fixes #8858 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
review-code: PASS @ 473b777 content:edd4d0e9a162 — merge-ready review-code at 473b777 — PASS. Re-review round: the prior PASS was bound to 6fd0902; the head moved by a rebase (base 69fcba3 → 47fc984) and the PR's own 12-file diff was re-read whole at this head. CI at head: Acceptance criteria (#8898,
Fan-out.
Deviations table:
Verdict-written: 2026-09-15T08:36:36Z |
|
review-doc: PASS @ 473b777 content:edd4d0e9a162 — merge-ready review-doc at 473b777 — PASS. Re-review round after a rebase; the doc-class slice is Hygiene checklist:
Criterion 4's doc half (ADR Consequences amended to the ruled shape) is met by this slice; the acceptance-criteria rows are graded in review-code. Verdict-written: 2026-09-15T08:36:46Z |
473b777 to
b0f86ee
Compare
|
governance: PASS @ b0f86ee content:048252e7ff39 — no contradiction, no weakening Governance verdict at b0f86ee — PASS. Third round: the prior PASS was bound to 473b777 and the head moved by a second rebase (base now db67f47); the 12-file diff was re-read whole at this head. Derivation. Questions the diff decides.
Corpus half. The amendment to 0372 narrows its own rule (five declarable effects → one) by the same argument the original decision made, keeps the 2026-09-09 decision paragraph verbatim, and marks the amendment with a date and the two issue-comment rulings. Those rulings are named here as evidence the record cites, not as this verdict's ground. Gate half. Who must approve is a separate question CODEOWNERS answers. |
|
review-code: PASS @ b0f86ee content:048252e7ff39 — merge-ready review-code at b0f86ee — PASS. Third round: the prior PASS was bound to 473b777 and the head moved by a second rebase (base now db67f47); the 12-file diff was re-read whole at this head. CI at head: Acceptance criteria (#8898,
Standing checks:
Deviations ( Fan-out: no in-scope finding to append; nothing out of scope to report. Verdict-written: 2026-09-15T08:58:26Z |
|
review-doc: PASS @ b0f86ee content:048252e7ff39 — merge-ready review-doc at b0f86ee — PASS. Third round after a second rebase (base now db67f47); the doc-class slice is Acceptance criterion 4 (doc half): Hygiene checklist:
Nothing to append; nothing to report. Verdict-written: 2026-09-15T08:58:47Z |
The founder ruled #8898 and #8858 together on 2026-09-10 PT: a Tuval command may only
send, and abare port name in one lands on the declaring program's own process. This implements both.
What was wrong. ADR 0372 refused
emitin acommandscell because a spell call is not aprocess step — the
Scopeit runs under names a workspace and a client, and theprocessit maycarry is the caller's. That argument never stopped at
emit.spawnandaskreadProcessSelffor a
selfthe call does not have andKerneldoes not carry (#8858);replyspends acorrelation the call was never handed;
stopends a process it holds no claim on. Meanwhile theroute ADR 0372's own Consequences prescribed — "a command
sends into the program's own in-port andlets the cell emit" — had no compilation path, so the worked
pr-reviewexample shipped a commandthat was a no-op with no window and a misdirected send with one (#8898).
What changed.
CommandEffectisSendEffect<SendTarget>—send, and nothing else.commands.unit.test.tsholds one
@ts-expect-errorcase per refused effect (emit,spawn,ask,reply,stop), sodeleting the narrowing turns the test red. The refusal is a type at the author's
run, not aruntime check.
SendEffecttakes its target as a parameter, defaulting to the addressedPortAddress. That iswhat keeps the bare form out of an
updatecell:send("pr", pr)is aSendEffect<"pr">, which acell's
ReadonlyArray<ProgramEffect>refuses at the line that wrote it.apps/tuval/src/authoring/own-process.tsresolves a bare name, by the ruled rule: exactly one liveprocess of the declaring program, that one; several with the caller's own
Scope.processamongthem, the caller's; anything else, a typed refusal (
NoLiveProcess,AmbiguousProcess) naming theprogram and the ids, which the executor surfaces as a spell reply rather than swallowing. The read
is
ProcessTable— the only service that answers "which processes of this program are alive" — andKernelalready names it, so the composition root owes nothing new.COMMAND_HANDLERSis{send: sendHandler}andCommandEffectServicesisSpawnedProcesses. Bothservices
Kerneldoes not name,ProcessPortsandProcessSelf, are now unreachable from acompiled spell rather than merely unused by one. That is A compiled command's spawn/ask needs ProcessSelf, which the Tuval Kernel union does not name #8858's whole contract.
send({process, port}, payload)form is untouched, and a test pins it.apps/tuval/src/authoring/example/pr-review.tsdeclaresrun: (pr) => send("pr", pr)— the shapedefineProgram: the Tuval program authoring API #8716 R16.1 and the worked pr-review example: a 30-line program in .tuval/tuval.config.ts that spawns a reviewer passed as an arg #8734's criterion 7 always specified — and is two lines shorter for it.
apps/tuval/src/authoring/own-process.unit.test.tsproves the reach on a real kernel: the payloadcrosses the in-port's own queue and pump and the process's state moves, which a stubbed service
cannot show. It also pins that a second live process is not written to.
.decisions/0372-a-commands-cell-cannot-emit.mdcarries a dated amendment in its lead and arewritten Consequences section: the two bullets that named A compiled command's spawn/ask needs ProcessSelf, which the Tuval Kernel union does not name #8858 and A Tuval command cannot address its declaring program's own process, so send("pr", pr) has no implementation #8898 as open holes are
closed, and the recorded route is now "send to your own program", offered rather than promised.
Gates, from
apps/tuval, re-run on the second rebase ontomain(over #8959):pnpm typecheck0errors;
npx biome check src/authoring src/commands src/ai-agent ../../.decisions199 files, 0errors (2 pre-existing warnings, both on
main);pnpm vitest run --project unit321 files / 3347tests passed;
pnpm vitest run --project integration18 passed, 1 skipped / 85 passed, 1 skipped.Boot proof:
pnpm devwithfeatures.prReviewExampletemporarily on booted 9 programs and 30spells with no
HandlerFailedand noShapeMismatch; the flag is back off in the diff.Ruling comments: #8898 (comment) and
#8858 (comment)
Fixes #8898
Fixes #8858
Base is
main.Deviations
criterion (the worked pr-review example: a 30-line program in .tuval/tuval.config.ts that spawns a reviewer passed as an arg #8734's 7) that prescribed the old route in line. Did: amended ADR 0372 and left
both issues alone. Why:
grepover the tree finds no R16.1 ledger row and no the worked pr-review example: a 30-line program in .tuval/tuval.config.ts that spawns a reviewer passed as an arg #8734 criteriontext; both live only in GitHub issue bodies, and epic defineProgram: the Tuval program authoring API #8716 and ticket the worked pr-review example: a 30-line program in .tuval/tuval.config.ts that spawns a reviewer passed as an arg #8734 are closed, so there
is nothing in-tree left prescribing
send({process, port: "pr"}, …). The ADR's Consequences say soexplicitly. Disposition: noted here, per the lane's instruction not to edit closed issues.
process
SpawnedProcessesholds, which is every process spawned through the spell service or anauthored
spawn, but not a graph-launched one. Why:sendHandlerdelivers throughSpawnedProcesses, whose map holds only what it spawned; the same is already true of theprocess sendspell, filed as process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944. Resolution is deliberately over the true live set anyway —answering "no live process" while one is plainly running would be the dishonest half.
Disposition: process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944 is open and named in ADR 0372's Consequences.
stoprefused too — Said: criterion 1 enumeratesspawn,ask,reply(andemit).Did: also made
stopundeclarable. Why: the criterion's own headline is "a compiledcommand can only
send", and the ruling's question was "a command can only send". Leavingstopdeclarable would have made
CommandEffecta two-member union with no ruled reason for the second.Disposition: stated in the ADR amendment and held by a
@ts-expect-errorcase.