-
Notifications
You must be signed in to change notification settings - Fork 0
469 lines (462 loc) · 26.3 KB
/
Copy pathrust.yml
File metadata and controls
469 lines (462 loc) · 26.3 KB
1
2
3
4
5
6
7
8
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
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
name: rust
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
uses: rainlanguage/rainix/.github/workflows/rainix-rs-test.yaml@main
secrets: inherit
static:
uses: rainlanguage/rainix/.github/workflows/rainix-rs-static.yaml@main
secrets: inherit
# The rainix reusables above build via cargo. The cron and the metrics
# tooling run the flake PACKAGE (`nix run .#pr-review-report`), which cargo
# CI does not exercise — a workspace/lockfile drift can leave cargo green
# while the package fails to build. This job guards that path directly.
nix-build:
name: nix-build
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@a7ad9c4f0c65208097f4d34f3cfa1913b80cce5c # main
- run: nix build .#pr-review-report --print-build-logs
# The crons do NOT resolve `.#pr-review-report` — campaign-run.sh and
# review-run.sh both resolve `path:$DIR#pr-review-report`. A `path:` ref copies
# the working directory verbatim, gitignored files INCLUDED, so any build churn
# the flake's fileset fails to exclude enters the source and changes its hash.
# When that happens a cron tick rebuilds the crate — and runs its release-profile
# test suite inside the derivation — before the model gets a single token, burning
# the run's own `timeout` budget. That is a silent, recurring cost: nothing fails,
# the run is just late.
#
# A fresh CI checkout has no `target/`, so merely building `path:` would pass
# straight through the bug. This job therefore CREATES the churn condition and
# asserts the derivation is unchanged — the invariant that actually matters.
flake-src-excludes-build-churn:
name: flake src excludes build churn
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@a7ad9c4f0c65208097f4d34f3cfa1913b80cce5c # main
- name: build churn must not change the cron's derivation
run: |
set -euo pipefail
# Every output a cron resolves, not just the crate. The runners are
# writeShellApplication packages that depend on pr-review-report, so the
# fileset governs their hashes too — churn leaking into the source
# rebuilds the runner as well as the binary.
OUTPUTS="pr-review-report campaign-run review-run refresh-human-queue design-doctor backfill-human-queue-history review-queue"
drvs() {
for o in $OUTPUTS; do
printf '%s %s\n' "$o" "$(nix path-info --derivation "path:$PWD#$o")"
done
}
before="$(drvs)"
echo "clean:"; echo "$before" | sed 's/^/ /'
# Exactly what cargo/nix leave behind in the live install dir (~100MB there).
mkdir -p pr-review-report-rs/target/debug/incremental
echo churn > pr-review-report-rs/target/debug/churn
echo churn > pr-review-report-rs/target/CACHEDIR.TAG
after="$(drvs)"
echo "churned:"; echo "$after" | sed 's/^/ /'
if [ "$before" != "$after" ]; then
echo "::error::Build churn changed a derivation the crons resolve."
echo "flake.nix's fileset must exclude cargo output; otherwise every cron"
echo "tick after a local build pays a full rebuild + in-derivation test suite."
diff <(echo "$before") <(echo "$after") || true
exit 1
fi
# The two refs must stay interchangeable, so warming one warms the other
# and a human debugging with `.#` sees what the cron actually runs.
for o in $OUTPUTS; do
dot="$(nix path-info --derivation ".#$o")"
path="$(nix path-info --derivation "path:$PWD#$o")"
if [ "$dot" != "$path" ]; then
echo "::error::'.#$o' and 'path:\$PWD#$o' diverged."
echo " .# $dot"
echo " path: $path"
exit 1
fi
done
echo "stable and equivalent across all cron outputs"
# `doCheck` is left ON deliberately: with the fileset fixed, a rebuild only
# happens on a real source change, and running the suite then is the point.
# This guards the other half — if someone later sets doCheck = false, the
# cargo test gate must still exist in CI or the crate ships untested.
- name: the cargo test gate still exists
run: |
set -euo pipefail
grep -q "rainix-rs-test" .github/workflows/rust.yml || {
echo "::error::rainix-rs-test is gone from rust.yml — the crate's test gate must live somewhere."
exit 1
}
echo "rainix-rs-test present"
# Every tool the crons execute must come from the flake (#76). These three ways of
# acquiring a tool ambiently all LOOK fine while they work and fail as something else:
# a registry flakeref floats with channels.nixos.org, a literal store path degrades to
# ~/.nix-profile when the store is gc'd, and ~/.nix-profile first on PATH means the cron
# runs whatever a human last installed. None of them can be caught by running the code,
# so they are asserted statically here.
tools-come-from-the-flake:
name: tools come from the flake
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@a7ad9c4f0c65208097f4d34f3cfa1913b80cce5c # main
- name: no ambient tool acquisition in any script
run: |
set -euo pipefail
fail=0
# `nixpkgs#x` resolves via the GLOBAL REGISTRY, not flake.lock.
if grep -rn 'nixpkgs#' ./*.sh; then
echo "::error::A script resolves a registry flakeref. Add the tool to the"
echo "package's runtimeInputs in flake.nix instead — the registry floats."
fail=1
fi
# A baked store path is not a pin: nothing GC-roots it, and when it vanishes
# PATH silently falls through to an unpinned copy.
if grep -rn '/nix/store/' ./*.sh; then
echo "::error::A script hardcodes a /nix/store path. Use runtimeInputs."
fail=1
fi
# Ambient authority: whatever a human last installed, ahead of the closure.
if grep -rn 'PATH=.*\.nix-profile' ./*.sh; then
echo "::error::A script puts ~/.nix-profile on PATH. Let nix build PATH."
fail=1
fi
[ "$fail" -eq 0 ] && echo "no ambient tool acquisition found"
exit "$fail"
- name: gh and jq resolve to the LOCKED nixpkgs, not the registry
run: |
set -euo pipefail
rev="$(nix flake metadata --json | nix run nixpkgs#jq -- -r '.locks.nodes.nixpkgs.locked.rev')"
echo "flake.lock nixpkgs rev: $rev"
# Build each tool straight from the locked rev and require the runner's copy to be
# the SAME store path. This is the assertion that actually distinguishes "pinned"
# from "happens to be in the store" — the registry produces store paths too.
for tool in gh jq; do
want="$(nix build --no-link --print-out-paths "github:nixos/nixpkgs/$rev#$tool" | head -1)"
got="$(nix develop .#cron -c sh -c "command -v $tool")"
echo " $tool: want ${want}/bin/$tool"
echo " $tool: got $got"
case "$got" in
"$want"/bin/"$tool") echo " $tool OK — from the locked nixpkgs" ;;
*) echo "::error::$tool does not come from flake.lock's nixpkgs ($rev)."; exit 1 ;;
esac
done
- name: the cron shell prints nothing on entry
run: |
set -euo pipefail
# The runners tee stdout into a JSONL trace; a shellHook banner corrupts the
# first line. Assert the shell contributes zero bytes of its own.
out="$(nix develop .#cron -c true)"
if [ -n "$out" ]; then
echo "::error::The cron devShell wrote to stdout on entry:"
printf '%s\n' "$out"
exit 1
fi
# And that a payload comes back byte-identical and parses as JSON.
echo '{"type":"system"}' | nix develop .#cron -c jq -e -c '.type' >/dev/null
echo "cron shell is silent on entry; JSON round-trips clean"
# A runner whose closure is missing a tool still WORKS on a developer box, because the
# ambient PATH quietly supplies it — and then behaves differently under cron, where the
# environment is bare. That is the same class of bug as the three above, so it is asserted
# the same way: resolve each tool using ONLY the package's own baked PATH, and require a
# store path. Writing this check is what found flock, getent, hostname and sed missing.
- name: each runner's closure is self-contained
run: |
set -euo pipefail
fail=0
check() {
pkg="$1"; bin="$2"; shift 2
out="$(nix build --no-link --print-out-paths ".#$pkg")"
# writeShellApplication bakes `export PATH="<runtimeInputs>"` into the script.
wp="$(grep -o 'export PATH="[^"]*"' "$out/bin/$bin" | head -1 | sed 's/export PATH="//;s/"$//')"
for t in "$@"; do
r="$(env -i PATH="$wp" /bin/sh -c "command -v $t" 2>/dev/null || true)"
case "$r" in
/nix/store/*) ;;
*) echo "::error::$pkg: '$t' is not in the package closure (resolved: ${r:-not found})"; fail=1 ;;
esac
done
}
# This list is what the SCRIPTS name. Tools the HARNESS execs on the model's behalf
# (`pdftoppm`, `pdfinfo`) are deliberately NOT repeated here — they are declared once, in
# `HARNESS_TOOLS`, and asserted by the preflight step below, so the two cannot drift.
check campaign-run campaign-run \
date uname getent id flock timeout find sort cut tail rm mkdir tee sed grep gh jq pr-review-report
check review-run review-run \
date uname getent id flock timeout find sort cut tail rm mkdir tee sed grep gh jq pr-review-report
check refresh-human-queue refresh-human-queue \
date getent id flock mktemp mv rm git pr-review-report
check design-doctor design-doctor \
date getent id cut flock timeout gh pr-review-report
check backfill-human-queue-history backfill-human-queue-history git wc pr-review-report
check review-queue sort-review-queue pr-review-report
check pr-review-report-sh pr-review-report-sh pr-review-report
[ "$fail" -eq 0 ] && echo "every runner closure is self-contained"
exit "$fail"
# The step above asserts the tools the SCRIPTS name. It structurally cannot see a tool only
# the HARNESS reaches for at read time — nothing in this repo names `pdftoppm`, so no walk
# over declarations could find it, which is how #85 passed a gate that was working correctly.
#
# The three gates below close that, and they are SUBCOMMANDS, not bash. Each one resolves a
# runner's capabilities from that runner's own baked PATH and from nothing else, so neither
# the declaration nor the closure can drift, and nothing that merely happens to be installed
# on this box can satisfy them. The logic lives in `pr-review-report` because it is real
# computation — a PDF generator, a PATH parser, a set difference — and there it is unit
# tested, runnable locally, and mutation-checked like the rest of the pipeline.
- name: each model runner satisfies its own preflight
run: nix run .#pr-review-report -- closure-preflight
# Presence is not capability: a closure can carry `pdftoppm` and still fail on a missing
# shared library or data file, and the harness turns BOTH into the same `isError` with the
# run still exiting 0. This renders a generated PDF end to end with the harness's own argv.
- name: the PDF read path renders, not just resolves
run: nix run .#pr-review-report -- closure-render
# A dependency discovered ONCE must not go missing from the other runner silently. This
# cannot see a capability both runners lack — #85's exact shape — which is why the two
# presence gates above are what actually hold that bug.
- name: the two model runners' capability surfaces match, except where declared
run: nix run .#pr-review-report -- closure-surface
- name: flake check
run: nix flake check --print-build-logs
# The producer's scratch dir (#106) is held together by ONE non-obvious harness fact, and every
# part of it looks redundant to a reader who does not know that fact:
#
# `--add-dir` does NOT make a directory writable by bash output redirection.
#
# Verified against claude 2.1.220: a redirection is a `create` operation, and working-directory
# membership — all that `--add-dir` and `permissions.additionalDirectories` confer — authorises
# `create` only in `acceptEdits` mode. Under `--permission-mode default` the decision falls
# through to an edit-kind ALLOW RULE. So the `--allowedTools "Edit(//...)"` rules sitting next
# to the `--add-dir` flags read like a belt-and-braces duplicate, and deleting one silently
# returns the producer to writing wherever it can — which is how the install dir came to hold six
# weeks of it. The refusal it gets even NAMES the directory as allowed, so the symptom does not
# point at the cause either.
#
# `--add-dir` and the edit rule are two HALVES OF ONE GRANT, which is why the first gate below
# derives the roots from the script rather than pinning a literal: a root that gets only the
# first half refuses redirects while naming itself allowed, and scratch-only left every work
# clone in exactly that state for as long as the scratch grant looked complete (#118).
#
# None of this can be caught by running the code (the run still exits 0; it just quietly litters),
# so it is asserted statically, the same way the ambient-tool-acquisition traps above are.
producer-scratch-dir-is-writable:
name: producer scratch dir is writable
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: every --add-dir root is also an Edit(//...) allow rule
run: |
set -euo pipefail
fail=0
# Read the roots out of the script instead of naming them here, so a third --add-dir
# arrives already covered by this gate rather than needing someone to remember it.
roots="$(grep -Eo -- '--add-dir[[:space:]]+"\$[A-Za-z_][A-Za-z0-9_]*"' campaign-run.sh \
| grep -Eo '\$[A-Za-z_][A-Za-z0-9_]*' | sort -u)"
if [ -z "$roots" ]; then
echo "::error::campaign-run.sh names no --add-dir roots, so this gate can no longer see"
echo "what it guards. If the flags moved, move this check with them."
exit 1
fi
for r in $roots; do
if ! grep -Fq -- "Edit(//$r/**)" campaign-run.sh; then
echo "::error::campaign-run.sh --add-dir's $r but never grants Edit(//$r/**)."
echo "--add-dir grants READS. An output redirection is a create, which needs an"
echo "edit-kind ALLOW RULE, so that root refuses every redirect — with a message that"
echo "names it as allowed. Half-granting \$WORK_DIR that way is #118."
fail=1
fi
done
# The scratch dir needs no rule of its own only because it is INSIDE $WORK_DIR. Move it
# out and the loop above still passes while the dir #106 exists for goes unwritable.
if ! grep -Eq '^SCRATCH_DIR="\$WORK_DIR/' campaign-run.sh; then
echo "::error::SCRATCH_DIR is no longer under \$WORK_DIR, so the Edit(//\$WORK_DIR/**)"
echo "rule no longer covers it and it needs a grant of its own."
fail=1
fi
# `Edit(/abs/**)` is not a stricter spelling of `Edit(//abs/**)` — it is inert. A rule that
# never matches fails exactly like no rule at all, with nothing in any log to say so.
# Scoped to the flag's own line: the block comment above it spells the broken form out.
if grep -- '--allowedTools' campaign-run.sh | grep -Eq 'Edit\(/[^/]'; then
echo "::error::A single-slash Edit(/...) rule in campaign-run.sh is silently inert."
echo "Absolute paths in a path rule need the // prefix: Edit(//abs/path/**)."
fail=1
fi
[ "$fail" -eq 0 ] && echo "every --add-dir root is granted, in the matching form"
exit "$fail"
- name: the scratch path reaches the prompt as a literal
run: |
set -euo pipefail
fail=0
if ! grep -Fq 's#{{SCRATCH_DIR}}#' campaign-run.sh; then
echo "::error::campaign-run.sh stopped substituting {{SCRATCH_DIR}} into the prompt."
fail=1
fi
if ! grep -Fq '{{SCRATCH_DIR}}' campaign-prompt.txt; then
echo "::error::campaign-prompt.txt never names {{SCRATCH_DIR}}, so the run has a scratch"
echo "dir the model is never told about — which is the state #106 was filed against."
fail=1
fi
# The harness refuses any redirect target containing '$' as unevaluable shell expansion,
# so a prompt that writes the redirect through the variable teaches a command that fails.
if grep -Eq '>[[:space:]]*\$SCRATCH_DIR' campaign-prompt.txt; then
echo "::error::campaign-prompt.txt gives \$SCRATCH_DIR as a redirect target. A redirect"
echo "target containing '\$' is refused by the harness — the prompt must use the"
echo "{{SCRATCH_DIR}} placeholder so the model receives the literal path."
fail=1
fi
[ "$fail" -eq 0 ] && echo "prompt receives the literal scratch path"
exit "$fail"
- name: scratch is per-run and reclaimed
run: |
set -euo pipefail
fail=0
# Per-run is what makes the footprint one run's worth. A path without $TS is shared
# between runs, and a shared dir nothing deletes is the pile #106 is about.
if ! grep -Eq '^SCRATCH_DIR=.*\$TS' campaign-run.sh; then
echo "::error::SCRATCH_DIR is no longer per-run (\$TS): runs would share one directory."
fail=1
fi
# …and $TS alone is not unique. The flock is per-install-dir while WORK_DIR defaults to
# $HOME/code, so two installs sharing a WORK_DIR run concurrently and can start in the
# same second — colliding on the path, then deleting each other's scratch on the way out.
if ! grep -Eq '^SCRATCH_DIR=.*\$\$' campaign-run.sh; then
echo "::error::SCRATCH_DIR has no per-process component (\$\$). Two installs sharing a"
echo "WORK_DIR can start in the same second and collide on \$TS alone."
fail=1
fi
if ! grep -Fq 'rm -rf "$SCRATCH_DIR"' campaign-run.sh; then
echo "::error::campaign-run.sh no longer deletes its scratch dir at the end of the run."
fail=1
fi
# …and deleting it from a plain statement is not reclaiming it. A statement at the foot of
# the script runs only when the script REACHES it, which a killed run never does — one
# left scratch/20260728T132223Z-3993293/ behind while the comment beside it said
# "Unconditional" (#118). On the trap, INT/TERM/HUP all reclaim; only SIGKILL and a reboot
# escape, and the age-bounded sweep on the next run's way in is what covers those.
if ! grep -Eq '^trap .*rm -rf "\$SCRATCH_DIR".* EXIT$' campaign-run.sh; then
echo "::error::the scratch reclaim is not installed as an EXIT trap."
echo "A plain statement is skipped by every path that leaves before reaching it,"
echo "which is every way a run gets killed."
fail=1
fi
# `-mtime +1` truncates to whole days, so it leaves a 25-hour-old leftover until it is 48
# hours old. Wrong quietly, never loudly — the sweep still runs and still reports nothing.
if grep -Eq 'find .*scratch.*-mtime' campaign-run.sh; then
echo "::error::The stale-scratch sweep uses -mtime, which rounds to whole days."
echo "Use -mmin +1440 so a leftover is reclaimed once it actually exceeds a day."
fail=1
fi
[ "$fail" -eq 0 ] && echo "scratch is per-run, unique, and reclaimed"
exit "$fail"
- name: a scratch dir that cannot be created stops the run
run: |
set -euo pipefail
# errexit is off in this runner, so an unchecked mkdir falls through on a full disk or a
# permission fault and the model is handed an authorised path that does not exist. Every
# redirect into it then fails — the pre-#106 state, minus the clue that something broke.
if ! grep -Eq 'if ! mkdir -p "\$SCRATCH_DIR"; then' campaign-run.sh; then
echo "::error::campaign-run.sh does not check that the scratch dir was created."
echo "With errexit off, a failed mkdir must end the run before a model invocation."
exit 1
fi
echo "a failed scratch mkdir ends the run"
# review-run.sh carries the same two `--add-dir` flags as the producer and NO `--allowedTools`,
# which is the identical half-grant shape the job above rejects. Here the answer is the opposite
# one: the vetter gets no edit-kind allow rule because it must not write. That reading is only
# true while the deny list holds — deny beats allow, and with `Bash` denied outright a bash
# output redirection is not expressible at all, so a rule would be inert AND would advertise a
# writing vetter. This job is what makes the omission a decision rather than an accident (#118):
# grant the vetter Bash and it fails until someone answers the redirect question on purpose.
vetter-has-no-write-grant:
name: vetter has no write grant
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: review-settings.json denies every write tool
run: |
set -euo pipefail
fail=0
# Without this, a missing jq makes every check below "fail" with a message about the deny
# list — a true report of nothing, aimed at the wrong file.
command -v jq >/dev/null || { echo "::error::this gate needs jq and cannot find it"; exit 1; }
for t in Bash Write Edit NotebookEdit; do
if ! jq -e --arg t "$t" '.permissions.deny | index($t)' review-settings.json >/dev/null; then
echo "::error::review-settings.json no longer denies '$t' outright."
echo "The vetter's inability to write is what makes review-run.sh's missing"
echo "--allowedTools correct. Without this deny it is just a hole."
fail=1
fi
done
[ "$fail" -eq 0 ] && echo "the vetter cannot write by any tool"
exit "$fail"
- name: review-run.sh grants no edit-kind allow rule
run: |
set -euo pipefail
# Comments are stripped first: the flag is NAMED in the comment that explains why it is
# absent, and a gate that cannot tell those apart would fire on its own documentation.
if grep -v '^[[:space:]]*#' review-run.sh | grep -q -- '--allowedTools'; then
echo "::error::review-run.sh passes --allowedTools. The vetter is a reader: pr_checkout"
echo "makes its checkouts server-side and record_verdict is its one write, both outside"
echo "the model's tool surface. An allow rule here is inert against the deny list and"
echo "misdescribes what the vetter does. If Bash was granted, decide this deliberately."
exit 1
fi
echo "the vetter's --add-dir flags confer reads and nothing else"
# A runner reads its prompt whole and re-reads it every turn, so a byte here is
# paid once per turn by every run, and nothing else in CI measures it. The glob is
# wider than the four prompts that exist so a new one is charged rather than
# escaping by not being named; the cap covers those files plus every repo file
# they name. Lowering it is the ratchet; raising it needs a reason in the diff.
prompt-cap:
name: prompt cap
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@a7ad9c4f0c65208097f4d34f3cfa1913b80cce5c # main
- uses: rainlanguage/rainix/.github/actions/prompt-cap@main
with:
paths: "**/*prompt*"
cap: 153919
# The plugin prompts, capped separately from the cron runners above. Same tax,
# different payer: a `/nr` or `/ncc` dispatch reads its agent file whole into a
# fresh window, so every byte is charged once per turn of that agent's run, and
# `**/*prompt*` never matched any of them — the four agents grew uncharged.
# Its own budget rather than a line in the runners': one total lets a cut in
# the cron prompts pay for growth in these, which is the ratchet cancelling
# itself out. The glob takes agents and commands of EVERY plugin so a new
# prompt is charged on arrival instead of escaping by not being named.
plugin-prompt-cap:
name: plugin prompt cap
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@a7ad9c4f0c65208097f4d34f3cfa1913b80cce5c # main
- uses: rainlanguage/rainix/.github/actions/prompt-cap@main
with:
paths: |
plugins/*/agents/*.md
plugins/*/commands/*.md
cap: 121990