Skip to content

feat(runner): dispatch a run.command extension command headlessly - #192

Merged
edgehero merged 1 commit into
mainfrom
feat/pi-command-runner
Aug 13, 2026
Merged

feat(runner): dispatch a run.command extension command headlessly#192
edgehero merged 1 commit into
mainfrom
feat/pi-command-runner

Conversation

@edgehero

Copy link
Copy Markdown
Owner

The runner half of #189 Gap 2 (the run.command trigger field follows as the producer half; Gap 1 merged as #190/#191). This ships the dispatch protocol the pinned artifact already supports, consumer-before-producer so the rollout is safe in both directions.

What

  • Env-authoritative prompt: a command job's prompt is rebuilt from PI_COMMAND as /<command>, never read from prompt.md. pi's dispatch grammar fires only when the entire text starts with /, parses the name to the first space, and hands everything after to the handler verbatim, so a "first line of a larger prompt" cannot exist; one in-container authority also means a worker bug cannot make the classification misread a flow job. prompt.md stays the byte-identical human record.
  • Pre-spend verification: session.extensionRunner.getCommand() before prompting; unregistered → command-unregistered, exit 2. An unregistered /name is not an error to pi: it falls through template expansion into a paid model call (or a same-named staged prompt template), both silent.
  • Outcome protocol (three reasons under existing codes, never a new worker outcome): clean headless return → command-completed exit 0 (previously the retryable no-terminal-message shape, re-billing a success); handler throw → command-error exit 1, observed via the public extensionRunner.onError channel (pi swallows the throw; that channel is the pin's only trace), retryable by explicit choice with the accepted cost recorded on the new DES entry; a handler that drove the model keeps its terminal's ordinary verdict; budget aborts keep first position.
  • Image capability: dev.pi-dispatch.capabilities gains commands, with a verify-image.sh case asserting the claim against the baked runner source. The worker preflight gate lands with the producer half.
  • Pinned-contract test: a keyless REAL session drives all four dispatch facts (headless resolve, verbatim args incl. newlines, swallowed throw on the error channel only, exact getCommand discrimination) — dispatch happens before any model/auth validation, which the keylessness itself proves.

Specs

INT-RUNNER-EXIT-CODE-PROTOCOL AMENDED, INT-CONTAINER-JOB-INPUTS AMENDED (PI_COMMAND), NEW DES-COMMAND-ENTRY-POINT (env-authoritative rationale, retryable-throw decision, fire-and-forget residual, rejected alternatives). UNCHANGED, checked: INT-TRIGGERS-FILE-CONTRACT, DES-FLOW-RESOLUTION-TWO-ADVISORY-LAYERS, DES-PER-TRIGGER-JOB-IMAGE, DES-TRIGGER-INSTRUCTION-IN-THE-ENVELOPE.

Suite: 2231 tests, 0 failed, 0 skipped, in the CI posture (loader/worker/receiver gates on, live Valkey).

The runner half of issue #189 Gap 2 (the trigger field itself follows as
the producer half). A trigger will be able to name a registered pi
extension command instead of a flow; this PR ships the dispatch protocol
the pinned artifact already supports.

The prompt for a command job is rebuilt from PI_COMMAND as /<command>,
never read from prompt.md: pi's dispatch grammar fires only when the
ENTIRE text starts with a slash, parses the name to the first space and
hands everything after it to the handler verbatim, so there is no such
thing as a command line at the top of a larger prompt, and one
in-container authority means a worker bug cannot make the classification
misread a flow job. prompt.md stays the byte-identical human record.

Before prompting, the runner verifies the name via
session.extensionRunner.getCommand() and refuses an unregistered one as
command-unregistered (exit 2, pre-spend): an unregistered /name is not an
error to pi, it falls through template expansion into a paid model call,
or into a same-named prompt template if one is staged. A handler throw is
SWALLOWED by pi (prompt() resolves cleanly) and surfaces only on the
public extensionRunner.onError channel; the runner subscribes and
classifies it command-error, exit 1 retryable by explicit choice: pi
hands us a message string, transient-vs-deterministic is undecidable, and
the accepted cost (a deterministic extension bug retries until attempts
run out) is recorded on the new DES entry. A clean headless return is
command-completed, exit 0; before that reason existed the same shape was
the retryable no-terminal-message, and the queue re-billed a success. A
handler that drove the model keeps its terminal's ordinary verdict, and
budget aborts keep first position.

