You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: specs/design.md
+12-2Lines changed: 12 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1293,8 +1293,17 @@ money with no upstream turn limit (`REQ-RUNNER-TURN-BUDGET`).
1293
1293
flow name structurally (`PI_FLOW`, `INT-CONTAINER-JOB-INPUTS`), and after the resource loader builds —
1294
1294
before any session or spend — the runner compares it against the **loaded** skill names and emits one
1295
1295
`flow_not_loaded` line on a miss (`isFlowLoaded`, exact name equality; a `disableModelInvocation` skill
1296
-
counts, it is invocable even though uncatalogued). **Doctor** is the approximate host-side layer, probing
1297
-
the tier directories per trigger (its own DES half lands with the doctor change). The job itself proceeds.
1296
+
counts, it is invocable even though uncatalogued). **Doctor** is the approximate host-side layer: one
1297
+
line per distinct (flow, folder, skillsDir, packages) question, probing the tiers in the loader's own
1298
+
precedence order — repo `.pi/skills` at HEAD (the gate's ls-tree read and 100644-blob rule, but
1299
+
HEAD-resolved by doctor itself and degrading to "unknown" on git failure, because the gate's
1300
+
fail-closed catch would print a confident wrong answer on an advisory line, and its no-ref rule
1301
+
defends against an agent that a host-side preflight does not have), injected `run.skillsDir`, overlay
1302
+
`skills/`, then staged packages via `readStagedSkills` (pi's manifest-vs-convention rule at the pin;
1303
+
glob/override manifests make a package "not enumerable" rather than guessed at, because patterns can
1304
+
also DISABLE files and a wrong ✓ is the one direction an advisory may not err in). ⚠ never ✗, no fix
1305
+
action, zero triggers add zero lines; a staged-package-only ✓ is deliberate (legal steady state). The
1306
+
job itself proceeds.
1298
1307
-**Why**: the flow reaches the model only as prompt prose (`Use the "X" skill`), and pi never matches
1299
1308
prose against loaded skill names — so a flow that materialised in no tier ran to a clean exit 0 without
1300
1309
the procedure it was written for, reporting success for work it could not have done. That is this
@@ -1980,6 +1989,7 @@ a tunnel.
1980
1989
1981
1990
| Date | Change |
1982
1991
|---|---|
1992
+
| 2026-08-13 | Issue #189 (Gap 1, doctor half). **DES-FLOW-RESOLUTION-TWO-ADVISORY-LAYERS AMENDED**: the doctor layer is now specified in full — per-tuple lines in loader precedence order; repo tier read with the gate's ls-tree mechanics but HEAD-resolved by doctor and degrading to unknown on git failure (readFlowGate was considered and REJECTED for this read: its fail-closed catch turns a broken folder into deny, and "deny implies the file exists" would print a confident wrong ✓; the gate's no-ref rule guards against an agent self-authorizing, which a host preflight does not face); staged tier via the new `readStagedSkills` whose pattern-manifest packages read as not-enumerable because manifest patterns can DISABLE files and a wrong ✓ is the one inadmissible error direction. **DES-AI-TRIGGER-FLOW-GATE UNCHANGED, checked** — the gate itself is untouched; doctor copies its read mechanics rather than calling it, precisely so gate semantics stay pure WHO-may-fire. **DES-CLI-SURFACE UNCHANGED, checked** — doctor stays read-only/always-safe; the new checks carry no fixAction. |
1983
1993
| 2026-08-13 | Issue #189 (Gap 1, runner half). **NEW `DES-FLOW-RESOLUTION-TWO-ADVISORY-LAYERS`**: flow resolution is verified at two advisory layers and refused at neither — the runner compares `PI_FLOW` against the LOADED skill names post-load, pre-session, pre-spend (`isFlowLoaded`, exact equality, `disableModelInvocation` counts) and reports a miss as one `flow_not_loaded` line; doctor is the approximate host-side layer, landing with the companion change. Records why report-not-refuse (flow is by doctrine a prompt hint; a refusal shipped in an image upgrade fails yesterday's jobs and burns a budget slot per delivery), why the runner layer is the exact one (pi names a skill `frontmatter.name \|\| parentDirName` at the pin, so only the loaded set is authoritative), and the rejected alternatives (boot-time failure — `parseTriggers` stays pure and absent-at-HEAD is a legal steady state; `event.json` carriage — execution knob, not a delivery fact; a new top-level outcome — admin surfaces drop unknown outcomes, new vocabulary rides a `reason`). **DES-AI-TRIGGER-FLOW-GATE UNCHANGED, checked** — the gate answers WHO may fire a flow and keeps its pinned-sha object-store read; the new entry answers whether the flow EXISTS in the box, and neither consults the other. **DES-TRIGGERS-UNIFIED-FILE UNCHANGED, checked** — the shared validator gains nothing; the flow travels as job data the queue already carried. |
1984
1994
| 2026-08-12 | Issue #181 (the budget lever and the trend lines). **DES-COST-FOLD-BY-SCAN AMENDED**: the fold gains `dailyByFlow` — the composite (day, flow) fold at the same loop `buildDaily` and `buildByFlow` already walk separately, gap-padded per flow over the SHARED span (small multiples are only comparable on one x-domain) with the machine-key/display-label split held (`flowLabelOf` extracted so the two flow folds cannot drift on what a flow is called). The series shares `daily`'s first-run origin for the same sparkline-density reason recorded on the `sinceMs` row. **CONST-BUDGET-BEFORE-TOKENS UNCHANGED, checked** — `readBudget`'s GET-only posture now covers the token counter too, and the new junk-URL parse guard degrades synchronously (the `readSchedulers` failFast posture; without it a canned "not-a-url" fixture burns the full timeout per test). **DES-ADMIN-VIA-PI-EXTENSION UNCHANGED, checked** (the dashboard's budget meters and `/dispatch budget` are untouched; the page ADDS a display, replaces nothing — the "no replacement on that" ruling). |
1985
1995
| 2026-08-12 | Issue #181 (insights becomes the ONE analytics surface). **DES-ADMIN-VIA-PI-EXTENSION AMENDED**: six in-component views become FOUR — the COSTS and GRAPH views leave the overlay for the insights artifact, taking their two per-view refresh policies with them (the stale-gated tick piggyback and the entry-plus-`r`-only posture existed for those fetch paths; the overlay is back to one snapshot poll plus the tail read); the slash-command list drops `costs` and `graph` for `insights`; the graph-html paragraph re-homes onto the bare `insights` command; the LIST footer's `c costs`/`g graph` pair becomes `i insights` (51+18=69 of 76 columns, the arithmetic comment re-run), and the `i` key resolves the overlay with a done-action so index.ts writes and opens the page between overlays — the addTrigger route, deliberately not a dep seam and not a TUI suspend bracket, and deliberately BEFORE the dialog guard (the action needs no dialogs, and an older pi without them must still reach the one analytics surface). `DES-QUEUE-BULLMQ-OVER-CUSTOM`'s parenthetical names the insights artifact now. The dashboard's fs ban is UNCHANGED, checked, and dashboard.ts drops its pricing/costs/graph-model imports entirely. **DES-COST-FOLD-BY-SCAN UNCHANGED, checked** (the fold and its joins are what the page is made of; nothing about them moved). **DES-GRAPH-EDGE-DERIVATION UNCHANGED, checked** (the edge rules' one home; the model gained no vocabulary). |
Copy file name to clipboardExpand all lines: specs/interfaces.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2255,6 +2255,7 @@ recorded repair is re-running `/dispatch setup` (or editing the pointer by hand)
2255
2255
2256
2256
| Date | Change |
2257
2257
|---|---|
2258
+
| 2026-08-13 | Issue #189 (Gap 1, doctor half: per-trigger flow-tier resolution lines). **INT-TRIGGERS-FILE-CONTRACT UNCHANGED, checked** — no new trigger field and no validation change; doctor reads the parsed file it already read, now carrying per-trigger (flow, folder, skillsDir, packages) tuples instead of only deduped sets. **INT-CONTAINER-JOB-INPUTS UNCHANGED, checked** — nothing new crosses the container boundary; the doctor lines are host-side only. **INT-PI-PACKAGES-FILE-CONTRACT UNCHANGED, checked** — the stage manifest's shape is untouched; `readStagedSkills` layers a never-throws skills enumeration over `readStageManifest` without reading any new field. |
2258
2259
| 2026-08-13 | Issue #189 (Gap 1, runner half: a `run.flow` that resolves in no skill tier is a silent exit-0 no-op). **INT-CONTAINER-JOB-INPUTS AMENDED**: the worker-passed env gains `PI_FLOW` — the trigger's `run.flow` verbatim, forwarded ONLY when the job carries a flow, omitted for a bare `run.task` cron job, never an empty string (the `PI_PACKAGES`/`PI_SESSION_FILE` omission shape). It is the flow's STRUCTURAL copy beside the existing prompt prose, read by the runner to compare against the loaded skill set and emit `flow_not_loaded` on a miss; it rides env and not `event.json` because an execution knob is not a fact about the delivery (the `run.replicas` line), and the runner takes no charset opinion on it (compared and logged, never interpolated — a runner stricter than the shared validator would fail yesterday's jobs on an image upgrade). **INT-RUNNER-EXIT-CODE-PROTOCOL UNCHANGED, checked** — the miss is one advisory log line before any session exists; no new exit code, no new reason, and the job proceeds (`DES-FLOW-RESOLUTION-TWO-ADVISORY-LAYERS` records why report-not-refuse). **INT-TRIGGERS-FILE-CONTRACT UNCHANGED, checked** — no new trigger field; `run.flow`'s validation is untouched. **INT-WEBHOOK-PAYLOAD-SUBSET UNCHANGED, checked** — the variable carries operator config, never payload text. |
2259
2260
| 2026-08-11 | Issue #54 (Gap 2: a forge run's record could not be attributed to the `triggers.json` entry that fired it). **INT-RUN-HISTORY-FILE-CONTRACT AMENDED**: two additive, nullable fields on the replica fields' precedent — `triggerIndex` (the raw triggers-array index of the winning entry, cron entries counted) and `triggerType` (that entry's `on.type`) — explicit literals read from the job's own `data.trigger.matched` by the same no-spread `buildRecord`. An integer and a fixed enum, the admissible class the record already holds; the third `matched` key (`label`/`phrase`/`action`) is deliberately NOT persisted, because a label that satisfied an `any` predicate is collaborator-applied payload text and this record's PII-free property rests on holding no attacker-chosen string. Cron records keep `null` for both on purpose: a cron job's attribution is already exact via the `repeat:<id>:<millis>` jobId join, which also reaches records written before these fields existed, where a new field cannot. Persisting `matched` was deferred by issue #49's own no-new-record-fields scope, not by this record's posture; issue #54 is the consumer that makes it earn its place. Acceptance pins index `0` persisting as `0`, never `null`. **INT-CONTAINER-JOB-INPUTS AMENDED**: one cross-reference — `matched` remains event.json-only inside the container and never enters the prompt; its `index`/`type` alone are now also persisted host-side. **INT-WEBHOOK-PAYLOAD-SUBSET UNCHANGED, checked**: `matched` is harness-computed metadata, not a payload field, so the subset is untouched. **INT-OUTBOX-CONTRACT UNCHANGED, checked**: chained children carry no `trigger` and record `null`/`null`, exactly as manual runs do. |
2260
2261
| 2026-08-09 | Follow-up audit after issue #60. **INT-SDK-SESSION-OPTIONS AMENDED**, a correction rather than an addition: the option block still showed the TWO-path `additionalSkillPaths` literal, while the prose beside it had already been updated to three protected roots. A contract block that disagrees with its own note is worse than either being wrong alone, since a reader checking the code against the spec would have found the spec confirming the old shape. Now shows repo, injected, overlay. No behaviour changed; the literal had been stale since the injected tier landed hours earlier. |
0 commit comments