Skip to content

feat(runner): report a run.flow that resolves in no loaded skill tier - #190

Merged
edgehero merged 1 commit into
mainfrom
feat/pi-flow-trace
Aug 13, 2026
Merged

feat(runner): report a run.flow that resolves in no loaded skill tier#190
edgehero merged 1 commit into
mainfrom
feat/pi-flow-trace

Conversation

@edgehero

Copy link
Copy Markdown
Owner

Gap 1, runner half of #189. The doctor half (per-trigger tier resolution lines) follows as its own PR.

What

A trigger's run.flow reached the container only as prompt prose, and pi never matches prose against loaded skill names, so a flow that materialised in no tier (repo, injected, overlay or staged package) ran to a clean exit 0 without the procedure it was written for. This PR makes that miss leave a named, greppable trace:

  • The worker forwards the flow structurally as PI_FLOW (omitted when the job carries none, never an empty string; env and not event.json, because an execution knob is not a fact about the delivery, the run.replicas line).
  • The runner compares it against the skill set the loader actually materialised, immediately after resource load and before any session or spend, and emits one flow_not_loaded line on a miss. getSkills() moves out of the packages-only guard so the check runs for every job.
  • Report, not refusal, deliberately: run.flow is by long doctrine a prompt hint, and a refusal shipped in an image upgrade would fail yesterday's jobs for a value the reviewed file has carried all along. The check sits at the pre-spend moment, so flipping report to refusal later is a one-line change at the same site. Recorded with the rejected alternatives on the new DES-FLOW-RESOLUTION-TWO-ADVISORY-LAYERS.

Acceptance mapping (#189)

  • "a job whose flow resolves in no loaded tier leaves a named greppable trace": the flow_not_loaded line, flow name and a loaded-skill count, never task content.
  • "a silent exit 0 for this case becomes a failing test": compose.test.mjs pins the line's existence, its placement before openSessionManager (pre-spend), and getSkills() outside the packages guard.
  • A new loader contract test pins pi's naming rule at the pin (frontmatter name || parent dir), the premise the comparison rests on; disableModelInvocation skills count as loaded (invocable, so not the silent no-op this line exists to catch).

Specs

INT-CONTAINER-JOB-INPUTS AMENDED (PI_FLOW), REQ-PER-TRIGGER-SKILLS AMENDED (runner acceptance clause), NEW DES-FLOW-RESOLUTION-TWO-ADVISORY-LAYERS. UNCHANGED, checked: INT-RUNNER-EXIT-CODE-PROTOCOL, INT-TRIGGERS-FILE-CONTRACT, INT-WEBHOOK-PAYLOAD-SUBSET, REQ-GLOBAL-PI-OVERLAY, REQ-DEPLOYMENT-BOOTSTRAP, DES-AI-TRIGGER-FLOW-GATE, DES-TRIGGERS-UNIFIED-FILE.

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

…#189)

The trigger's run.flow reached the container only as prompt prose, and pi
never matches prose against loaded skill names, so a flow that materialised
in no tier (repo, injected, overlay or staged package) ran to a clean
exit 0 without the procedure it was written for and reported success for
work it could not have done.

The worker now forwards the flow structurally as PI_FLOW (omitted when the
job carries none, never an empty string; env and not event.json, because an
execution knob is not a fact about the delivery), and the runner compares it
against the skill set the loader actually materialised, immediately after
resource load and before any session or spend, emitting one flow_not_loaded
line on a miss. Report, not refusal, deliberately: run.flow is by long
doctrine a prompt hint, and a refusal shipped in an image upgrade would fail
yesterday's jobs for a value the reviewed file has carried all along. The
check sits at the pre-spend moment, so flipping report to refusal later is a
one-line change at the same site.

getSkills() moves out of the packages-only guard so the check runs for every
job. isFlowLoaded is exact name equality against the LOADED names (pi names
a skill frontmatter name || parent dir at the pin, now pinned by a loader
test), and a disableModelInvocation skill counts as loaded: it is absent
from the catalogue but invocable, so it is not the silent no-op this line
exists to catch.

Specs: INT-CONTAINER-JOB-INPUTS AMENDED (PI_FLOW), REQ-PER-TRIGGER-SKILLS
AMENDED (the runner acceptance clause), NEW
DES-FLOW-RESOLUTION-TWO-ADVISORY-LAYERS (report-not-refuse and the rejected
alternatives: boot-time failure, event.json carriage, a new top-level
outcome). UNCHANGED, checked: INT-RUNNER-EXIT-CODE-PROTOCOL,
INT-TRIGGERS-FILE-CONTRACT, INT-WEBHOOK-PAYLOAD-SUBSET,
REQ-GLOBAL-PI-OVERLAY, REQ-DEPLOYMENT-BOOTSTRAP, DES-AI-TRIGGER-FLOW-GATE,
DES-TRIGGERS-UNIFIED-FILE.

Signed-off-by: Rob Boerman <robboerman@live.nl>
@edgehero
edgehero merged commit 47eb487 into main Aug 13, 2026
12 checks passed
edgehero added a commit that referenced this pull request Aug 13, 2026
The doctor half of issue #189 Gap 1 (the runner half merged as #190): one
line per distinct (flow, folder, skillsDir, packages) question, naming the
skill tier that resolves the trigger's run.flow, probed in the loader's own
precedence order.

The repo tier reuses the flow gate's ls-tree mechanics (100644-blob rule,
hardened git flags) but deliberately NOT readFlowGate itself: the gate's
fail-closed catch turns a broken folder into deny, and deny-implies-exists
would print a confident wrong answer on an advisory line; its no-ref rule
guards against an agent self-authorizing, which a host-side preflight does
not face, so doctor resolves HEAD itself and degrades to unknown. The
staged tier gets the genuinely new reader, readStagedSkills in
worker/src/packages.mjs (never-throws, reachable by admin for issue #188's
topology): it mirrors pi's manifest-vs-convention rule at the 0.80.7 pin,
including that a pi manifest WITHOUT a skills key contributes nothing, and
declares a pattern-carrying manifest not-enumerable rather than guessing,
because patterns can also disable files and a wrong checkmark is the one
direction an advisory line may not err in.

When nothing resolves the line is a warn, never a fail and never a
fixAction (triggers content is the never tier), naming the tiers checked
and the ones not checkable on this host: a forge trigger's repo is not
here, and mid-setup is legal. Staged-package-only resolution is a plain
checkmark naming the package, per the issue's acceptance. A flow failing
the skill charset is its own warn. Zero triggers add zero lines; the two
package-feature no-op pins keep their broad needle by stripping the flow
lines, which name staged packages as a tier.

Specs: REQ-PER-TRIGGER-SKILLS AMENDED (the doctor acceptance clause),
REQ-GLOBAL-PI-OVERLAY AMENDED (overlay + staged tiers join doctor's
obligations), DES-FLOW-RESOLUTION-TWO-ADVISORY-LAYERS AMENDED (the doctor
layer specified in full, readFlowGate rejection recorded). UNCHANGED,
checked: REQ-DEPLOYMENT-BOOTSTRAP (warn-tier, no fixAction),
INT-TRIGGERS-FILE-CONTRACT, INT-CONTAINER-JOB-INPUTS,
INT-PI-PACKAGES-FILE-CONTRACT, DES-AI-TRIGGER-FLOW-GATE, DES-CLI-SURFACE.

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