feat(runner): dispatch a run.command extension command headlessly - #192
Merged
Conversation
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
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>
This was referenced Aug 13, 2026
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.
The runner half of #189 Gap 2 (the
run.commandtrigger 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
PI_COMMANDas/<command>, never read fromprompt.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.mdstays the byte-identical human record.session.extensionRunner.getCommand()before prompting; unregistered →command-unregistered, exit 2. An unregistered/nameis not an error to pi: it falls through template expansion into a paid model call (or a same-named staged prompt template), both silent.command-completedexit 0 (previously the retryableno-terminal-messageshape, re-billing a success); handler throw →command-errorexit 1, observed via the publicextensionRunner.onErrorchannel (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.dev.pi-dispatch.capabilitiesgainscommands, with averify-image.shcase asserting the claim against the baked runner source. The worker preflight gate lands with the producer half.getCommanddiscrimination) — dispatch happens before any model/auth validation, which the keylessness itself proves.Specs
INT-RUNNER-EXIT-CODE-PROTOCOLAMENDED,INT-CONTAINER-JOB-INPUTSAMENDED (PI_COMMAND), NEWDES-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).