The image's capabilities label gains commands, with a verify-image.sh
case asserting the claim against the baked runner source; the worker-side
preflight gate lands with the producer half. A keyless real-session
contract test pins all four dispatch facts against the pinned artifact.

Specs: INT-RUNNER-EXIT-CODE-PROTOCOL AMENDED (three reasons under
existing codes, never a new outcome), INT-CONTAINER-JOB-INPUTS AMENDED
(PI_COMMAND), NEW DES-COMMAND-ENTRY-POINT. UNCHANGED, checked:
INT-TRIGGERS-FILE-CONTRACT, DES-FLOW-RESOLUTION-TWO-ADVISORY-LAYERS,
DES-PER-TRIGGER-JOB-IMAGE, DES-TRIGGER-INSTRUCTION-IN-THE-ENVELOPE.

Signed-off-by: Rob Boerman <robboerman@live.nl>
@edgehero
edgehero merged commit d6a816a into main Aug 13, 2026
12 checks passed
edgehero added a commit that referenced this pull request Aug 13, 2026
…#189)

The producer half of issue #189 Gap 2 (the runner protocol merged as
#192): a trigger may now name a registered pi extension command instead of
a flow, on all four trigger kinds.

Parse (shared validator, both services fail identically): exactly one of
run.flow or run.command, refused before every flow check so a command-only
entry never sees the wrong error; the value is validated exactly as the
runner validates PI_COMMAND (non-empty, no leading slash, no surrounding
whitespace, no control characters DEL included) so a file that loads never
refuses in-container; run.task, run.instructions and run.resume are
refused beside command, each naming both fields; everything else stays
orthogonal; an unflagged trigger normalizes byte-identically.

Prompt: a command job's prompt is exactly /name args, local and forge
alike, no envelope, no pointer, no trailing newline; the delivery rides
/job/event.json as the handler's data channel. CONST-ISSUE-TEXT-IS-DATA is
preserved and arguably strengthened: payload text reaches a command job
only as a file the handler chooses to parse, never as prompt prose.

Never AI-reachable, stricter than flows and BUILT rather than fallen out
(the OQ-022 inversion, now recorded there): chain requests carrying a
command key refuse outright as chain-command-refused before the charset
check, with no opt-in, and dispatch_run refuses a slash-leading flow with
a readable message on both invocation paths while remaining structurally
incapable of naming a command. Commands may chain OUT through their own
outbox; nothing chains INTO a command.

Receiver: knownFlows collects only flow-carrying rules; all four forge
gates (github, gitlab, forgejo, azure) dispatch command rules with the
comment phrase-plus-flow override channel INERT on them, so a collaborator
can neither retarget nor veto a command by appending words. Worker: cron
and forge job data carry command; both dedup keys use a cmd: prefix so a
command and a flow of one name cannot collide; image-preflight refuses a
command job on an image not declaring the commands capability, pre-spend,
as job-image-commands-unsupported; doctor counts command triggers and says
where they are verifiable. Admin: display and TUI show /name; the graph
carries command for issue #188; writeTriggers inherits the exclusion.

Specs: INT-TRIGGERS-FILE-CONTRACT AMENDED (the command clause on the
run.image no-model-callable-path template), INT-OUTBOX-CONTRACT AMENDED,
DES-COMMAND-ENTRY-POINT AMENDED (producer half), DES-AI-TRIGGER-FLOW-GATE
AMENDED, DES-JOB-OUTBOX-CHAINING AMENDED,
DES-TRIGGER-INSTRUCTION-IN-THE-ENVELOPE AMENDED (the byte-for-byte
objection answered: command prompts are NEW prompts), OQ-022 AMENDED,
REQ-AI-TRIGGERED-RUNS, REQ-CRON-SCHEDULED-JOBS and REQ-TRIGGER-AUTHOR-GATE
AMENDED. UNCHANGED, checked: DES-TRIGGERS-UNIFIED-FILE,
REQ-DEPLOYMENT-BOOTSTRAP, OQ-008, OQ-019 (its (b) stays open), OQ-009.
docs/workflows.md doctrine rewritten (run.flow and run.command are the two
entry points; the nobody-to-type-it premise refuted at the pin), both
READMEs and docs/job-image.md updated, triggers.example.json gains a cron
command entry.

Signed-off-by: Rob Boerman <robboerman@live.nl>
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.

1 participant