Skip to content

Commit 617d930

Browse files
authored
Merge pull request #101 from edgehero/docs/triggers-and-workflows
docs: lead with triggers, and document workflows and pi extensions
2 parents b16f0f0 + 796281e commit 617d930

14 files changed

Lines changed: 443 additions & 141 deletions

File tree

.github/workflows/image.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
# No secret required: GHCR auth uses the built-in GITHUB_TOKEN (packages: write). One-time after the first
99
# successful run: make the ghcr.io/edgehero/pi-job package Public in the org's package settings, otherwise
1010
# `docker pull` needs auth.
11+
#
12+
# Tags: `latest`, the git `sha`, and the PRODUCT version from the root package.json (so a release cuts a
13+
# tag that never moves, and a deployment can pin `ghcr.io/edgehero/pi-job:<version>` instead of tracking latest).
1114

1215
name: image
1316

@@ -16,6 +19,7 @@ on:
1619
branches: [main]
1720
paths:
1821
- "image/**"
22+
- "package.json" # the ROOT one: a product version bump republishes under its own tag
1923
- ".github/workflows/image.yml"
2024
# Manual re-run (available once this file is on the default branch).
2125
workflow_dispatch: {}
@@ -46,13 +50,18 @@ jobs:
4650
username: ${{ github.actor }}
4751
password: ${{ secrets.GITHUB_TOKEN }}
4852

53+
- name: Resolve the product version (the tag operators can pin)
54+
id: v
55+
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
56+
4957
- name: Tags + labels
5058
id: meta
5159
uses: docker/metadata-action@v5
5260
with:
5361
images: ${{ env.IMAGE }}
5462
tags: |
5563
type=raw,value=latest
64+
type=raw,value=${{ steps.v.outputs.version }}
5665
type=sha
5766
5867
- name: Build + push (amd64 + arm64)

.github/workflows/receiver-image.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
# No secret required: GHCR auth uses the built-in GITHUB_TOKEN (packages: write). One-time after the first
1111
# successful run: make the ghcr.io/edgehero/pi-dispatch-receiver package Public in the org's package
1212
# settings, otherwise `docker pull` needs auth.
13+
#
14+
# Tags: `latest`, the git `sha`, and the PRODUCT version from the root package.json (so a release cuts a
15+
# tag that never moves, and a deployment can pin `ghcr.io/edgehero/pi-dispatch-receiver:<version>` instead of tracking latest).
1316

1417
name: receiver-image
1518

@@ -21,6 +24,7 @@ on:
2124
- "worker/src/**"
2225
- "worker/package.json"
2326
- "package-lock.json"
27+
- "package.json" # the ROOT one: a product version bump republishes under its own tag
2428
- ".github/workflows/receiver-image.yml"
2529
# Manual re-run (available once this file is on the default branch).
2630
workflow_dispatch: {}
@@ -51,13 +55,18 @@ jobs:
5155
username: ${{ github.actor }}
5256
password: ${{ secrets.GITHUB_TOKEN }}
5357

58+
- name: Resolve the product version (the tag operators can pin)
59+
id: v
60+
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
61+
5462
- name: Tags + labels
5563
id: meta
5664
uses: docker/metadata-action@v5
5765
with:
5866
images: ${{ env.IMAGE }}
5967
tags: |
6068
type=raw,value=latest
69+
type=raw,value=${{ steps.v.outputs.version }}
6170
type=sha
6271
6372
- name: Build + push (amd64 + arm64)

README.md

Lines changed: 186 additions & 127 deletions
Large diffs are not rendered by default.

admin/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ Every trigger produces the same job, through the same path: one queue, one conta
3333

