Skip to content

feat(tuval)!: a command may only send, and a bare port name lands on its own program - #9221

Merged
cansirin merged 1 commit into
mainfrom
can/8898-command-sends-to-own-program
Sep 15, 2026
Merged

cansirin merged 1 commit into
mainfrom
can/8898-command-sends-to-own-program

Conversation

@cansirin

@cansirin cansirin commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

The founder ruled #8898 and #8858 together on 2026-09-10 PT: a Tuval command may only send, and a
bare port name in one lands on the declaring program's own process. This implements both.

What was wrong. ADR 0372 refused emit in a commands cell because a spell call is not a
process step — the Scope it runs under names a workspace and a client, and the process it may
carry is the caller's. That argument never stopped at emit. spawn and ask read ProcessSelf
for a self the call does not have and Kernel does not carry (#8858); reply spends a
correlation the call was never handed; stop ends a process it holds no claim on. Meanwhile the
route ADR 0372's own Consequences prescribed — "a command sends into the program's own in-port and
lets the cell emit" — had no compilation path, so the worked pr-review example shipped a command
that was a no-op with no window and a misdirected send with one (#8898).

What changed.

  • CommandEffect is SendEffect<SendTarget> — send, and nothing else. commands.unit.test.ts
    holds one @ts-expect-error case per refused effect (emit, spawn, ask, reply, stop), so
    deleting the narrowing turns the test red. The refusal is a type at the author's run, not a
    runtime check.
  • SendEffect takes its target as a parameter, defaulting to the addressed PortAddress. That is
    what keeps the bare form out of an update cell: send("pr", pr) is a SendEffect<"pr">, which a
    cell's ReadonlyArray<ProgramEffect> refuses at the line that wrote it.
  • apps/tuval/src/authoring/own-process.ts resolves a bare name, by the ruled rule: exactly one live
    process of the declaring program, that one; several with the caller's own Scope.process among
    them, the caller's; anything else, a typed refusal (NoLiveProcess, AmbiguousProcess) naming the
    program 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" — and
    Kernel already names it, so the composition root owes nothing new.
  • COMMAND_HANDLERS is {send: sendHandler} and CommandEffectServices is SpawnedProcesses. Both
    services Kernel does not name, ProcessPorts and ProcessSelf, are now unreachable from a
    compiled 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.
  • The explicit send({process, port}, payload) form is untouched, and a test pins it.
  • apps/tuval/src/authoring/example/pr-review.ts declares run: (pr) => send("pr", pr) — the shape
    defineProgram: 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.ts proves the reach on a real kernel: the payload
    crosses 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.md carries a dated amendment in its lead and a
    rewritten 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 onto main (over #8959): pnpm typecheck 0
errors; npx biome check src/authoring src/commands src/ai-agent ../../.decisions 199 files, 0
errors (2 pre-existing warnings, both on main); pnpm vitest run --project unit 321 files / 3347
tests passed; pnpm vitest run --project integration 18 passed, 1 skipped / 85 passed, 1 skipped.
Boot proof: pnpm dev with features.prReviewExample temporarily on booted 9 programs and 30
spells with no HandlerFailed and no ShapeMismatch; the flag is back off in the diff.

Ruling comments: #8898 (comment) and
#8858 (comment)

Fixes #8898
Fixes #8858

Base is main.

Deviations

@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Preview deployed

  • web — Stage pr-9221 torn down.

@cansirin

Copy link
Copy Markdown
Contributor Author

governance: PASS @ 6fd0902 content:093d43fd9d8f — no contradiction, no weakening

Governance verdict at 6fd0902 — PASS.

Derivation. governance scope 9221: required over one root, .decisions/ (1 file), self false, so the self fence did not apply and head rules were judged by. Record 0372 is modified; adr resolve reads it live accepted.

Questions the change decides.

  1. May a commands cell declare spawn, ask, reply or stop? — No; only send (CommandEffect = SendEffect<SendTarget>).
  2. What does a bare port name in a command's send mean? — An in-port of the declaring program's own process, resolved at the call by a three-case rule (one live → it; several with the caller among them → the caller's; else typed refusal).
  3. What does a compiled spell require of the composition root? — SpawnedProcesses plus ProcessTable; ProcessSelf and ProcessPorts are unreachable from it and Kernel must not be widened to name them.

Corpus half. sweep 9221 --record 0372 → shortlist; top hit 0348 (spell registry, live accepted). Read by hand against 0348: R1.2 (spells declared at registration, never invented at runtime) — unchanged, the spell is still declared on the row; the resolution happens at execution, not registration. R2.2 (kernel decides scope, page never names a process) — unchanged and reinforced: the bare form names no process anywhere, the kernel resolves it. 0348's "a spell that targets another process takes that id as a parameter" — the bare form targets the declaring program's own process, and the explicit send({process, port}, …) form for another process is kept and pinned by test; the tension is named, not a contradiction, and the amendment to 0372 in this same diff is the record that decides it. No other live record speaks to commands cells, CommandEffect or Scope.process (corpus grep). The rest of the shortlist (0346, 0313, 0250, 0388, 0043, 0096, 0042) is lexical overlap with no shared question.

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 accepted, which matches a body that speaks in the present tense about a rule now in force. The record ends narrower than it started (one effect instead of five) — a tightening, not a softening.

Gate half. guards 9221 → no-anchors-in-reach 0; 10 files compared block-by-block. No gate invariant is in this diff's reach. The one guard-shaped thing in the diff — the @ts-expect-error cases in commands.unit.test.ts — grows from one refused effect to five; nothing is removed.

Evidence cited from issues (#8898 / #8858 ruling comments) is named as context only; the verdict rests on the record text and the diff.

@cansirin

Copy link
Copy Markdown
Contributor Author

review-code: PASS @ 6fd0902 content:093d43fd9d8f — merge-ready

review-code at 6fd0902 — PASS. CI at head: settle settled, green (45 runs, 39 success, 6 skipped; 28 authored workflows ran).

Acceptance criteria (#8898, fixes):

  • [PASS] A compiled command can only send; spawn, ask and reply are undeclarable — apps/tuval/src/authoring/commands.ts CommandEffect = SendEffect<SendTarget>; commands.unit.test.ts holds one @ts-expect-error per refused effect (emit, spawn, ask, reply, stop) plus expectTypeOf<CommandEffect["type"]>().toEqualTypeOf<"send">(); define-program.ts COMMAND_HANDLERS = {send: sendHandler}, CommandEffectServices = Extract<EffectServices, SpawnedProcesses>. Typecheck green at head.
  • [PASS] Bare port name resolves by the three-case rule — apps/tuval/src/authoring/own-process.ts resolveOwnProcess: filters ProcessTable.list by programId; one → it; several with scope.process among them → caller's; else NoLiveProcess / AmbiguousProcess naming program, port and ids. Pinned at handler level (commands.unit.test.ts, 4 cases + explicit-form case) and on a real kernel (own-process.unit.test.ts, 4 it.live cases, including that the second live process is not written to). Refusal surfaces as a typed failure of execute, which commands/executor.ts turns into a spell.reply with ok: false — not swallowed.
  • [PASS] run: (pr) => send("pr", pr) compiles and reaches the process — apps/tuval/src/authoring/example/pr-review.ts declares exactly that; pr-review.unit.test.ts "reaches the process" drives the compiled spell's execute through a live-table stub and asserts [[live, "pr", 8690]].
  • [PASS] ADR 0372 Consequences amended to the ruled shape; ledger row / ticket criterion in line — Consequences rewritten (see review-doc). Epic defineProgram: 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 already read send("pr", pr) (verified on the issue bodies) so they are in line as they stand; both closed, disclosed under Deviations.

#8858 (Fixes in body, not the bound issue): its four criteria — ruling recorded, dated amendment to 0372, final Consequences bullet re-pointed, record states what Kernel must not name — are each met by the ADR amendment in this diff. Noted for the record; the bound contract is #8898.

Fan-out.

  • Silent failure: none. Resolution is a typed Effect failure; interpret addresses each send immediately before running it so a refusal stops the list where written.
  • Type design: SendEffect<To extends SendTarget = PortAddress> keeps the bare form out of update cells by default parameter; send is generic so send("pr", pr) is SendEffect<"pr">. Sound. One nit, non-blocking: commands.ts address casts effect.to as string inside the Effect.map closure because the typeof narrowing does not survive into it — a const to = effect.to before the ternary would drop the cast.
  • Test gap: none found. Staleness: resolution reads the live table on every call, nothing cached.
  • Test honesty (class-6): two assertions replaced — pr-review.unit.test.ts:90 (old {process, port} send → send("pr", 8690) plus a new reach test) and test-program.unit.test.ts:220 (the ask moved from the now-undeclarable command into a recheck update cell, and both ask cases re-pointed at it, so testProgram's ask path stays covered). Both follow from criterion 1, not from convenience.
  • Release containment: authoring-surface change, no user-facing default; the pr-review example stays behind features.prReviewExample.

Deviations table:

Entry Substance matched Disposition
Criterion 4 half in-tree issues closed and already in line accepted
Bare send to a graph-launched process refused UnknownProcess pre-existing #8944, named in ADR accepted
stop refused too criterion headline is "only send"; held by @ts-expect-error accepted

deviation-disclosure: PASS — nothing undisclosed this gate could see.

Verdict-written: 2026-09-15T07:53:46Z

@cansirin

Copy link
Copy Markdown
Contributor Author

review-doc: PASS @ 6fd0902 content:093d43fd9d8f — merge-ready

review-doc at 6fd0902 — PASS. Slice: .decisions/0372-a-commands-cell-cannot-emit.md. CI at head green (link and ADR-integrity gates included).

  • Right surface: a ruling and its consequences, in the decision corpus. Correct home.
  • Diátaxis: explanation — the signal is that every added paragraph argues why a rule holds (spawn has no honest self, reply spends a correlation it was never handed), not how to do something. The Consequences bullets name code shapes ({send: sendHandler}, Extract<...>) as this corpus's ADRs routinely do to locate the consequence; no leak into how-to or reference.
  • Supersession explicit: dated amendment note in the lead pointing at Consequences, a dated blockquote heading the rewritten section, the two "open hole" bullets (A Tuval command cannot address its declaring program's own process, so send("pr", pr) has no implementation #8898, A compiled command's spawn/ask needs ProcessSelf, which the Tuval Kernel union does not name #8858) replaced rather than left beside the new ones, and the remaining gap named as process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944. The original "What this decides" paragraph is kept verbatim and marked as no longer the live list.
  • Status sanity: status: accepted unchanged; body speaks in the present tense about a rule in force. Title still true (a commands cell still cannot emit).
  • Claims trace: "Kernel names ProcessTable" — apps/tuval/src/boot.ts Kernel union lists ProcessTable; "Kernel does not name ProcessSelf/ProcessPorts" — neither appears in that union. Both verified in source.
  • Portability guard: not owed (no claude-plugins/fabrika/ or packages/fabrika-cli/src/ file in the diff).
  • Prose craft (writing-for-agents): front-loaded bullets, one claim per bullet, each ending on a locatable file. One nit, non-blocking: the lead amendment note and the Consequences blockquote state the same two sentences twice; the lead is the pointer and could be one line.

Conversation-authored: no. Bound to #8898 criterion 4 (in-tree half), which this slice discharges.

Verdict-written: 2026-09-15T07:53:53Z

@cansirin

cansirin commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor Author

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 ui class was raised by the tuval-chat prefix (apps/tuval/src/), not by any rendered change. The twelve changed files are one ADR (.decisions/0372-a-commands-cell-cannot-emit.md) and eleven .ts files, all under apps/tuval/src/authoring/:

  • commands.ts, effect.ts, define-program.ts — CommandEffect narrowed to SendEffect<SendTarget>, SendEffect takes its target as a type parameter, COMMAND_HANDLERS shrinks to {send}. Type-level and handler-table changes.
  • own-process.ts — new bare-port-name resolver over ProcessTable with typed NoLiveProcess / AmbiguousProcess refusals. Kernel-side logic.
  • example/pr-review.ts, test-program.ts — authored example programs whose run now uses the bare send("pr", pr) form. Program definitions, no view.
  • *.unit.test.ts, reload/authoring-reload.integration.test.ts — tests.

No .tsx, no .css, no JSX, no template string that reaches the DOM. The only <...> in the diff are TypeScript generics. Nothing the tuval chat surface paints is touched, so there is no capture to take and no law row to judge against. LAW-SOURCE: manifest-prose (registry untyped, exit 13) — noted for the record, not applied.

Text judgment of these files is review's lane; review-code and review-doc already stand PASS at this head.

@cansirin

Copy link
Copy Markdown
Contributor Author

ship: BASE-CONFLICTED — PR #9221 @ 6fd0902 → repair

ship enqueue refused at exit 21: mergeable_state: dirty — main moved under the branch (head is 2 commits behind) and the merge conflicts. Nothing was armed; nothing about the artifact was judged, so no repair round is charged.

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).

@cansirin
cansirin force-pushed the can/8898-command-sends-to-own-program branch from 6fd0902 to 473b777 Compare September 15, 2026 08:06
@cansirin

Copy link
Copy Markdown
Contributor Author

governance: PASS @ 473b777 content:edd4d0e9a162 — no contradiction, no weakening

Governance verdict at 473b777 — PASS.

Scope. governance required, derived over .decisions/ (1 file): ADR 0372 modified in place. self false; the self fence did not apply. Re-review round: the prior governance PASS was bound to 6fd0902 and is stale at this head.

Questions the diff decides.

  1. May a commands cell declare any effect other than send? — No (was: five of six).
  2. What does a bare port name in a command's send mean? — An in-port of the declaring program's own process, resolved at the call by the three-case rule (one live → it; several with caller among them → caller; else typed refusal).
  3. Does resolving that widen what a compiled spell requires of Kernel? — By ProcessTable, which boot.ts already names (Kernel union, line 61 at base).

Corpus half. sweep --record 0372 → shortlist; top hit 0348 (spell registry). Read by hand: 0348 R2.2 ("the page never names a process") and its Consequences ("a spell that targets another process takes that id as a parameter") stay intact — the bare form has the page name nothing, and the explicit {process, port} form is unchanged. 0372's own Decision §1/§3 text is kept as written and marked superseded-in-part by a dated > Amended blockquote pointing at Consequences, which is the in-place amendment shape 0126 and 0129 already use. adr resolve 0372 → live accepted. The founder rulings cited (#8898, #8858 comments) are named as evidence the record points at, not the ground of this verdict.

Gate half. guards → no-anchors-in-reach, 10 files compared block-by-block. No gate invariant is in this diff's reach. The type change is a narrowing (Exclude<ProgramEffect, EmitEffect> → SendEffect<SendTarget>; COMMAND_HANDLERS five keys → one) held by four new @ts-expect-error cases; nothing is softened.

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>
@cansirin

Copy link
Copy Markdown
Contributor Author

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: settle settled, green (45 runs, 39 success, 6 skipped; 28 authored workflows ran) after the governance floor re-fired against this round's governance verdict.

Acceptance criteria (#8898, fixes):

  • [PASS] A compiled command can only send; spawn, ask, reply undeclarable — apps/tuval/src/authoring/commands.ts CommandEffect = SendEffect<SendTarget>; define-program.ts COMMAND_HANDLERS = {send: sendHandler}, CommandEffectServices = Extract<EffectServices, SpawnedProcesses>; commands.unit.test.ts holds one @ts-expect-error per refused effect (emit, spawn, ask, reply, stop) and expectTypeOf<CommandEffect["type"]>().toEqualTypeOf<"send">(). Typecheck green at head.
  • [PASS] Bare port name resolves by the three-case rule — apps/tuval/src/authoring/own-process.ts resolveOwnProcess: filters ProcessTable.list by programId; one → it; several with scope.process among them → the caller's; else NoLiveProcess / AmbiguousProcess naming program, port and ids. Pinned at handler level (commands.unit.test.ts, four cases plus the explicit-form case) and on a real kernel (own-process.unit.test.ts, four it.live cases including that the second live process is not written to). ProcessTable is in the Kernel union (boot.ts), so the widening is by a service already provided.
  • [PASS] run: (pr) => send("pr", pr) compiles and reaches the process — apps/tuval/src/authoring/example/pr-review.ts declares exactly that; pr-review.unit.test.ts "reaches the process" drives the compiled spell's execute through a live-table stub and asserts [[live, "pr", 8690]].
  • [PASS] ADR 0372 Consequences amended to the ruled shape; ledger row / ticket criterion in line — Consequences rewritten (graded in review-doc). defineProgram: 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 already read send("pr", pr) (read off the issue bodies this round), so they are in line as they stand; git grep at head finds no in-tree text prescribing the old send({process, port: "pr"}, …) route. Disclosed under Deviations.

Fan-out.

  • Silent failure: none. A refusal is a typed Effect failure of execute; interpret addresses each send immediately before running it, so a refusal stops the list where it was written.
  • Type design: SendEffect<To extends SendTarget = PortAddress> keeps the bare form out of update cells by the default parameter; send is generic so send("pr", pr) is SendEffect<"pr">; AnyEffect exists only for testProgram. Sound. Nit, non-blocking: commands.ts address casts effect.to as string inside the closure because the typeof narrowing does not survive into it.
  • Test gap: none traced to the issue goal. Staleness: resolution reads the live table on every call, nothing cached.
  • Test honesty (class-6): two assertions replaced — pr-review.unit.test.ts:90 (old addressed send → send("pr", 8690), plus a new reach test) and test-program.unit.test.ts:220 (the ask moved from the now-undeclarable command into a recheck update cell and both ask cases re-pointed at it, so testProgram's ask path stays covered). Both follow from criterion 1.
  • Release containment: authoring-surface change, no user-facing default; the pr-review example stays behind features.prReviewExample.
  • Comment discipline: docblocks state constraints the types cannot (which service each handler reads, why the compile site takes the program id). No narration findings.

Deviations table:

Entry Substance matched Disposition
Criterion 4 half in-tree issues closed and already in line; nothing in-tree prescribes the old route accepted
Bare send to a graph-launched process refused UnknownProcess pre-existing #8944, named in ADR accepted
stop refused too criterion headline is "only send"; held by @ts-expect-error accepted

deviation-disclosure: PASS — nothing undisclosed this gate could see.

Verdict-written: 2026-09-15T08:36:36Z

@cansirin

Copy link
Copy Markdown
Contributor Author

review-doc: PASS @ 473b777 content:edd4d0e9a162 — merge-ready

review-doc at 473b777 — PASS. Re-review round after a rebase; the doc-class slice is .decisions/0372-a-commands-cell-cannot-emit.md, re-read whole at this head. CI at head green (link check, machine-local paths and ADR-number lock are CI's; read, not re-derived).

Hygiene checklist:

  • Right surface: a why-record amended in the decision corpus; the code-shape facts it names are pointers into source, not a second home. PASS.
  • Diátaxis: single-mode: explanation — the slice exists to make the reader understand why the ruling closes each of the four other effects, and the signal is the argument itself ("the same argument that refused emit refuses the other four"). File paths and type names appear as pointers, not as a look-up table. No intruding mode.
  • Supersession explicit: in-place amendment by a dated > Amended blockquote in Consequences plus a pointer under "What this decides", the shape 0126 and 0129 already use; the original Decision text is kept as written and named as the 2026-09-09 ruling. Both rulings are linked. PASS.
  • Status sanity: status: accepted unchanged; the body speaks in the present tense about what is live and marks the old list as no longer the live rule. Consistent with 0273 (in-place amended, still accepted). PASS.
  • Claims trace: Kernel naming ProcessTable and not ProcessSelf/ProcessPorts is verified against apps/tuval/src/boot.ts at head; the UnknownProcess reach gap cites process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944; the resolution rule cites the ruling comments. PASS.
  • Portability: not under claude-plugins/fabrika/ or packages/fabrika-cli/src/; N/A.
  • Prose craft (writing-for-agents): one meaning stated twice — the header note and the Consequences blockquote — but the header note is a pointer to the second, not a duplicate rule. Sentences run long in the house ADR register; each carries a constraint, none is a no-op. PASS, nit only.

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

@cansirin
cansirin force-pushed the can/8898-command-sends-to-own-program branch from 473b777 to b0f86ee Compare September 15, 2026 08:38
@cansirin

Copy link
Copy Markdown
Contributor Author

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. governance scope 9221: required over one root, .decisions/ (1 file). self false — the self fence did not apply; head rules were judged by. Record 0372 is modified; adr resolve 0372 reads it live accepted at the base.

Questions the diff decides.

  1. May a commands cell declare any effect other than send? — No. CommandEffect = SendEffect<SendTarget>; COMMAND_HANDLERS = {send: sendHandler}.
  2. What does a bare port name in a command's send mean? — An in-port of the declaring program's own process, resolved at the call by the three-case rule (one live → that one; several with the caller among them → the caller's; else a typed refusal NoLiveProcess / AmbiguousProcess).
  3. What does a compiled spell now require? — SpawnedProcesses plus ProcessTable. Verified at head: Kernel in apps/tuval/src/boot.ts names both, so the composition root's obligation (commands/executor.ts's erasure note) grows by nothing it did not already provide.
  4. Can an update cell write the bare form? — No. Answer<S> holds ReadonlyArray<ProgramEffect>, and ProgramEffect carries SendEffect at its default PortAddress parameter, so send("pr", pr) (a SendEffect<"pr">) is refused at the line that writes it.

Corpus half. sweep 9221 --record 0372: shortlist, 318 uncited live-accepted records ranked. Top hit 0348 (Tuval command framework). Read by hand: R2.2 says a spell runs with a kernel-resolved Scope and "the page never names a process". The diff keeps that — the bare form names a port, and which process it lands on is resolved kernel-side against ProcessTable; the wire is untouched. R1.1's spell shape and R2.4's process send spell are unchanged. 0346 (Sub failure policy) is adjacent by vocabulary only; nothing in it speaks to command effects. No other live record governs what a commands cell may declare (git grep over .decisions/ at base finds only 0372). No contradiction found.

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. guards 9221: no-anchors-in-reach, 0 anchored invariants; 10 files compared block-by-block against the base. Hand read of the reach: no shipper matcher, no CI gate, no hook, no .fabrika.jsonc touched. The one guard-shaped change is in the test surface, and it strengthens: commands.unit.test.ts goes from one @ts-expect-error (emit) to five (emit, spawn, ask, reply, stop) plus expectTypeOf<CommandEffect["type"]>().toEqualTypeOf<"send">(), so deleting the narrowing still turns the test red. No gate invariant is in this diff's reach; nothing removed or softened.

Who must approve is a separate question CODEOWNERS answers.

@cansirin

Copy link
Copy Markdown
Contributor Author

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: settle settled, green (45 runs, 39 success, 6 skipped; 28 authored workflows ran) after the governance floor re-fired against this round's governance verdict.

Acceptance criteria (#8898, fixes):

  • [PASS] A compiled command can only send; spawn, ask and reply are undeclarable in a commands cell — apps/tuval/src/authoring/commands.ts CommandEffect = SendEffect<SendTarget>; define-program.ts COMMAND_HANDLERS = {send: sendHandler} typed over CommandEffectServices = Extract<EffectServices, SpawnedProcesses>; commands.unit.test.ts holds one @ts-expect-error per refused effect (emit, spawn, ask, reply, stop) plus expectTypeOf<CommandEffect["type"]>().toEqualTypeOf<"send">(). stop is refused as well, disclosed as deviation 3.
  • [PASS] A bare port name resolves to a process of the declaring program by the three-case rule — apps/tuval/src/authoring/own-process.ts resolveOwnProcess: one live → that one; several with scope.process among them → the caller's; otherwise NoLiveProcess / AmbiguousProcess, both Schema.TaggedError naming the program, the port and (for ambiguity) the ids. commands.ts address resolves each bare send immediately before its handler runs, so a refusal stops the list where it was written. Covered twice: commands.unit.test.ts (stubbed ProcessTable, four cases plus the explicit form untouched) and own-process.unit.test.ts (real kernel via SpawnedProcesses.layer + Processes.layer + Registry.layer, payload read back from the process state, and the "several, caller among them" case asserts the other process was not written to). Refusal surfaces as a SpellReplyError: commands/executor.ts Effect.catch → refused(call, failureOf(...)), read at head.
  • [PASS] run: (pr) => send("pr", pr) in apps/tuval/src/authoring/example/pr-review.ts compiles and reaches the process — the declaration is at line 59 (commands: {review: {args: Schema.Number, run: (pr: number) => send("pr", pr)}}); pr-review.unit.test.ts executes the compiled spell off the registry row and asserts sent == [[live, "pr", 8690]]. Typecheck is CI's, green.
  • [PASS, code half] Criterion 4's code consequences: nothing in-tree prescribes the old send({process, port: "pr"}, …) route — sendPr and its Scope import are deleted from the example, and test-program.unit.test.ts moves its ask fixture into a recheck update cell. The ADR half is review-doc's. The epic ledger row and ticket criterion live only in closed issues (deviation 1), stated as a fact, not a finding.

Standing checks:

  • Test honesty: two removed assertions (pr-review.unit.test.ts:114, test-program.unit.test.ts:220) are each the direct consequence of a criterion — the first asserted the route criterion 3 replaces and is succeeded by a stronger case that drives the compiled spell to a process; the second asserted an ask from a command, which criterion 1 makes undeclarable, and the ask behaviour keeps its coverage through recheck. Neither is a test rewritten to assert the implementation against itself.
  • Type design: SendEffect<To extends SendTarget = PortAddress> keeps the bare form out of update cells — Answer<S> is readonly [S, ReadonlyArray<ProgramEffect>] and ProgramEffect carries SendEffect at its default, so send("pr", pr) (a SendEffect<"pr">) is refused at the line that writes it. AnyEffect is scoped to test-program.ts's run record. Invalid states unrepresentable, as the repo's law asks.
  • Silent failure: none — every non-resolution is a typed failure on the spell's E channel; nothing swallows or no-ops.
  • Release containment: authoring-layer type narrowing, compile-time; the ! in the title is honest and the one in-tree author (pr-review) is migrated in the same diff. No runtime default-on surface.
  • Comment discipline: comments state the constraint the types cannot (why each effect is refused, why ProcessTable not SpawnedProcesses, why the cast in bindArgs). No narration or separators added.
  • Staleness traps: none — resolution reads the live ProcessTable per call, caches nothing.

Deviations (review deviations: found, 3 entries): all three match findings by substance — criterion 4's half in closed issues; the graph-launched reach gap through SpawnedProcesses (#8944, pre-existing, also named in the ADR); stop refused beyond the enumerated three. The six @ts-expect-error flags are the criterion's own proof cases, two of them prose in the ADR. Nothing undisclosed that this gate could see.

Fan-out: no in-scope finding to append; nothing out of scope to report.

Verdict-written: 2026-09-15T08:58:26Z

@cansirin

Copy link
Copy Markdown
Contributor Author

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 .decisions/0372-a-commands-cell-cannot-emit.md, re-read whole at this head. CI at head green (link check, machine-local paths and ADR-number lock are CI's; read, not re-derived).

Acceptance criterion 4 (doc half): 0372's Consequences section is amended to the ruled shape — a dated blockquote names the two rulings, the six original bullets that carried the open holes are replaced by six that state the one-effect rule, the bare-port resolution, the ProcessTable read, the worked example and the remaining SpawnedProcesses reach gap (#8944). The Decision paragraph gains a dated amendment note and keeps the 2026-09-09 text verbatim. [PASS]

Hygiene checklist:

  • Right surface: a why-record amended in the decision corpus; the code shapes it names ({send: sendHandler}, Extract<EffectServices, SpawnedProcesses>) are pointers into source with a reason beside each, not a second home for the code. PASS.
  • Diátaxis (diataxis skill, fired on the slice): single-mode: explanation. Signal: no ordered sequence, and every added paragraph argues why a rule holds (spawn has no honest self, reply spends a correlation it was never handed, stop ends a process the call has no claim on). The Consequences bullets describe resulting shape but do not harden into steps or a parameter list. No mix. PASS.
  • Supersession explicit: the record amends itself in place, dated, and states which paragraph is kept and which list is no longer live; it retires no other record and none answers the same question (governance's sweep at this head confirms). PASS.
  • Status sanity: frontmatter accepted, body speaks in the present tense about a rule now in force. PASS.
  • Claims trace: every runtime claim points at a file (boot.ts for Kernel naming ProcessTable/not ProcessSelf, own-process.ts for the rule, commands.unit.test.ts for the @ts-expect-error cases, own-process.unit.test.ts for the live-kernel reach) and each was verified at head in review-code. The process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944 gap is stated as pre-existing and is. PASS.
  • Portability: N/A — no file under claude-plugins/fabrika/ or packages/fabrika-cli/src/ in the diff.
  • Prose craft (writing-for-agents, read inline): plain words, one claim per sentence, no hedging, no reader-must-reread constructions; the longest bullet (CommandEffect is SendEffect<SendTarget>) runs six lines but each clause carries a distinct effect's reason and cutting one would drop that reason. PASS.

Nothing to append; nothing to report.

Verdict-written: 2026-09-15T08:58:47Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant