Skip to content

Commit 4460c24

Browse files
committed
feat: a trigger may attach a standing instruction to its prompt (issue #60)
`run.instructions` is one line of operator text on a forge trigger, rendered into the USER prompt's envelope: above the fenced data region, below the harness's own numbered steps, and BEFORE the never-merge paragraph. Label, comment and pull_request triggers carried no prompt text at all, so "for this trigger specifically: the tests run with X, this repo's convention is Y" had to be committed into the repo's SKILL.md or pushed into the deployment-wide persona, which applies it to every job everywhere. PLACEMENT IS ARGUED, NOT ASSUMED, because it is the whole of the change. CONST-ISSUE-TEXT-IS-DATA governs event PAYLOADS. This is operator text from a reviewed, git-tracked file, which passes the same mutability test DES-FLOWS-ARE-DATA-PERSONA-IS-CODE already applies to the overlay persona: "Mutability, not the persona/flow label, is the boundary." Inside the fenced data region it would be DOCUMENTED TO BE IGNORED, since dataRegion tells the model everything below its heading must be reported rather than obeyed. In the system prompt it would work and be marginally cheaper per turn, and is still refused: every other member of that layer is read from a fixed file path once at loader build, which is the shape CONST-PERSONA-IN-CACHED-PREFIX's acceptance leans on, and run.task is already contracted user-prompt-only, so two operator text fields with two placements would be an incoherence. It sits BEFORE the never-merge paragraph because later text reads as more specific, and the harness's non-negotiables must be the last thing before the data region rather than something an operator instruction appears to qualify. That costs nothing and forecloses the argument. CONST-ISSUE-TEXT-IS-DATA gains one clause for the region it never described, in the shape of the replay clause run.resume added, written as four checkable parts rather than as a permission. This widens WHO may write an instruction, from the flow author and the deploy-time operator to the same operator per trigger, and not WHAT may become one. dataRegion is UNCHANGED, which is the point: because the text lands in the envelope, the shared export keeps its signature and the new-parameters-go-last rule is honoured with no hole threaded through three sibling forges. They are still edited, all three, because a gitlab trigger whose instruction was silently dropped is the no-op this project refuses. Refused on cron, naming run.task: a local job's prompt IS run.task, with no envelope, no data heading and no fence, so there is no standing region distinct from the task for a second field to occupy, and two fields writing one region with an undefined order would both appear to work. Capped at 2000 characters, refused rather than truncated. The caching argument does NOT justify this and the spec says so: the text is written once, session.prompt() is called once, and at the pin pi-ai attaches cache_control to the LAST USER MESSAGE as well as the system prompt. What justifies it is a context overflow inside a PAID container with no pre-spend signal, and keeping the field in its lane, since anything longer belongs in the flow's SKILL.md or the overlay persona. The refusal names both. Surrounding whitespace is NOT refused, unlike run.image, because whitespace changes what an image REFERENCE means and does not change what prose means; whitespace-only is. No content filtering, and a comment says why: placement is the boundary and the delimiter is defence in depth, so an operator who writes a fake data heading into their own text has forged nothing -- the real heading is emitted after theirs and still opens the real region. There is a test for exactly that. No model-callable path: dispatch_trigger_add/_edit gain no parameter, on run.resume's test rather than f.forge's, because standing text reaching every future job of a trigger is a self-influence channel one confirm dialog does not bound. CONST-ISSUE-TEXT-IS-DATA AMENDED. NEW REQ-PER-TRIGGER-INSTRUCTION and DES-TRIGGER-INSTRUCTION-IN-THE-ENVELOPE (five rejected placements recorded). INT-TRIGGERS-FILE-CONTRACT, INT-CONTAINER-JOB-INPUTS: AMENDED. CONST-PERSONA-IN-CACHED-PREFIX UNCHANGED, checked, and it records WHY the obvious reading is wrong rather than borrowing an argument that does not apply. CONST-TRIGGER-AUTHOR-GATE UNCHANGED, checked: the field changes how a job is prompted, never whether it starts. CONST-ISOLATION-CONTAINER-PER-JOB, INT-CONTAINER-RUNTIME-CONTRACT, CONST-BUDGET-BEFORE-TOKENS, CONST-RETRY-INFRA-ONLY, INT-WEBHOOK-PAYLOAD-SUBSET, REQ-PER-TRIGGER-SKILLS, DES-FLOWS-ARE-DATA-PERSONA-IS-CODE: UNCHANGED, checked. Mutation-checked: moving the block below the never-merge paragraph turns the last-word test red, and fencing it turns the not-fenced test red. 2069 tests, 0 failures, 0 skipped in the CI posture. Signed-off-by: Rob Boerman <robboerman@live.nl>
1 parent b0d1856 commit 4460c24

29 files changed

Lines changed: 523 additions & 58 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ or what it costs):
180180
([`docs/global-pi-overlay.md`](docs/global-pi-overlay.md)).
181181
- `"replicas": 2` (GitHub only) races independent sandboxes on the same event and opens one PR per
182182
replica. Each replica spends its own budget slot ([`docs/replicas.md`](docs/replicas.md)).
183+
- `"instructions"` attaches one line of standing text to that trigger (forge triggers only, up to 2000
184+
characters). It reaches the job's prompt above the issue or PR text, labelled as coming from you rather
185+
than from the issue, so "the tests run with pnpm here" applies to every run of that trigger without
186+
being committed to the repo or pushed into the deployment wide persona. Cron triggers use `task`
187+
instead, which is the same text in the same place.
183188
- `"resume": true` continues the session that opened the PR ([`docs/sessions.md`](docs/sessions.md)).
184189
- `"github": true` on a cron trigger mints the same per-job GitHub token the webhook path gets, so a
185190
scheduled flow can use `gh`.