3434
The container is the boundary (pi's missing permission system, enforced by Docker). Those isolation flags are built by the worker's own `docker run` argv, so nothing an image contains can weaken them; the non-root user is a property of the **image**, which is why an image has to meet the conformance checklist in [`docs/job-image.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/job-image.md). Spend is checked before a container starts, so a runaway or a junk trigger costs a refusal, not a surprise bill. The job image is yours to shape, per deployment or per trigger, and it ships Playwright and Chromium so a flow can build a frontend, screenshot it, and iterate on the render.
3535

36+
## Triggers: what starts a job
37+
38+
Every trigger is one `{ on, run }` entry in a single `triggers.json`, read live by the worker (cron) and the receiver (forge webhooks), and editable from the console. **`on` is what fires it; `run` is the skill it runs.**
39+
40+
| `on.type` | Fires on | What narrows it | What the agent gets as its task |
41+
|---|---|---|---|
42+
| `cron` | your schedule | nothing: a schedule is its own condition | the `task` written in the file |
43+
| `label` | a label on an **issue** (or an Azure work item), never a pull request | a label predicate: `any`, `all`, or `none` (which can suppress a fire, never cause one) | the issue title and body |
44+
| `comment` | a comment containing your phrase, for example `@pi` | the phrase, and one comment trigger per forge | the comment body, plus the issue title and body |
45+
| `pull_request` | a PR or MR event | `action`, in your forge's own words, plus the same label predicate | the PR title and body |
46+
47+
Four forges: GitHub, GitLab, Forgejo (and Gitea), Azure DevOps. **Who may fire a trigger is your forge's decision, not this service's**: on GitHub the label *is* the approval, because only collaborators can apply one, while GitLab, Forgejo and Azure resolve the actor's permission through their APIs. Each forge's action vocabulary is validated when the file loads, so a word from the wrong forge is refused instead of silently never matching.
48+
49+
**Flows, and workflows.** `run.flow` names a skill committed to the target repo at `.pi/skills/<flow>/SKILL.md`, read from the **default branch**, so the repo owns the prompt and merging it is the repo's consent. A skill may call other skills, which is already a workflow. For typed multi-stage ones, a pi extension such as `@juicesharp/rpiv-workflow` can be staged into the deployment: pinned to an exact version, installed on your host (never at job time, since jobs run offline), loaded in every container, and declinable per trigger.
50+
51+
**How a workflow gets triggered, in one line:** `label / comment / PR / cron` fires **one job in one container**, that job runs `run.flow`, and the flow drives whatever stages follow. Four consequences worth knowing before you build on it. A trigger names a **flow, never a workflow**, so which stages run is decided inside the job by that skill. A job is **not an interactive session** (the container hands pi one assembled prompt and reads the exit line), so a workflow extension's slash command has nobody to type it: in a job the flow's instructions drive the workflow, or a small extension you also staged calls its API from a lifecycle hook. One trigger is **one job, one budget slot and one turn budget**, so ten stages share the same `PI_MAX_TURNS` and per-job token budget, and exhausting either ends the job as a policy refusal that is never retried. And whether the workflow's own state survives depends on the trigger kind: a cron or CLI job has your folder mounted read-write so state persists between runs, while a forge job gets a fresh clone that is discarded with the container. Full reference: [`docs/workflows.md`](https://github.com/edgehero/pi-dispatch/blob/main/docs/workflows.md).
52+
3653
## The console: `/dispatch`
3754

3855
One command puts a live TUI over the whole deployment:

admin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@edgehero/pi-dispatch-admin",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"description": "Operator console (a pi extension) + skill for pi-dispatch: run the pi coding agent as a self-hosted service. A /dispatch TUI for the queue, spend caps, run history, editable GitHub, GitLab, Forgejo and Azure DevOps triggers (cron/label/comment/PR/MR/work item), and scheduled pause windows — plus AI-operable, human-confirmed controls. Runs against a live pi-dispatch deployment.",
55
"keywords": [
66
"pi-package",

admin/src/setup-wizard.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Notify = ((message: string, type?: string) => void) | undefined;
5252
* version, so a release bump stays atomic: bump the worker and the test fails here until this literal
5353
* follows in the same change.
5454
*/
55-
export const RUNTIME_VERSION = "0.1.1";
55+
export const RUNTIME_VERSION = "0.1.2";
5656

5757
/**
5858
* The `@edgehero/pi-dispatch-receiver` version the trigger-edge step installs -- pinned for exactly the
@@ -62,7 +62,7 @@ export const RUNTIME_VERSION = "0.1.1";
6262
* independently (the receiver's dependency range on the runtime is `^`), and pretending otherwise would
6363
* install a version that does not exist the first time they diverge.
6464
*/
65-
export const RECEIVER_VERSION = "0.1.0";
65+
export const RECEIVER_VERSION = "0.1.1";
6666

6767
/** The two npm package names, spelled once. Literals of this module -- see npmInstallArgsFor's argument. */
6868
const RUNTIME_PKG = "@edgehero/pi-dispatch";

admin/test/setup-wizard.test.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ test("wizard: the edge's service answer installs the PINNED receiver, then the -
803803
assert.equal(attached.length, 2, "the npm install and the unit install — nothing else");
804804
assert.equal(attached[0].argv0, "npm", "posix npm, per npmSpawnOptions");
805805
assert.deepEqual(attached[0].args, mod.npmInstallArgsFor(RECEIVER_PKG, mod.RECEIVER_VERSION));
806-
assert.ok(attached[0].args.includes(`${RECEIVER_PKG}@0.1.0`), "the pinned name@version token, spelled out");
806+
assert.ok(attached[0].args.includes(`${RECEIVER_PKG}@0.1.1`), "the pinned name@version token, spelled out");
807807
assert.equal(attached[0].cwd, dir, "installed into the deployment dir, by cwd");
808808
assert.deepEqual(
809809
JSON.parse(readFileSync(join(dir, "package.json"), "utf8")),
@@ -817,7 +817,7 @@ test("wizard: the edge's service answer installs the PINNED receiver, then the -
817817
assert.equal(attached[1].cwd, dir);
818818

819819
const c = seen.confirm.find((x) => /receiver/i.test(x.title));
820-
assert.match(c.message, new RegExp(`${RECEIVER_PKG.replace("/", "\\/")}@0\\.1\\.0`), "the confirm shows the exact pin");
820+
assert.match(c.message, new RegExp(`${RECEIVER_PKG.replace("/", "\\/")}@0\\.1\\.1`), "the confirm shows the exact pin");
821821
assert.match(c.message, /service install --receiver/, "and the unit command it will run after");
822822
assert.ok(c.message.includes(dir), "and names the cwd");
823823
assert.ok(reachedFirstTrigger(seen), "the wizard continued to step 11");

docs/global-pi-overlay.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ be in the overlay. Never place the admin extension there (it can enqueue paid jo
118118

119119
## Packages (pinned, per-trigger)
120120

121-
A **pi package** is third-party code from npm that contributes extensions, skills, prompts and themes. Every
121+
A **pi package** is third-party code from npm that contributes extensions, skills, prompts and themes. This
122+
is also the road a **workflow extension** takes into a job, and what a workflow can and cannot keep across
123+
jobs is its own reference: [`workflows.md`](workflows.md). Every
122124
job runs with `PI_OFFLINE=1`, which makes pi's resolver **refuse to shell out to npm**, so a package cannot
123125
be installed at job time: you stage it on **your host**, into the overlay, and a trigger opts in. Note what
124126
is doing the work here. The container's network is **not** cut off (egress is open, as above, and

docs/job-image.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ a second thing to forget to bump. Re-declare only a label whose truth your layer
3636
single re-declared `dev.pi-dispatch.forges` with `azure` appended (see
3737
[azure-devops.md](azure-devops.md), which also names the `--build-arg BASE=…` you need).
3838

39+
**Pin the base if you would rather not track `latest`.** Every published build also carries the **product
40+
version** as a tag (`ghcr.io/edgehero/pi-job:0.8.0`) and the git `sha`, and neither ever moves, while
41+
`latest` follows `main`. The receiver image is tagged the same way. A pinned base is the honest choice for a
42+
derived image whose layer assumes something about the base; the cost is that a pin does not pick up a
43+
security rebuild, so bump it deliberately.
44+
3945
**Copy `image/Dockerfile` and add to it.** You inherit every property in the checklist below for free, and
4046
the only thing you own is your own `RUN apt-get install …` layer. Prefer this over the layer above only when
4147
you need to change something *inside* the base build (a different base distro, a different pi pin).

0 commit comments

Comments
 (0)