@edgehero/pi-dispatch-admin 4.3.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -68
- package/dist/console.mjs +78286 -0
- package/dist/index.mjs +151651 -107648
- package/dist/sandbox-session-monitor.mjs +106913 -0
- package/package.json +8 -4
- package/skills/operate-pi-dispatch/SKILL.md +147 -393
|
@@ -1,399 +1,153 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: operate-pi-dispatch
|
|
3
|
-
description:
|
|
3
|
+
description: Observe and operate pi-dispatch with its safe read tools, shared action planners, person confirmations and explicitly delegated unattended controls.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Operating pi-dispatch
|
|
7
7
|
|
|
8
|
-
pi-dispatch
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- `
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- `
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- `
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- `dispatch_limit_add
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- `
|
|
154
|
-
has no key that sets it.
|
|
155
|
-
- `dispatch_trigger_add` and `dispatch_trigger_edit` **have no `packages` parameter**. You can change it in
|
|
156
|
-
neither direction — you cannot make a trigger load packages and you cannot make one decline them — the
|
|
157
|
-
same reason `dispatch_run` withholds the provider and model from you.
|
|
158
|
-
|
|
159
|
-
So if a user asks you to change a trigger's packages flag, or to stage a package: **say plainly that you
|
|
160
|
-
cannot, and that it is an edit they make to `triggers.json` (and to their overlay dir) themselves.** Do not
|
|
161
|
-
attempt it through `dispatch_trigger_edit`, do not write the triggers file by another route, and do not treat
|
|
162
|
-
the missing parameter as a bug to work around. Reporting which triggers load the staged set and explaining
|
|
163
|
-
the change they would make is the whole of your part.
|
|
164
|
-
|
|
165
|
-
## The job image — `run.image`, and why you cannot set it
|
|
166
|
-
|
|
167
|
-
A trigger may carry `run.image`: the container image that trigger's jobs run in. Absent means the
|
|
168
|
-
deployment default (`PI_JOB_IMAGE`). It exists so one flow can have a Python toolchain and another Node +
|
|
169
|
-
Playwright, without one image carrying the union of both.
|
|
170
|
-
|
|
171
|
-
Report it when asked, and be precise about what it does and does not decide. **Which image a job runs is
|
|
172
|
-
which code it runs** — the pi version, the runner, the guardrail floor and the loader's discovery posture
|
|
173
|
-
all come from the image. What it does *not* decide is what the container may do: `--cap-drop=ALL`, the
|
|
174
|
-
non-root user, the read-only `/job` and the closed env allowlist are built by the worker for every image
|
|
175
|
-
alike. The panel shows the tag in the trigger list and states it in the drill-in either way.
|
|
176
|
-
|
|
177
|
-
**You cannot change it, in either direction.** `dispatch_trigger_add` and `dispatch_trigger_edit` have **no
|
|
178
|
-
`image` parameter**, `dispatch_run` has none, and there is no allowlist for you to consult — because there
|
|
179
|
-
is nothing model-callable to bound. Naming an image is an operator edit to the reviewed `triggers.json`,
|
|
180
|
-
exactly like `run.packages`.
|
|
181
|
-
|
|
182
|
-
So if a user asks you to point a trigger at a different image, or to build one: **say plainly that you
|
|
183
|
-
cannot, and that it is an edit they make to `triggers.json` themselves.** Do not route around it via
|
|
184
|
-
`dispatch_trigger_edit`, which changes only the flow, the venue and the model. Two useful things you
|
|
185
|
-
*can* say: the image must be built or pulled on the worker's own host, because jobs run with
|
|
186
|
-
`--pull=never` and nothing is fetched at job time; and `pi-dispatch doctor` lists every image their triggers name and flags one that is missing.
|
|
187
|
-
|
|
188
|
-
## Tool exclusion — `run.excludeTools`, and why you cannot set it
|
|
189
|
-
|
|
190
|
-
A trigger may carry `run.excludeTools`: built-in pi tools its jobs' sessions do NOT have. It exists so a
|
|
191
|
-
read-only flow (a triage that must not edit, a report that must not run a shell) is read-only by
|
|
192
|
-
construction rather than by request: the excluded tools are removed from the session's tool registry, so
|
|
193
|
-
nothing inside the job can switch them back on, and every such job logs the active tool list read back.
|
|
194
|
-
The drill-in states it beside the image row either way (`full pinned tool set` when absent).
|
|
195
|
-
|
|
196
|
-
**You cannot change it, in either direction.** `dispatch_trigger_add` and `dispatch_trigger_edit` have no
|
|
197
|
-
such parameter, and a chained job's request file can neither set nor drop it (the child inherits the
|
|
198
|
-
parent's exclusions). This is a permission surface, and the widening direction (a name quietly dropped
|
|
199
|
-
from the array) would read as harmless in any confirm dialog, which is exactly why no model-callable
|
|
200
|
-
route exists.
|
|
201
|
-
|
|
202
|
-
So if a user asks you to exclude a tool from a trigger, or to restore one: **say plainly that you cannot,
|
|
203
|
-
and that it is an edit they make to `triggers.json` themselves.** Two useful things you *can* say: only
|
|
204
|
-
the built-in names are legal (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`; a misspelling
|
|
205
|
-
refuses at load rather than silently excluding nothing), and a genuinely read-only trigger usually also
|
|
206
|
-
wants `"packages": false`, because extension tools are not excludable.
|
|
207
|
-
|
|
208
|
-
## Vault secrets — `run.secrets`, and why you cannot set it
|
|
209
|
-
|
|
210
|
-
A trigger may carry `run.secrets`: a map of environment variable name to an opaque **reference**, plus
|
|
211
|
-
`run.secretsProfile` naming which of the operator's resolvers reads them. Before the container starts, the
|
|
212
|
-
worker runs that resolver once per reference on the HOST and injects the values into the job's environment.
|
|
213
|
-
It exists so one trigger can hold a deploy key while every other job on the deployment holds none.
|
|
214
|
-
|
|
215
|
-
Report it when asked, and be precise about the shape of the trade. **The job holds no vault credential.** It
|
|
216
|
-
receives values, never the thing that can fetch values, so it cannot enumerate the vault or reach anything
|
|
217
|
-
the operator did not name in a reviewed file. What that does *not* do is bound where a value can go once the
|
|
218
|
-
agent has it: the agent can read its own environment, and the forge is on the egress allowlist by necessity.
|
|
219
|
-
The panel shows the count and the profile name in the trigger list and in the drill-in, never the references.
|
|
220
|
-
|
|
221
|
-
**You cannot set it, in either direction.** No `dispatch_*` tool has a `secrets` parameter, and none has a
|
|
222
|
-
`secretsProfile` parameter either. The second absence is worth understanding rather than treating as an
|
|
223
|
-
oversight: a profile that resolves nothing is refused at load, and no tool can write `run.secrets`, so a
|
|
224
|
-
profile picker could never produce a valid trigger even once. It would be a control that looks like a grant
|
|
225
|
-
and is only ever an error.
|
|
226
|
-
|
|
227
|
-
You also cannot declare a **resolver profile**. That is `/dispatch secrets add`, which the operator types
|
|
228
|
-
themselves, because declaring one means naming an absolute host path the worker executes.
|
|
229
|
-
|
|
230
|
-
So if a user asks you to give a trigger access to a vault, or to wire up 1Password: **say plainly that you
|
|
231
|
-
cannot, and that both halves are theirs.** Three useful things you *can* say: declaring the manager is
|
|
232
|
-
`/dispatch secrets add`, and it is two questions (a name and the path to a one-line script such as
|
|
233
|
-
`exec op read --no-newline "$1"`); binding it to a trigger is an edit to `triggers.json` beside that
|
|
234
|
-
trigger's `flow`; and `pi-dispatch doctor` lists every declared profile, fails loudly when a trigger names
|
|
235
|
-
one that is not declared, and warns when a **local** trigger binds secrets, because a local job edits the
|
|
236
|
-
operator's own folder in place and a credential the agent writes into `.env` lands in their real repository.
|
|
237
|
-
|
|
238
|
-
## Portfolio jobs: `run.portfolio`, and why you cannot set it
|
|
239
|
-
|
|
240
|
-
A cron trigger may carry `"portfolio": true`. Its jobs are then portfolio jobs, the one kind of job that may
|
|
241
|
-
send a budget priorities plan back. It is cron only, and never beside `run.command`.
|
|
242
|
-
|
|
243
|
-
**You cannot set it, in either direction.** No `dispatch_*` tool has a `portfolio` parameter, and the panel
|
|
244
|
-
never asks for it. It is budget authority, so it is a reviewed edit to `triggers.json`, `run.secrets`' rule.
|
|
245
|
-
`dispatch_trigger_edit` keeps a flag that is already there.
|
|
246
|
-
|
|
247
|
-
When such a job starts, the worker reads the triggers file again. If the flag is gone, the job runs as an
|
|
248
|
-
ordinary cron job. If this worker's envelope does not let `portfolio-job` write a plan (no envelope,
|
|
249
|
-
delegation off, or `portfolio-job` not in `delegation.writers`), the job is refused as `portfolio-no-envelope`
|
|
250
|
-
before anything is spent. The operator can fire such a trigger once by hand with
|
|
251
|
-
`pi-dispatch run --trigger <id>` in a terminal; no tool does that.
|
|
252
|
-
|
|
253
|
-
A portfolio job reads `/job/portfolio.json` (ids, numbers and operator labels only) and may write `/outbox/priorities.json`. The
|
|
254
|
-
worker applies that plan after the job completes, under the same rules as an operator's own plan. Its run
|
|
255
|
-
record's `plan` field says what happened: `applied`, `duplicate`, or `refused` with a fixed reason
|
|
256
|
-
(`plan-absent` when it wrote none, `plan-not-portfolio`, `plan-too-soon`, `plan-stale`, `plan-invalid` and the rest). A refused plan never makes
|
|
257
|
-
the job failed. A snapshot too large for the job refuses it as `portfolio-snapshot-oversize`, pre-spend.
|
|
258
|
-
|
|
259
|
-
## The forge a trigger listens to — `run.kind`
|
|
260
|
-
|
|
261
|
-
A webhook trigger names its forge: `"kind"` is `github`, `gitlab`, `forgejo` or `azure`. Everything else
|
|
262
|
-
about the trigger is the same — the `on.type`, the `{any, all, none}` label predicate, `flow`, `packages`,
|
|
263
|
-
`image`, `replicas`.
|
|
264
|
-
|
|
265
|
-
Three things are NOT the same, and all of them refuse at load rather than misbehaving quietly:
|
|
266
|
-
|
|
267
|
-
- **`pull_request` actions are the forge's own words.** GitHub takes
|
|
268
|
-
`labeled | opened | synchronize | reopened | review_submitted | closed`; GitLab takes
|
|
269
|
-
`open | update | reopen | approved | close`; Forgejo takes
|
|
270
|
-
`label_updated | opened | synchronized | reopened | closed`; Azure takes `created | updated` and has
|
|
271
|
-
no close word (a close trigger on azure is refused at load — not yet covered, not declined). A word
|
|
272
|
-
from the wrong forge is refused when the file is written. It would not break anything otherwise — it
|
|
273
|
-
would simply never match an event, and the trigger would look configured while doing nothing.
|
|
274
|
-
The close word rides **alone**: a rule mixing it with other actions is refused, because a close is
|
|
275
|
-
gated on the actor who closed the item where every other action gates on the author or a label.
|
|
276
|
-
`review_submitted` is GitHub's `pull_request_review` event: it fires on **every** submitted review, so
|
|
277
|
-
add `reviewState` (`approved | changes_requested | commented`) to narrow which verdicts are worth paying
|
|
278
|
-
for. That field is github-only and legal only beside `review_submitted`; anywhere else it refuses at
|
|
279
|
-
load, because a narrowing that cannot apply reads as one that does.
|
|
280
|
-
- **One `comment` trigger per forge.** Two GitHub comment triggers are refused; one GitHub and one GitLab
|
|
281
|
-
are fine.
|
|
282
|
-
- **Azure names no repository.** A work item belongs to a project, so an azure `label` or `comment` trigger
|
|
283
|
-
MUST set `run.repository` (the repo within the project to clone) and every other forge's must not. An
|
|
284
|
-
azure `pull_request` trigger may not carry a label predicate at all: Azure tags work items, never pull
|
|
285
|
-
requests, so `any`/`all`/`none` could never match and a rule that loads clean and never fires reads as a
|
|
286
|
-
broken harness.
|
|
287
|
-
|
|
288
|
-
## Close triggers and one-shots — `on.once`, and the re-arm
|
|
289
|
-
|
|
290
|
-
Two shapes fire on a close: the `issue` trigger type (an issue closing, `on.action` in the forge's close
|
|
291
|
-
word), and a `pull_request` rule whose only action is the close word (on GitHub and Forgejo a merged PR
|
|
292
|
-
counts as closed; on GitLab only an explicit close fires it). Both
|
|
293
|
-
may carry `on.number` to pin one specific item, and `once: true` (which requires `number`) makes the rule
|
|
294
|
-
a **one-shot**: after one run the worker marks the entry spent by adding
|
|
295
|
-
`on.disarmed: { at, jobId }` to it in `triggers.json`. The entry is never deleted — run history
|
|
296
|
-
attributes by array position — so `dispatch_triggers` and the panel keep showing it, marked spent,
|
|
297
|
-
matching nothing.
|
|
298
|
-
|
|
299
|
-
Four things to say when an operator asks:
|
|
300
|
-
|
|
301
|
-
- **A failed run still spends it.** "Fired" means "produced a run record", so a one-shot whose job failed
|
|
302
|
-
is spent too. The fix is the re-arm below, after they have read why it failed.
|
|
303
|
-
- **Re-arming is deleting `on.disarmed` from the entry** in `triggers.json`, nothing else. It is an
|
|
304
|
-
operator file edit: no tool and no panel key writes or removes that mark, so say so plainly rather than
|
|
305
|
-
reaching for `dispatch_trigger_edit` (which changes only the flow, the venue and the model).
|
|
306
|
-
- **Authoring one**: `dispatch_trigger_add` takes `kind: issue` (plus `number` and `once`), behind the
|
|
307
|
-
same confirm dialog as every trigger write, and a close-only `pull_request` rule accepts the same two
|
|
308
|
-
fields. The shared validator refuses anything malformed, and `once` requires `number`.
|
|
309
|
-
- **Both services must read the same file.** The disarm is a worker write, so a receiver pointed at a
|
|
310
|
-
different `triggers.json` stays armed; `PI_TRIGGERS_FILE` set to one absolute path is the fix, and
|
|
311
|
-
`pi-dispatch doctor` warns about it. In the compose topology the receiver's read-only mount lags a
|
|
312
|
-
disarm until restart, and the worker's own pre-spend check is what prevents a second run meanwhile.
|
|
313
|
-
|
|
314
|
-
## Racing two agents on one event — `run.replicas`
|
|
315
|
-
|
|
316
|
-
`"replicas": 2` on a `label`, `comment` or `pull_request` trigger turns one delivery into **two independent
|
|
317
|
-
paid jobs**: two containers, two branches (`pi/issue-7-r1` and `-r2`), two review requests, one human
|
|
318
|
-
picking. It works on every forge. Absent, nothing changes.
|
|
319
|
-
|
|
320
|
-
Three things to say when an operator asks about it:
|
|
321
|
-
|
|
322
|
-
- **It multiplies spend, and the caps are the only ceiling.** Each replica reserves its own budget slot
|
|
323
|
-
before its own tokens, so a `replicas: 2` trigger firing ten times a day consumes twenty slots of the
|
|
324
|
-
daily cap, not ten. Nothing is discounted; that is the feature, not an oversight.
|
|
325
|
-
- **You cannot set it from here.** There is no `replicas` parameter on `dispatch_trigger_add` or
|
|
326
|
-
`_edit`, and no panel key. It is a reviewed edit to `triggers.json`, deliberately: a spend multiplier is
|
|
327
|
-
plainly a capability a model should not gain. Say so rather than looking for a way around it.
|
|
328
|
-
- **It refuses on cron, beside `resume`, and beside `once: true`.** A local job's `/workspace` IS the
|
|
329
|
-
operator's folder, so two replicas would edit one working tree with no gate and no undo. A resumed run
|
|
330
|
-
continues one lineage where replicas exist to fork it. And a one-shot promises exactly one run, which N
|
|
331
|
-
racing sandboxes contradict.
|
|
332
|
-
|
|
333
|
-
`dispatch_trigger_add` takes an optional `forge` parameter, defaulting to `github`. Unlike `image`, this
|
|
334
|
-
one IS offered to the model — a model that can already add a GitHub trigger can already arm a paid run,
|
|
335
|
-
and naming GitLab instead does not widen that. Both paths stay behind the same operator confirm.
|
|
336
|
-
|
|
337
|
-
If you are asked why a GitLab trigger did not fire, the usual answers in order:
|
|
338
|
-
|
|
339
|
-
1. **The actor was not a Developer.** Every GitLab trigger is gated on the actor's project access level,
|
|
340
|
-
including label triggers — a GitLab label is not an approval the way a GitHub one is.
|
|
341
|
-
2. **No label was added by that event.** GitLab has no `labeled` action; the trigger fires on the labels
|
|
342
|
-
an event *added*, so editing an already-labelled issue does nothing. This is deliberate.
|
|
343
|
-
3. **The action word belongs to the other forge.** See above.
|
|
344
|
-
|
|
345
|
-
## Held jobs — what `waitFor` looks like from here
|
|
346
|
-
|
|
347
|
-
A trigger may carry `run.waitFor`, and a job whose conditions have not cleared is **held**: deferred, with
|
|
348
|
-
no budget slot reserved, no kill timer armed, no retry attempt consumed, and no run record written. It runs
|
|
349
|
-
exactly once when every condition clears.
|
|
350
|
-
|
|
351
|
-
What that means when you are asked about a job that "has not run":
|
|
352
|
-
|
|
353
|
-
- **Look at `dispatch_waits` before the queue counts.** A held job is not `waiting` and not `active`. It is
|
|
354
|
-
in the delayed set, which also holds every cron trigger's next occurrence, so the panel's `delayed` number
|
|
355
|
-
is not an answer to "what is stuck".
|
|
356
|
-
- **A held job has cost nothing yet.** Do not reason about it against the spend caps: they count container
|
|
357
|
-
starts, and this job has started none. The bounds that apply to it are its own (`PI_WAIT_MAX_MS`,
|
|
358
|
-
`PI_WAIT_MAX_CHECKS`), and when one is reached the job terminates with a run record naming the reason.
|
|
359
|
-
- **The terminal reasons say what to do.** `wait-refused` means the operator's check reported the condition
|
|
360
|
-
will never clear. `wait-unanswerable` means the check could not answer repeatedly, which usually means the
|
|
361
|
-
script is broken rather than the condition slow. `wait-expired` means a bound was reached.
|
|
362
|
-
`wait-skew` / `wait-unreadable` mean two services in the deployment disagree about the field and one needs
|
|
363
|
-
upgrading or restarting.
|
|
364
|
-
- **Cancelling is an operator decision.** `dispatch_wait_cancel` needs a confirm, writes no run record of its
|
|
365
|
-
own (a held job usually never ran; one held again on a retry keeps whatever its earlier attempts recorded), and
|
|
366
|
-
cannot be undone: the delivery is gone, and a webhook does not resend itself.
|
|
367
|
-
|
|
368
|
-
## Scoped limits — and the folder mutex you cannot turn off
|
|
369
|
-
|
|
370
|
-
`scoped-limits.json` holds per-scope bounds beside the deployment-global ones: `day`/`week`/`month` cap
|
|
371
|
-
how many jobs a repo or folder may run per window (refused pre-spend, reason `scope-cap`, never retried,
|
|
372
|
-
the scope's own counter still counts the refusal), and `concurrent` caps how many run at once (the excess
|
|
373
|
-
is deferred to the delayed set and runs when a slot frees — never dropped, no budget spent while waiting).
|
|
374
|
-
Edits apply live: the worker hot-reloads the file and keeps the last good version on a bad edit.
|
|
375
|
-
|
|
376
|
-
A `project:<id>` row caps every repo and folder of one project (projects.json) together. A job reserves in
|
|
377
|
-
its own repo or folder row first, then its project's row, then the global caps. A full project window refuses
|
|
378
|
-
with reason `project-cap` (the repo's slot is given back); a full project dollar window refuses `dollar-cap`,
|
|
379
|
-
or `allocation-cap` when the project's share of an allocation envelope is the number that bound.
|
|
380
|
-
A portfolio job on a worker whose envelope does not let `portfolio-job` write a plan refuses
|
|
381
|
-
`portfolio-no-envelope`, pre-spend and never retried (see the portfolio section above).
|
|
382
|
-
A row naming an id that projects.json does not define stops the worker from starting, so add the project first.
|
|
383
|
-
|
|
384
|
-
Separate from all of that, **local jobs carry a built-in one-job-per-folder mutex: at most one job per
|
|
385
|
-
folder at a time, always on, with NO configuration, NO tool, and NO panel key.** If an operator asks to
|
|
386
|
-
disable it, say plainly that there is no switch, deliberately: two agents editing one working tree race
|
|
387
|
-
each other with no gate and no undo, and an off switch's only use is re-opening that race. A `concurrent`
|
|
388
|
-
value on a folder scope can never raise the mutex's one-at-a-time (the lower bound always wins).
|
|
389
|
-
|
|
390
|
-
Three more things to say when asked:
|
|
391
|
-
|
|
392
|
-
- **A `scope-cap` or `project-cap` refusal is final for that window.** The counter is not resettable from any tool; the
|
|
393
|
-
window rolls over on its own (day/week/month, UTC). Raising the cap via `dispatch_limit_edit` takes
|
|
394
|
-
effect at the next job.
|
|
395
|
-
- **Deferrals are visible only as the queue's delayed count** (the panel's status line shows it when it
|
|
396
|
-
is nonzero). That count also includes cron next-occurrences and retry backoff — a nonzero number is
|
|
397
|
-
normal on any deployment with schedules.
|
|
398
|
-
- **Per-scope in-flight is not displayed anywhere.** The panel and `dispatch_limits` show `concurrent` as
|
|
399
|
-
configuration only; the live count lives inside the worker process and no reader can see it.
|
|
8
|
+
pi-dispatch starts paid coding jobs in isolated containers. Observe first, explain the proposed change, then use the
|
|
9
|
+
specific tool. Treat a refusal as an answer: report its reason and do not retry it by changing surfaces or writing files.
|
|
10
|
+
A partial or uncertain result means inspect the resulting state before doing anything again.
|
|
11
|
+
|
|
12
|
+
## Three action marks
|
|
13
|
+
|
|
14
|
+
Every console action declares an agent mark and its reason. The shared policy judges the exact validated transition,
|
|
15
|
+
not your explanation. A mixed request containing a permitted decrease and a widening is refused as a whole.
|
|
16
|
+
|
|
17
|
+
- `unattended`: eligible without a person only within the bounds below.
|
|
18
|
+
- `confirm`: a model may propose it, but a person must approve the concrete change.
|
|
19
|
+
- `person`: no model tool may do it, even behind a confirmation dialog. Explain the operator action instead.
|
|
20
|
+
|
|
21
|
+
In an interactive pi session every write shows a human confirmation, including pause, run and priorities. Declining
|
|
22
|
+
changes nothing. With no UI, every write requires `PI_DISPATCH_AGENT_WRITES=1` in the pi process's own environment
|
|
23
|
+
when that session starts, and an `unattended` transition. Putting it in a deployment `.env`, settings file, pointer or
|
|
24
|
+
tool argument grants nothing. A headless session keeps its initial grant until it exits.
|
|
25
|
+
|
|
26
|
+
The console's `agent` token role has the same unattended bounds. It uses the ordinary authenticated preview and
|
|
27
|
+
single-use Confirm ticket; an operator token is not a substitute. An operator mints and revokes tokens on the host.
|
|
28
|
+
Upgrade both the console and runtime on that host before minting an agent token: older versions reject the whole
|
|
29
|
+
file containing an unknown role. Revocation ends sessions at their next request.
|
|
30
|
+
|
|
31
|
+
Every tool write is audited as role `agent`, using the account's login name; API writes use the token name. Human
|
|
32
|
+
confirmation is recorded separately from unattended intervention. Undo goes through the same policy as another write:
|
|
33
|
+
an unattended pause never grants permission to resume a person's pause.
|
|
34
|
+
|
|
35
|
+
## Read tools
|
|
36
|
+
|
|
37
|
+
- `dispatch_status`: queue state, counts, budget, settings and schedulers.
|
|
38
|
+
- `dispatch_runs`: run records, recovery and turn limits; local workspace retention where known. Never raw logs,
|
|
39
|
+
transcript text, questions, drafts or bundle variable names.
|
|
40
|
+
- `dispatch_costs`: `7d`, `30d` or `mtd` history, optional flow/repository/project filters, dollar counters and current
|
|
41
|
+
running holds; model what-if over the same bounded runs. Quote every dollar's class (`metered`, `estimated`,
|
|
42
|
+
`seeded`, or a floor). Counter amounts are integer micro-dollars, one million per dollar. Unknown coverage is not zero.
|
|
43
|
+
- `dispatch_capacity`: `24h`, `7d` or `30d` fleet utilisation, optionally one host. Missing history is not idle time;
|
|
44
|
+
this deployment's accounting cannot measure unrelated processes on the host.
|
|
45
|
+
- `dispatch_hosts`: heartbeat/skew, versions, capacity and its source, CPU reserve, running/orphan holds, kept workspace
|
|
46
|
+
IDs, doctor counts and venue declarations/provenance. Never a doctor fix's text, a host path or a command.
|
|
47
|
+
- `dispatch_failed`: failed job IDs, queues, attempts and reason tokens. Never BullMQ's raw failure text.
|
|
48
|
+
- `dispatch_waiting`: queued work's reason, due time and holder, from the waiting read model without reading job data.
|
|
49
|
+
- `dispatch_setup`: first-run checklist states and trusted setup command arguments. A command in this result is advice,
|
|
50
|
+
not permission to execute a person-only step.
|
|
51
|
+
- `dispatch_triggers`: indices, configured execution facts, cron stalls and one-shot disarmed state. Indices are
|
|
52
|
+
zero-based inputs even where the console labels the same entry as row one.
|
|
53
|
+
- `dispatch_models`: roles by scope, effective fallbacks, price eligibility, cooldowns, model windows, typed proposals
|
|
54
|
+
and retained canary comparisons. Missing evidence is unknown. A role edit or adoption belongs to a person on the
|
|
55
|
+
Models page; no model tool can change the map or launch a canary.
|
|
56
|
+
- `dispatch_pauses`: pause windows and their indices.
|
|
57
|
+
- `dispatch_limits`: scoped limits, indices and observed use.
|
|
58
|
+
- `dispatch_waits`: held jobs and safe machine facts, distinct from delayed cron occurrences. Held work has not reserved
|
|
59
|
+
a job slot merely by waiting. Never infer that a nonzero delayed count means work is stuck.
|
|
60
|
+
- `dispatch_projects`: project IDs/members, spend/share, sizes and suggestions, and edit/delete refusal facts.
|
|
61
|
+
- `dispatch_allocations`: envelope, current weights/shares, history and writer hosts. Plan reasons stay private.
|
|
62
|
+
- `dispatch_secrets`: profile/bundle names, sources and problem words; received variables only as counts. Never variable
|
|
63
|
+
names, paths, roots, references or values.
|
|
64
|
+
- `dispatch_connections`: configured forge key names, egress, endpoint slots, credential sources, MCP/package names and
|
|
65
|
+
which deployment keys are set. Unknown or unreadable is not absent or healthy.
|
|
66
|
+
|
|
67
|
+
An agent reads machine facts, not operator or job prose. A viewer page can contain information an agent must not read;
|
|
68
|
+
use the agent projection. Logs, transcripts, parked questions, drafts, classifications, citations, standing answers,
|
|
69
|
+
answer rules, phase names/notes and raw failure messages stay outside model tools and agent-token responses.
|
|
70
|
+
|
|
71
|
+
## Unattended writes and their bounds
|
|
72
|
+
|
|
73
|
+
- `dispatch_set`: may lower a numeric cap explicitly present in the settings file: `dailyCap`, `weeklyCap`, `monthlyCap`,
|
|
74
|
+
`dailyTokenCap`, `maxTurns`, `maxTokens`, `concurrency`, `softHoldPct`, `maxCostUsd`, `dailyCostUsd`, `weeklyCostUsd`,
|
|
75
|
+
`monthlyCostUsd`. Never assume an inherited environment/default value. Raising, unsetting, model or provider changes
|
|
76
|
+
require a person. Dollars use ordinary decimal strings, at most six decimals, without exponents.
|
|
77
|
+
- `dispatch_limit_add`: may add a restrictive row without `minJobs`, `memory` or `cpus`.
|
|
78
|
+
- `dispatch_limit_edit`: may lower explicit counts, dollars or `hostShare` in the same scope; no scope, size, minimum
|
|
79
|
+
or inherited-bound changes. `dispatch_limit_delete` requires a person. Local folders always retain their one-job mutex.
|
|
80
|
+
- `dispatch_pause`: pause the fleet, or a declared `queue`; already running containers finish.
|
|
81
|
+
- `dispatch_pause_add`: add a pause window. Editing/deleting one can release a person's pause and requires confirmation.
|
|
82
|
+
- `dispatch_cancel`: cancel an active, queued or held job; `allHeld` requests a bounded cancellation of held jobs.
|
|
83
|
+
`dispatch_wait_cancel` remains the held-job form. Cancellation cannot restore the stopped work.
|
|
84
|
+
- `dispatch_remove_failed`: remove one failed job, or `all` matching a queue/repository. Run history remains.
|
|
85
|
+
- `dispatch_trigger_edit`: unattended only when turning existing `recover`, `steer`, `progress` or `statusComment` off.
|
|
86
|
+
Omitted flow and flags are preserved. Changes to a trigger's `maxTurns` still require a person.
|
|
87
|
+
- `dispatch_priorities_set`: weights inside the current envelope, floors, writers, step and interval, never dollars.
|
|
88
|
+
`plan-too-soon` means wait; `plan-stale` means read again, not replay; `plan-incomplete` means name every project.
|
|
89
|
+
- `dispatch_run`, `dispatch_trigger_fire` and `dispatch_rerun`: paid starts under the admission rules below.
|
|
90
|
+
|
|
91
|
+
Unattended controls can disrupt service, start capped paid work and move budget inside the envelope. They cannot raise
|
|
92
|
+
an operator's boundary. Pi's own `bash`, `write` and `edit` tools are outside these controls; an unattended agent session
|
|
93
|
+
must run with those tools disabled. Do not route around a dispatch refusal with a shell or file edit.
|
|
94
|
+
|
|
95
|
+
## Paid starts
|
|
96
|
+
|
|
97
|
+
`dispatch_run` takes a local folder, flow and task, with no model or spending controls. Fire now takes a cron trigger's
|
|
98
|
+
index; rerun takes a local/cron run ID. They all check the actual process's `PI_DISPATCH_RUN_ROOTS`, current committed
|
|
99
|
+
`ai-trigger: allow` in `.pi/skills/<flow>/SKILL.md` at HEAD, a clean repository root and the shared
|
|
100
|
+
`PI_DISPATCH_RUN_PER_HOUR` counter immediately before enqueue. Agent Fire now/rerun requires both grants explicitly
|
|
101
|
+
present and valid in the process. Preview consumes no hourly attempt. Refused attempts can consume an attempt.
|
|
102
|
+
|
|
103
|
+
`dispatch_trigger_fire` fires once without changing the schedule, preserves its reviewed execution settings and is
|
|
104
|
+
limited to a schedule served by this deployment's queue. A second fire in the same minute is refused, even if the first
|
|
105
|
+
job was removed. A cron rerun uses the trigger as configured now. Forge reruns, `run.command`, slash commands and
|
|
106
|
+
portfolio fires are person-only on these model paths, with or without a UI. An injected host skill's frontmatter is
|
|
107
|
+
not read by the committed repository opt-in. Every worker start still passes its ordinary budgets and isolation gates.
|
|
108
|
+
|
|
109
|
+
## Writes requiring a person
|
|
110
|
+
|
|
111
|
+
- `dispatch_trigger_add`, `dispatch_trigger_delete`, and every `dispatch_trigger_edit` outside the flag-off subset.
|
|
112
|
+
Adding a trigger authorizes later work outside per-call local admission, so it cannot be unattended. A tool-added
|
|
113
|
+
trigger always has `run.secretBundle: false`; an edit preserves reviewed fields.
|
|
114
|
+
- `dispatch_retry_failed`: one job or `all` by queue/repository; the preview names each job's reserve. A retry replays
|
|
115
|
+
saved execution fields, so a human must decide even when the current trigger is narrower.
|
|
116
|
+
- `dispatch_resume`: fleet or declared queue; an agent cannot know who paused it.
|
|
117
|
+
- `dispatch_pause_edit`, `dispatch_pause_delete`, `dispatch_limit_delete`, and limit changes beyond explicit decreases.
|
|
118
|
+
- `dispatch_project_add`, `dispatch_project_edit`, `dispatch_project_delete`: membership decides which cap applies.
|
|
119
|
+
- `dispatch_envelope_set`: the envelope is the operator's boundary.
|
|
120
|
+
- `dispatch_allocation_revert`: history index; revert bypasses a new plan's step and interval.
|
|
121
|
+
- Job sizes require confirmation, including a suggestion's exact `dispatch_limit_add`/`dispatch_limit_edit` call.
|
|
122
|
+
A job can fake an out-of-memory observation; a suggestion is advisory evidence.
|
|
123
|
+
|
|
124
|
+
## Person-only fields and operations
|
|
125
|
+
|
|
126
|
+
Never set, clear or bypass these through a model tool, including alternate spellings:
|
|
127
|
+
`run.models`, `run.fallbackModels`, `run.maxCostUsd`, `run.mcp`, `run.secretBundle`, `modelRoles`,
|
|
128
|
+
`run.command`, `run.instructions`, `run.skillsDir`, `run.awaitInput`, `run.image`, `run.resume`, `run.packages`,
|
|
129
|
+
`run.excludeTools`, `run.excludeExtensions`, `run.excludeExtensionTools`, `run.secrets`, `run.secretsProfile`, `run.replicas`, `run.portfolio`, `run.waitFor`,
|
|
130
|
+
`on.disarmed`, `answerRules`, `standingAnswers`, `secretBundles`, `secretProfiles`.
|
|
131
|
+
`TRIGGER_ADD_FIELDS` is the allowed schema, not an invitation to use a file editor for missing parameters.
|
|
132
|
+
|
|
133
|
+
Secret resolver/profile/bundle declarations name host scripts or credential authority (`OPERATOR_ONLY_KEYS`), so only
|
|
134
|
+
an operator's typed command or console can change them. The same holds for tokens, credentials, egress, model endpoint
|
|
135
|
+
files, a spent one-shot's re-arm, replacing the whole pause-window list, the sandbox terminal/forks and setup wizard.
|
|
136
|
+
Packages load by default unless `run.packages:false`; global overlay extensions are a separate switch. Neither may be
|
|
137
|
+
changed by tools. A job image selects code; the worker's container argv still owns isolation. Images must already exist
|
|
138
|
+
on the worker (`--pull=never`).
|
|
139
|
+
|
|
140
|
+
Steering, follow-ups, withdrawing a person's message, parked answers and approvals have no model tool. Tell the person
|
|
141
|
+
to use the run page or type `/dispatch chat <run-id>` / `pi-dispatch chat`. A draft job only proposes an answer; a person
|
|
142
|
+
alone previews and confirms its send. Standing-answer adoption is a separate person's confirmation. Private decision
|
|
143
|
+
context can propose a change but cannot grant permissions, widen answer rules or send its own answer.
|
|
144
|
+
|
|
145
|
+
## Setup from a shell
|
|
146
|
+
|
|
147
|
+
`pi-dispatch doctor --json` emits the same sanitized machine record doctor publishes, with human diagnostics on stderr.
|
|
148
|
+
`--no-publish` does not prevent JSON output. Check its exit code and fail/warn states.
|
|
149
|
+
|
|
150
|
+
`pi-dispatch up --yes` does not grant every disruptive step. The operator may additionally authorize `--adopt-volume`,
|
|
151
|
+
`--restart-proxy` and `--refresh-proxy-rules`. A combined refresh/replacement with jobs attached needs both proxy flags;
|
|
152
|
+
no flag overrides another deployment's volume ownership. The exact commands are printed before execution and backups
|
|
153
|
+
are retained. `pi-dispatch setup github` still needs a person's browser click.
|