admin/src/read-model.mjs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,10 @@ export function normalizeTriggerForDisplay(entry) {
696696
// `image`, and shown for the same reason: which skills a job loads IS what the agent can do. `null` is
697697
// the none sentinel, matching this function's own convention.
698698
const skillsDir = typeof run.skillsDir === "string" && run.skillsDir.trim() !== "" ? run.skillsDir : null;
699+
// Whether this trigger attaches operator standing text (REQ-PER-TRIGGER-INSTRUCTION). A BOOLEAN, not
700+
// the text: the panel line must say that a trigger carries one, and the text itself may be 2000
701+
// characters. The detail view is where the words belong.
702+
const instructions = typeof run.instructions === "string" && run.instructions.trim() !== "";
699703
// An opt-IN, so `=== true` and not `!== false` -- the opposite test from `packages` directly above, and
700704
// the difference is the whole point. Getting this polarity wrong is the defect 0.1.4 shipped a fix for:
701705
// the riskiest triggers rendered with no badge and no warning, quiet exactly where the risk was.
@@ -721,12 +725,13 @@ export function normalizeTriggerForDisplay(entry) {
721725
packages,
722726
image,
723727
skillsDir,
728+
instructions,
724729
resume,
725730
};
726731
case "label":
727-
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, packages, image, skillsDir, resume, replicas, forge };
732+
return { type: "label", any: normalizeSelector(on.any), all: normalizeSelector(on.all), none: normalizeSelector(on.none), flow, packages, image, skillsDir, instructions, resume, replicas, forge };
728733
case "comment":
729-
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, packages, image, skillsDir, resume, replicas, forge };
734+
return { type: "comment", phrase: typeof on.phrase === "string" ? on.phrase : null, flow, packages, image, skillsDir, instructions, resume, replicas, forge };
730735
case "pull_request":
731736
return {
732737
type: "pull_request",
@@ -738,6 +743,7 @@ export function normalizeTriggerForDisplay(entry) {
738743
packages,
739744
image,
740745
skillsDir,
746+
instructions,
741747
resume,
742748
replicas,
743749
forge,

admin/src/render.mjs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,22 +181,25 @@ function triggerLine(t) {
181181
// the full path lives in the trigger detail view, where the panel is the operator's own session on their
182182
// own host and a path discloses nothing new.
183183
const skl = t?.skillsDir ? ` [skills ${String(t.skillsDir).split(/[\\/]/).filter(Boolean).pop()}]` : "";
184+
// A trigger that puts operator standing text into every job's prompt says so. Same doctrine as the
185+
// badges above: a trigger that changes what the agent is told must never render like one that does not.
186+
const ins = t?.instructions === true ? " [instructions]" : "";
184187
const res = t?.resume === true ? " [resume]" : "";
185188
// A trigger that turns one delivery into N paid runs says so (REQ-REPLICA-RUNS). Same class of badge as
186189
// [resume]: not a preference an operator can skim past, but the field that multiplies the bill. Absent on
187190
// an unreplicated trigger, appended last, so every existing line is byte-identical.
188191
const rep = t?.replicas > 1 ? ` [x${t.replicas}]` : "";
189192
switch (t?.type) {
190193
case "cron":
191-
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"}${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${res}${rep}`;
194+
return `cron ${t.id ?? "-"} ${t.pattern ?? "-"}${t.folder ?? "-"}/${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
192195
case "label":
193-
return `label ${ruleClauses(t) || "(no selector)"}${flow}${forge}${pkgs}${img}${skl}${res}${rep}`;
196+
return `label ${ruleClauses(t) || "(no selector)"}${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
194197
case "comment":
195-
return `comment "${t.phrase ?? "-"}" → ${flow}${forge}${pkgs}${img}${skl}${res}${rep}`;
198+
return `comment "${t.phrase ?? "-"}" → ${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
196199
case "pull_request": {
197200
const clauses = ruleClauses(t);
198201
const action = `action[${(t.action ?? []).join(",")}]`;
199-
return `pull_request ${action}${clauses ? ` ${clauses}` : ""}${flow}${forge}${pkgs}${img}${skl}${res}${rep}`;
202+
return `pull_request ${action}${clauses ? ` ${clauses}` : ""}${flow}${forge}${pkgs}${img}${skl}${ins}${res}${rep}`;
200203
}
201204
default:
202205
return "(unknown trigger)";

admin/test/read-model.test.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,10 @@ test("readTriggers normalizes each on.type into its discriminated display record
448448
const res = readTriggers({ triggersPath: "/x/triggers.json", fs: fakeFs(files) });
449449
// Every entry omits `run.packages`, and packages is an OPT-OUT -- so all four normalize to `true`.
450450
assert.deepEqual(res.triggers, [
451-
{ type: "cron", id: "nightly", pattern: "0 3 * * *", folder: "/srv/p", flow: "tidy", model: null, packages: true, image: null, skillsDir: null, resume: false },
452-
{ type: "label", any: ["pi:frontend"], all: [], none: ["wontfix"], flow: "frontend-fix", packages: true, image: null, skillsDir: null, resume: false, replicas: null, forge: "github" },
453-
{ type: "comment", phrase: "@pi", flow: "fix", packages: true, image: null, skillsDir: null, resume: false, replicas: null, forge: "github" },
454-
{ type: "pull_request", action: ["labeled"], any: ["pi:review"], all: [], none: [], flow: "review", packages: true, image: null, skillsDir: null, resume: false, replicas: null, forge: "github" },
451+
{ type: "cron", id: "nightly", pattern: "0 3 * * *", folder: "/srv/p", flow: "tidy", model: null, packages: true, image: null, skillsDir: null, instructions: false, resume: false },
452+
{ type: "label", any: ["pi:frontend"], all: [], none: ["wontfix"], flow: "frontend-fix", packages: true, image: null, skillsDir: null, instructions: false, resume: false, replicas: null, forge: "github" },
453+
{ type: "comment", phrase: "@pi", flow: "fix", packages: true, image: null, skillsDir: null, instructions: false, resume: false, replicas: null, forge: "github" },
454+
{ type: "pull_request", action: ["labeled"], any: ["pi:review"], all: [], none: [], flow: "review", packages: true, image: null, skillsDir: null, instructions: false, resume: false, replicas: null, forge: "github" },
455455
]);
456456
});
457457

@@ -516,7 +516,7 @@ test("readTriggers skips an entry that is not a usable { on, run } object (viewe
516516
}),
517517
};
518518
const res = readTriggers({ triggersPath: "/x/triggers.json", fs: fakeFs(files) });
519-
assert.deepEqual(res.triggers, [{ type: "label", any: ["pi:frontend"], all: [], none: [], flow: "frontend-fix", packages: true, image: null, skillsDir: null, resume: false, replicas: null, forge: "github" }]);
519+
assert.deepEqual(res.triggers, [{ type: "label", any: ["pi:frontend"], all: [], none: [], flow: "frontend-fix", packages: true, image: null, skillsDir: null, instructions: false, resume: false, replicas: null, forge: "github" }]);
520520
});
521521

522522
test("readTriggers returns { invalid } when there is no triggers array", () => {

docs/workflows.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,27 @@ copied at all, because pi would register no skill for it. A skill past the size
8989
refuses the job with the cap named, which is deliberate: a partly copied skill is a skill whose
9090
instructions point at files that are not there.
9191

92+
## A standing instruction, and what it is not
93+
94+
`run.instructions` on a forge trigger attaches one line of your own text to every job that trigger
95+
starts. It reaches the prompt above the issue or pull request text, labelled as coming from you, so the
96+
agent can tell it apart from whatever a stranger wrote in the issue.
97+
98+
It is the smallest of three places instructions can live, and picking the right one matters more than the
99+
feature does:
100+
101+
| Where | Scope | Reviewed by | Reach for it when |
102+
|---|---|---|---|
103+
| `run.instructions` | one trigger | your edit to `triggers.json` | a sentence or two, specific to this pairing of event and flow |
104+
| the flow's `SKILL.md` | every job that runs that flow, on that repo | a merge on the target repo | the instructions ARE the procedure |
105+
| the overlay's `APPEND_SYSTEM.md` | every job, every repo, every flow | `import-pi`, at deploy time | house style that outlives any one trigger |
106+
107+
The 2000 character cap is there to keep the first row from quietly becoming the second. If your standing
108+
text no longer fits on a screen, it is a flow, and it belongs in a file someone reviews.
109+
110+
Cron triggers do not take it, and use `task` instead. That is not an omission: a scheduled job's whole
111+
prompt IS its `task`, so a second field would write the same region with no defined order between them.
112+
92113
## The structured case: stage a workflow extension
93114

94115
**Nothing installs at job time.** Job containers run with `PI_OFFLINE=1` and, on the shipped image, no

receiver/src/config.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,13 @@ function loadTriggers(env, readFile, fileExists) {
275275
knownFlows.add(run.flow);
276276
const group = groups[run.kind];
277277
if (on.type === "label") {
278-
group.label.push({ index, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, packages: run.packages, image: run.image, skillsDir: run.skillsDir, resume: run.resume, replicas: run.replicas, repository: run.repository });
278+
group.label.push({ index, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, packages: run.packages, image: run.image, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, replicas: run.replicas, repository: run.repository });
279279
} else if (on.type === "comment") {
280-
group.comment = { index, phrase: on.phrase, defaultFlow: run.flow, packages: run.packages, image: run.image, skillsDir: run.skillsDir, resume: run.resume, replicas: run.replicas, repository: run.repository }; // parseTriggers guarantees at most one per forge
280+
group.comment = { index, phrase: on.phrase, defaultFlow: run.flow, packages: run.packages, image: run.image, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, replicas: run.replicas, repository: run.repository }; // parseTriggers guarantees at most one per forge
281281
} else if (on.type === "pull_request") {
282282
// `reviewStates` is null rather than an empty Set when unnarrowed: the filter tests it for
283283
// presence, and an empty Set would read as "no verdict matches" and silently refuse everything.
284-
group.pullRequest.push({ index, actions: new Set(on.action), reviewStates: on.reviewState ? new Set(on.reviewState) : null, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, packages: run.packages, image: run.image, skillsDir: run.skillsDir, resume: run.resume, replicas: run.replicas });
284+
group.pullRequest.push({ index, actions: new Set(on.action), reviewStates: on.reviewState ? new Set(on.reviewState) : null, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, packages: run.packages, image: run.image, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, replicas: run.replicas });
285285
}
286286
}
287287

receiver/src/filter-azure.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export function filterAzure(subset, triggers, knownFlows, selfId, authorized, de
110110
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
111111
// basename(folder) restraint already exists to prevent.
112112
...(resolved.skillsDir !== undefined ? { skillsDir: resolved.skillsDir } : {}),
113+
...(resolved.instructions !== undefined ? { instructions: resolved.instructions } : {}),
113114
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
114115
trigger: {
115116
event,
@@ -171,6 +172,7 @@ function matchLabelRules(subset, triggers, labels, action) {
171172
packages: rule.packages,
172173
image: rule.image,
173174
skillsDir: rule.skillsDir,
175+
instructions: rule.instructions,
174176
resume: rule.resume,
175177
matched: { index: rule.index, type: "label", label: matchedLabel(L, rule.predicate) },
176178
target: { type: "issue", number: subset.target?.number, title: subset.target?.title, body: subset.target?.body },
@@ -200,6 +202,7 @@ function routeComment(subset, triggers, knownFlows, targetType) {
200202
packages: triggers.comment.packages,
201203
image: triggers.comment.image,
202204
skillsDir: triggers.comment.skillsDir,
205+
instructions: triggers.comment.instructions,
203206
resume: triggers.comment.resume,
204207
matched: { index: triggers.comment.index, type: "comment", phrase },
205208
// No author_association: Azure has none, and the authority that admitted this comment was resolved
@@ -227,6 +230,7 @@ function routePullRequest(subset, triggers, action) {
227230
packages: rule.packages,
228231
image: rule.image,
229232
skillsDir: rule.skillsDir,
233+
instructions: rule.instructions,
230234
resume: rule.resume,
231235
matched: { index: rule.index, type: "pull_request", action },
232236
target: {

receiver/src/filter-forgejo.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export function filterForgejo(eventName, subset, triggers, knownFlows, selfId, a
112112
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
113113
// basename(folder) restraint already exists to prevent.
114114
...(resolved.skillsDir !== undefined ? { skillsDir: resolved.skillsDir } : {}),
115+
...(resolved.instructions !== undefined ? { instructions: resolved.instructions } : {}),
115116
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
116117
trigger: {
117118
event: eventName,
@@ -139,6 +140,7 @@ function routeIssueLabel(subset, triggers) {
139140
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
140141
image: rule.image,
141142
skillsDir: rule.skillsDir,
143+
instructions: rule.instructions,
142144
resume: rule.resume,
143145
matched: { index: rule.index, type: "label", label: matchedLabel(L, rule.predicate) },
144146
target: { type: "issue", number: subset.issue?.number, title: subset.issue?.title, body: subset.issue?.body },
@@ -173,6 +175,7 @@ function routeComment(subset, triggers, knownFlows) {
173175
packages: triggers.comment.packages,
174176
image: triggers.comment.image,
175177
skillsDir: triggers.comment.skillsDir,
178+
instructions: triggers.comment.instructions,
176179
resume: triggers.comment.resume,
177180
matched: { index: triggers.comment.index, type: "comment", phrase },
178181
// The invoking comment rides on the trigger. No author_association: Forgejo has none, and the
@@ -198,6 +201,7 @@ function routePullRequest(subset, triggers, action) {
198201
packages: rule.packages,
199202
image: rule.image,
200203
skillsDir: rule.skillsDir,
204+
instructions: rule.instructions,
201205
resume: rule.resume,
202206
matched: { index: rule.index, type: "pull_request", action },
203207
target: {

receiver/src/filter-gitlab.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export function filterGitLab(subset, triggers, knownFlows, selfId, authorized, d
103103
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
104104
// basename(folder) restraint already exists to prevent.
105105
...(resolved.skillsDir !== undefined ? { skillsDir: resolved.skillsDir } : {}),
106+
...(resolved.instructions !== undefined ? { instructions: resolved.instructions } : {}),
106107
// Conditional like packages/image, and for the same reason: an unflagged job's data must stay
107108
// byte-identical to today's, so the key is absent rather than present-and-undefined.
108109
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
@@ -137,6 +138,7 @@ function routeLabel(subset, triggers, targetType) {
137138
packages: rule.packages,
138139
image: rule.image,
139140
skillsDir: rule.skillsDir,
141+
instructions: rule.instructions,
140142
resume: rule.resume,
141143
matched: { index: rule.index, type: "label", label: matchedLabel(added, rule.predicate) },
142144
target: buildTarget(subset, targetType),
@@ -195,6 +197,7 @@ function mrResult(subset, rule, matched) {
195197
packages: rule.packages,
196198
image: rule.image,
197199
skillsDir: rule.skillsDir,
200+
instructions: rule.instructions,
198201
resume: rule.resume,
199202
matched,
200203
target: buildTarget(subset, "pull_request"),
@@ -230,6 +233,7 @@ function routeNote(subset, triggers, knownFlows) {
230233
packages: triggers.comment.packages,
231234
image: triggers.comment.image,
232235
skillsDir: triggers.comment.skillsDir,
236+
instructions: triggers.comment.instructions,
233237
resume: triggers.comment.resume,
234238
matched: { index: triggers.comment.index, type: "comment", phrase },
235239
target: buildTarget(subset, targetType),

receiver/src/filter.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export function filter(eventName, subset, cfg, selfId, deliveryId) {
116116
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
117117
// basename(folder) restraint already exists to prevent.
118118
...(resolved.skillsDir !== undefined ? { skillsDir: resolved.skillsDir } : {}),
119+
...(resolved.instructions !== undefined ? { instructions: resolved.instructions } : {}),
119120
// Conditional like packages/image, and for the same reason: an unflagged job's data must stay
120121
// byte-identical to today's, so the key is absent rather than present-and-undefined.
121122
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
@@ -159,6 +160,7 @@ function routeIssueLabel(subset, triggers) {
159160
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
160161
image: rule.image,
161162
skillsDir: rule.skillsDir,
163+
instructions: rule.instructions,
162164
resume: rule.resume,
163165
replicas: rule.replicas,
164166
matched: { index: rule.index, type: "label", label: matchedLabel(L, rule.predicate) },
@@ -204,6 +206,7 @@ function routeComment(subset, triggers, knownFlows) {
204206
packages: triggers.comment.packages,
205207
image: triggers.comment.image,
206208
skillsDir: triggers.comment.skillsDir,
209+
instructions: triggers.comment.instructions,
207210
resume: triggers.comment.resume,
208211
replicas: triggers.comment.replicas,
209212
matched: { index: triggers.comment.index, type: "comment", phrase },
@@ -297,6 +300,7 @@ function routePullRequest(subset, triggers, action) {
297300
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
298301
image: rule.image,
299302
skillsDir: rule.skillsDir,
303+
instructions: rule.instructions,
300304
resume: rule.resume,
301305
replicas: rule.replicas,
302306
matched: { index: rule.index, type: "pull_request", action },

0 commit comments

Comments
 (0)