@edgehero/pi-dispatch-receiver 4.1.0 → 4.3.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/package.json +2 -2
- package/src/config.mjs +5 -5
- package/src/filter-azure.mjs +24 -4
- package/src/filter-forgejo.mjs +28 -6
- package/src/filter-gitlab.mjs +28 -6
- package/src/filter.mjs +26 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgehero/pi-dispatch-receiver",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The pi-dispatch trigger edge: verifies GitHub, GitLab, Forgejo and Azure DevOps webhooks, or polls GitHub with no public URL, and queues one pi coding agent job per event (per replica, if set) for the worker.",
|
|
6
6
|
"keywords": [
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@octokit/webhooks": "14.2.0",
|
|
51
|
-
"@edgehero/pi-dispatch": "^4.
|
|
51
|
+
"@edgehero/pi-dispatch": "^4.3.0",
|
|
52
52
|
"bullmq": "5.80.4",
|
|
53
53
|
"ioredis": "5.11.1"
|
|
54
54
|
}
|
package/src/config.mjs
CHANGED
|
@@ -318,9 +318,9 @@ function loadTriggers(env, readFile, fileExists) {
|
|
|
318
318
|
}
|
|
319
319
|
const group = groups[run.kind];
|
|
320
320
|
if (on.type === "label") {
|
|
321
|
-
group.label.push({ index, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository });
|
|
321
|
+
group.label.push({ index, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, awaitInput: run.awaitInput, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository });
|
|
322
322
|
} else if (on.type === "comment") {
|
|
323
|
-
group.comment = { index, phrase: on.phrase, defaultFlow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository }; // parseTriggers guarantees at most one per forge
|
|
323
|
+
group.comment = { index, phrase: on.phrase, defaultFlow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, awaitInput: run.awaitInput, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository }; // parseTriggers guarantees at most one per forge
|
|
324
324
|
} else if (on.type === "pull_request") {
|
|
325
325
|
// The close-only split (issue #231): a close rule routes through the close gate (the CLOSER's
|
|
326
326
|
// resolved authority) and every other PR rule through the author gate, so they live in separate
|
|
@@ -329,17 +329,17 @@ function loadTriggers(env, readFile, fileExists) {
|
|
|
329
329
|
// split reads the SHARED close-word table, never a re-typed word.
|
|
330
330
|
const closeWord = PR_CLOSE_ACTIONS[run.kind];
|
|
331
331
|
if (closeWord !== undefined && on.action.includes(closeWord)) {
|
|
332
|
-
group.prClose.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
332
|
+
group.prClose.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, awaitInput: run.awaitInput, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
333
333
|
} else {
|
|
334
334
|
// `reviewStates` is null rather than an empty Set when unnarrowed: the filter tests it for
|
|
335
335
|
// presence, and an empty Set would read as "no verdict matches" and silently refuse everything.
|
|
336
|
-
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, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
336
|
+
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, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, awaitInput: run.awaitInput, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
337
337
|
}
|
|
338
338
|
} else if (on.type === "issue") {
|
|
339
339
|
// The close-trigger kind (issue #231). No predicate and no reviewStates by construction (the
|
|
340
340
|
// loader refused both), and `number`/`once` ride conditionally-present exactly as the loader
|
|
341
341
|
// normalized them -- the filter's close route reads them, nothing else does.
|
|
342
|
-
group.issue.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
342
|
+
group.issue.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, awaitInput: run.awaitInput, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
|
package/src/filter-azure.mjs
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { createHash } from "node:crypto";
|
|
28
28
|
import { escapeRegExp, firstMatchingRule, labelSet, matchedLabel, matchesRule } from "./predicate.mjs";
|
|
29
29
|
import { PR_COMMENT_EVENT, PR_EVENTS, WORK_ITEM_EVENTS } from "./azure-subset.mjs";
|
|
30
|
+
import { hasParkedLabel } from "@edgehero/pi-dispatch/parked";
|
|
30
31
|
|
|
31
32
|
/** The `pull_request` actions a trigger may name, mirrored from the loader's azure vocabulary. */
|
|
32
33
|
export const PR_ACTION_FOR = {
|
|
@@ -117,6 +118,9 @@ export function filterAzure(subset, triggers, knownFlows, selfId, authorized, de
|
|
|
117
118
|
...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
|
|
118
119
|
// #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
|
|
119
120
|
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
121
|
+
// REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
|
|
122
|
+
// the job without its servers.
|
|
123
|
+
...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
|
|
120
124
|
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
121
125
|
// that named only a model would have its provider's absence decide whether the model survives,
|
|
122
126
|
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
@@ -140,6 +144,11 @@ export function filterAzure(subset, triggers, knownFlows, selfId, authorized, de
|
|
|
140
144
|
...(resolved.waitFor !== undefined ? { waitFor: resolved.waitFor } : {}),
|
|
141
145
|
...(resolved.instructions !== undefined ? { instructions: resolved.instructions } : {}),
|
|
142
146
|
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
|
|
147
|
+
// REQ-AWAIT-INPUT (issue #625): whether the job may end awaiting a person's answer. Present only when armed,
|
|
148
|
+
// resume's rule, so an unflagged job's data stays byte-identical.
|
|
149
|
+
...(resolved.awaitInput === true ? { awaitInput: true } : {}),
|
|
150
|
+
// A phrase-free reply to a parked issue (issue #625): the worker gates it on the `parked` sidecar before any spend.
|
|
151
|
+
...(resolved.parkedReply === true ? { parkedReply: true } : {}),
|
|
143
152
|
// How many independent sandboxes race this flow (REQ-REPLICA-RUNS). At JOB level and conditional for the
|
|
144
153
|
// reasons filter.mjs states in full: receiver.mjs reads this to decide how many times to enqueue, and an
|
|
145
154
|
// unflagged job's data must stay byte-identical to today's.
|
|
@@ -203,7 +212,7 @@ function matchLabelRules(subset, triggers, labels, action) {
|
|
|
203
212
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
204
213
|
repository: rule.repository,
|
|
205
214
|
packages: rule.packages,
|
|
206
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
215
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
207
216
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
208
217
|
skillsDir: rule.skillsDir,
|
|
209
218
|
secrets: rule.secrets,
|
|
@@ -211,6 +220,7 @@ function matchLabelRules(subset, triggers, labels, action) {
|
|
|
211
220
|
waitFor: rule.waitFor,
|
|
212
221
|
instructions: rule.instructions,
|
|
213
222
|
resume: rule.resume,
|
|
223
|
+
awaitInput: rule.awaitInput,
|
|
214
224
|
replicas: rule.replicas,
|
|
215
225
|
matched: { index: rule.index, type: "label", label: matchedLabel(L, rule.predicate) },
|
|
216
226
|
target: { type: "issue", number: subset.target?.number, title: subset.target?.title, body: subset.target?.body },
|
|
@@ -221,7 +231,14 @@ function matchLabelRules(subset, triggers, labels, action) {
|
|
|
221
231
|
function routeComment(subset, triggers, knownFlows, targetType) {
|
|
222
232
|
const phrase = triggers.comment?.phrase;
|
|
223
233
|
const body = subset.comment;
|
|
224
|
-
|
|
234
|
+
const phraseHit = typeof phrase === "string" && typeof body === "string" && body.includes(phrase);
|
|
235
|
+
// A reply WITHOUT the phrase resumes a parked issue (issue #625, parked.mjs): only on an issue (a pull request's key
|
|
236
|
+
// needs its head branch, which costs a token to read, so its reply keeps the phrase), only when the comment
|
|
237
|
+
// trigger arms run.resume and is not a command, and only when the forge says the issue carries the marker. The
|
|
238
|
+
// author gate above already ran: this widens WHICH comments start a job, never WHO may. The marker is a hint; the
|
|
239
|
+
// worker refuses the job before any spend unless the key's `parked` sidecar exists.
|
|
240
|
+
const parkedReply = !phraseHit && typeof phrase === "string" && typeof body === "string" && triggers.comment?.resume === true && triggers.comment?.command === undefined && targetType === "issue" && hasParkedLabel(subset.tags);
|
|
241
|
+
if (!phraseHit && !parkedReply) {
|
|
225
242
|
return { enqueue: false, reason: "no-trigger-phrase" };
|
|
226
243
|
}
|
|
227
244
|
// On a COMMAND rule (issue #189) the flow-resolution block below -- default flow, the `<phrase> <flow>`
|
|
@@ -248,7 +265,7 @@ function routeComment(subset, triggers, knownFlows, targetType) {
|
|
|
248
265
|
...(command !== undefined ? { command } : { flow }),
|
|
249
266
|
repository: triggers.comment.repository,
|
|
250
267
|
packages: triggers.comment.packages,
|
|
251
|
-
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
|
|
268
|
+
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
|
|
252
269
|
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
253
270
|
skillsDir: triggers.comment.skillsDir,
|
|
254
271
|
secrets: triggers.comment.secrets,
|
|
@@ -256,8 +273,10 @@ function routeComment(subset, triggers, knownFlows, targetType) {
|
|
|
256
273
|
waitFor: triggers.comment.waitFor,
|
|
257
274
|
instructions: triggers.comment.instructions,
|
|
258
275
|
resume: triggers.comment.resume,
|
|
276
|
+
awaitInput: triggers.comment.awaitInput,
|
|
259
277
|
replicas: triggers.comment.replicas,
|
|
260
278
|
matched: { index: triggers.comment.index, type: "comment", phrase },
|
|
279
|
+
...(parkedReply && { parkedReply: true }),
|
|
261
280
|
// No author_association: Azure has none, and the authority that admitted this comment was resolved
|
|
262
281
|
// from the graph, not read off the body.
|
|
263
282
|
comment: { body },
|
|
@@ -282,7 +301,7 @@ function routePullRequest(subset, triggers, action) {
|
|
|
282
301
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
283
302
|
repository: rule.repository,
|
|
284
303
|
packages: rule.packages,
|
|
285
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
304
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
286
305
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
287
306
|
skillsDir: rule.skillsDir,
|
|
288
307
|
secrets: rule.secrets,
|
|
@@ -290,6 +309,7 @@ function routePullRequest(subset, triggers, action) {
|
|
|
290
309
|
waitFor: rule.waitFor,
|
|
291
310
|
instructions: rule.instructions,
|
|
292
311
|
resume: rule.resume,
|
|
312
|
+
awaitInput: rule.awaitInput,
|
|
293
313
|
replicas: rule.replicas,
|
|
294
314
|
matched: { index: rule.index, type: "pull_request", action },
|
|
295
315
|
target: {
|
package/src/filter-forgejo.mjs
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import { escapeRegExp, firstMatchingRule, labelSet, matchedLabel, matchesRule } from "./predicate.mjs";
|
|
37
37
|
import { findCloseRule } from "./close.mjs";
|
|
38
38
|
import { isRecognizedAction, mapAction } from "./forgejo-subset.mjs";
|
|
39
|
+
import { hasParkedLabel } from "@edgehero/pi-dispatch/parked";
|
|
39
40
|
|
|
40
41
|
const LABEL_ACTIONS = new Set(["opened", "labeled", "reopened"]);
|
|
41
42
|
export const PR_ACTIONS = new Set(["labeled", "opened", "synchronize", "reopened"]);
|
|
@@ -131,6 +132,9 @@ export function filterForgejo(eventName, subset, triggers, knownFlows, selfId, a
|
|
|
131
132
|
...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
|
|
132
133
|
// #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
|
|
133
134
|
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
135
|
+
// REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
|
|
136
|
+
// the job without its servers.
|
|
137
|
+
...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
|
|
134
138
|
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
135
139
|
// that named only a model would have its provider's absence decide whether the model survives,
|
|
136
140
|
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
@@ -154,6 +158,11 @@ export function filterForgejo(eventName, subset, triggers, knownFlows, selfId, a
|
|
|
154
158
|
...(resolved.waitFor !== undefined ? { waitFor: resolved.waitFor } : {}),
|
|
155
159
|
...(resolved.instructions !== undefined ? { instructions: resolved.instructions } : {}),
|
|
156
160
|
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
|
|
161
|
+
// REQ-AWAIT-INPUT (issue #625): whether the job may end awaiting a person's answer. Present only when armed,
|
|
162
|
+
// resume's rule, so an unflagged job's data stays byte-identical.
|
|
163
|
+
...(resolved.awaitInput === true ? { awaitInput: true } : {}),
|
|
164
|
+
// A phrase-free reply to a parked issue (issue #625): the worker gates it on the `parked` sidecar before any spend.
|
|
165
|
+
...(resolved.parkedReply === true ? { parkedReply: true } : {}),
|
|
157
166
|
// How many independent sandboxes race this flow (REQ-REPLICA-RUNS). At JOB level and conditional for the
|
|
158
167
|
// reasons filter.mjs states in full: receiver.mjs reads this to decide how many times to enqueue, and an
|
|
159
168
|
// unflagged job's data must stay byte-identical to today's.
|
|
@@ -183,7 +192,7 @@ function routeIssueLabel(subset, triggers) {
|
|
|
183
192
|
// A command rule (issue #189) skips flow resolution entirely: the label match IS the dispatch.
|
|
184
193
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
185
194
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
186
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
195
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
187
196
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
188
197
|
skillsDir: rule.skillsDir,
|
|
189
198
|
secrets: rule.secrets,
|
|
@@ -191,6 +200,7 @@ function routeIssueLabel(subset, triggers) {
|
|
|
191
200
|
waitFor: rule.waitFor,
|
|
192
201
|
instructions: rule.instructions,
|
|
193
202
|
resume: rule.resume,
|
|
203
|
+
awaitInput: rule.awaitInput,
|
|
194
204
|
replicas: rule.replicas,
|
|
195
205
|
matched: { index: rule.index, type: "label", label: matchedLabel(L, rule.predicate) },
|
|
196
206
|
target: { type: "issue", number: subset.issue?.number, title: subset.issue?.title, body: subset.issue?.body },
|
|
@@ -201,7 +211,14 @@ function routeIssueLabel(subset, triggers) {
|
|
|
201
211
|
function routeComment(subset, triggers, knownFlows) {
|
|
202
212
|
const phrase = triggers.comment?.phrase;
|
|
203
213
|
const body = subset.comment?.body;
|
|
204
|
-
|
|
214
|
+
const phraseHit = typeof phrase === "string" && typeof body === "string" && body.includes(phrase);
|
|
215
|
+
// A reply WITHOUT the phrase resumes a parked issue (issue #625, parked.mjs): only on an issue (a pull request's key
|
|
216
|
+
// needs its head branch, which costs a token to read, so its reply keeps the phrase), only when the comment
|
|
217
|
+
// trigger arms run.resume and is not a command, and only when the forge says the issue carries the marker. The
|
|
218
|
+
// author gate above already ran: this widens WHICH comments start a job, never WHO may. The marker is a hint; the
|
|
219
|
+
// worker refuses the job before any spend unless the key's `parked` sidecar exists.
|
|
220
|
+
const parkedReply = !phraseHit && typeof phrase === "string" && typeof body === "string" && triggers.comment?.resume === true && triggers.comment?.command === undefined && subset.isPull !== true && hasParkedLabel(subset.issue?.labels);
|
|
221
|
+
if (!phraseHit && !parkedReply) {
|
|
205
222
|
return { enqueue: false, reason: "no-trigger-phrase" };
|
|
206
223
|
}
|
|
207
224
|
// On a COMMAND rule (issue #189) the flow-resolution block below -- default flow, the `<phrase> <flow>`
|
|
@@ -233,7 +250,7 @@ function routeComment(subset, triggers, knownFlows) {
|
|
|
233
250
|
enqueue: true,
|
|
234
251
|
...(command !== undefined ? { command } : { flow }),
|
|
235
252
|
packages: triggers.comment.packages,
|
|
236
|
-
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
|
|
253
|
+
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
|
|
237
254
|
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
238
255
|
skillsDir: triggers.comment.skillsDir,
|
|
239
256
|
secrets: triggers.comment.secrets,
|
|
@@ -241,8 +258,10 @@ function routeComment(subset, triggers, knownFlows) {
|
|
|
241
258
|
waitFor: triggers.comment.waitFor,
|
|
242
259
|
instructions: triggers.comment.instructions,
|
|
243
260
|
resume: triggers.comment.resume,
|
|
261
|
+
awaitInput: triggers.comment.awaitInput,
|
|
244
262
|
replicas: triggers.comment.replicas,
|
|
245
263
|
matched: { index: triggers.comment.index, type: "comment", phrase },
|
|
264
|
+
...(parkedReply && { parkedReply: true }),
|
|
246
265
|
// The invoking comment rides on the trigger. No author_association: Forgejo has none, and the
|
|
247
266
|
// authority that admitted this comment was resolved from the API, not read off the body.
|
|
248
267
|
comment: { body: subset.comment.body },
|
|
@@ -265,7 +284,7 @@ function routePullRequest(subset, triggers, action) {
|
|
|
265
284
|
// A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
|
|
266
285
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
267
286
|
packages: rule.packages,
|
|
268
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
287
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
269
288
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
270
289
|
skillsDir: rule.skillsDir,
|
|
271
290
|
secrets: rule.secrets,
|
|
@@ -273,6 +292,7 @@ function routePullRequest(subset, triggers, action) {
|
|
|
273
292
|
waitFor: rule.waitFor,
|
|
274
293
|
instructions: rule.instructions,
|
|
275
294
|
resume: rule.resume,
|
|
295
|
+
awaitInput: rule.awaitInput,
|
|
276
296
|
replicas: rule.replicas,
|
|
277
297
|
matched: { index: rule.index, type: "pull_request", action },
|
|
278
298
|
target: {
|
|
@@ -321,7 +341,7 @@ function routeIssueClose(subset, triggers, authorized) {
|
|
|
321
341
|
enqueue: true,
|
|
322
342
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
323
343
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
324
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
344
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
325
345
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
326
346
|
skillsDir: rule.skillsDir,
|
|
327
347
|
secrets: rule.secrets,
|
|
@@ -329,6 +349,7 @@ function routeIssueClose(subset, triggers, authorized) {
|
|
|
329
349
|
waitFor: rule.waitFor,
|
|
330
350
|
instructions: rule.instructions,
|
|
331
351
|
resume: rule.resume,
|
|
352
|
+
awaitInput: rule.awaitInput,
|
|
332
353
|
replicas: rule.replicas,
|
|
333
354
|
// `once: true` rides `matched` ONLY when armed (spreading false is a no-op), so the enqueue path
|
|
334
355
|
// can see a disarm is owed without re-reading the rule table -- and a plain close rule's matched
|
|
@@ -358,7 +379,7 @@ function routePullRequestClose(subset, triggers, authorized) {
|
|
|
358
379
|
enqueue: true,
|
|
359
380
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
360
381
|
packages: rule.packages,
|
|
361
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
382
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
362
383
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
363
384
|
skillsDir: rule.skillsDir,
|
|
364
385
|
secrets: rule.secrets,
|
|
@@ -366,6 +387,7 @@ function routePullRequestClose(subset, triggers, authorized) {
|
|
|
366
387
|
waitFor: rule.waitFor,
|
|
367
388
|
instructions: rule.instructions,
|
|
368
389
|
resume: rule.resume,
|
|
390
|
+
awaitInput: rule.awaitInput,
|
|
369
391
|
replicas: rule.replicas,
|
|
370
392
|
// No `number` on a PR close's matched record -- parallel to routePullRequest's, where the item
|
|
371
393
|
// number is the target's business; the issue route carries it because the github design does.
|
package/src/filter-gitlab.mjs
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
import { firstMatchingRule, labelSet, matchedLabel } from "./predicate.mjs";
|
|
44
44
|
import { findCloseRule } from "./close.mjs";
|
|
45
|
+
import { hasParkedLabel } from "@edgehero/pi-dispatch/parked";
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
/** The merge-request actions a trigger may name, mirrored from the loader's gitlab vocabulary. */
|
|
@@ -134,6 +135,9 @@ export function filterGitLab(subset, triggers, knownFlows, selfId, authorized, d
|
|
|
134
135
|
...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
|
|
135
136
|
// #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
|
|
136
137
|
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
138
|
+
// REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
|
|
139
|
+
// the job without its servers.
|
|
140
|
+
...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
|
|
137
141
|
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
138
142
|
// that named only a model would have its provider's absence decide whether the model survives,
|
|
139
143
|
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
@@ -159,6 +163,11 @@ export function filterGitLab(subset, triggers, knownFlows, selfId, authorized, d
|
|
|
159
163
|
// Conditional like packages/image, and for the same reason: an unflagged job's data must stay
|
|
160
164
|
// byte-identical to today's, so the key is absent rather than present-and-undefined.
|
|
161
165
|
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
|
|
166
|
+
// REQ-AWAIT-INPUT (issue #625): whether the job may end awaiting a person's answer. Present only when armed,
|
|
167
|
+
// resume's rule, so an unflagged job's data stays byte-identical.
|
|
168
|
+
...(resolved.awaitInput === true ? { awaitInput: true } : {}),
|
|
169
|
+
// A phrase-free reply to a parked issue (issue #625): the worker gates it on the `parked` sidecar before any spend.
|
|
170
|
+
...(resolved.parkedReply === true ? { parkedReply: true } : {}),
|
|
162
171
|
// How many independent sandboxes race this flow (REQ-REPLICA-RUNS). At JOB level and conditional for the
|
|
163
172
|
// reasons filter.mjs states in full: receiver.mjs reads this to decide how many times to enqueue, and an
|
|
164
173
|
// unflagged job's data must stay byte-identical to today's.
|
|
@@ -193,7 +202,7 @@ function routeLabel(subset, triggers, targetType) {
|
|
|
193
202
|
// A command rule (issue #189) skips flow resolution entirely: the label match IS the dispatch.
|
|
194
203
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
195
204
|
packages: rule.packages,
|
|
196
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
205
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
197
206
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
198
207
|
skillsDir: rule.skillsDir,
|
|
199
208
|
secrets: rule.secrets,
|
|
@@ -201,6 +210,7 @@ function routeLabel(subset, triggers, targetType) {
|
|
|
201
210
|
waitFor: rule.waitFor,
|
|
202
211
|
instructions: rule.instructions,
|
|
203
212
|
resume: rule.resume,
|
|
213
|
+
awaitInput: rule.awaitInput,
|
|
204
214
|
replicas: rule.replicas,
|
|
205
215
|
matched: { index: rule.index, type: "label", label: matchedLabel(added, rule.predicate) },
|
|
206
216
|
target: buildTarget(subset, targetType),
|
|
@@ -258,7 +268,7 @@ function mrResult(subset, rule, matched) {
|
|
|
258
268
|
// A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
|
|
259
269
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
260
270
|
packages: rule.packages,
|
|
261
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
271
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
262
272
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
263
273
|
skillsDir: rule.skillsDir,
|
|
264
274
|
secrets: rule.secrets,
|
|
@@ -266,6 +276,7 @@ function mrResult(subset, rule, matched) {
|
|
|
266
276
|
waitFor: rule.waitFor,
|
|
267
277
|
instructions: rule.instructions,
|
|
268
278
|
resume: rule.resume,
|
|
279
|
+
awaitInput: rule.awaitInput,
|
|
269
280
|
replicas: rule.replicas,
|
|
270
281
|
matched,
|
|
271
282
|
target: buildTarget(subset, "pull_request"),
|
|
@@ -305,7 +316,7 @@ function routeIssueClose(subset, triggers, authorized) {
|
|
|
305
316
|
enqueue: true,
|
|
306
317
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
307
318
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
308
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
319
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
309
320
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
310
321
|
skillsDir: rule.skillsDir,
|
|
311
322
|
secrets: rule.secrets,
|
|
@@ -313,6 +324,7 @@ function routeIssueClose(subset, triggers, authorized) {
|
|
|
313
324
|
waitFor: rule.waitFor,
|
|
314
325
|
instructions: rule.instructions,
|
|
315
326
|
resume: rule.resume,
|
|
327
|
+
awaitInput: rule.awaitInput,
|
|
316
328
|
replicas: rule.replicas,
|
|
317
329
|
// `once: true` rides `matched` ONLY when armed (spreading false is a no-op), so the enqueue path
|
|
318
330
|
// can see a disarm is owed without re-reading the rule table -- and a plain close rule's matched
|
|
@@ -340,7 +352,7 @@ function routeMergeRequestClose(subset, triggers, authorized) {
|
|
|
340
352
|
enqueue: true,
|
|
341
353
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
342
354
|
packages: rule.packages,
|
|
343
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
355
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
344
356
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
345
357
|
skillsDir: rule.skillsDir,
|
|
346
358
|
secrets: rule.secrets,
|
|
@@ -348,6 +360,7 @@ function routeMergeRequestClose(subset, triggers, authorized) {
|
|
|
348
360
|
waitFor: rule.waitFor,
|
|
349
361
|
instructions: rule.instructions,
|
|
350
362
|
resume: rule.resume,
|
|
363
|
+
awaitInput: rule.awaitInput,
|
|
351
364
|
replicas: rule.replicas,
|
|
352
365
|
// No `number` on an MR close's matched record -- parallel to routeMergeRequest's, where the item
|
|
353
366
|
// number is the target's business; the issue route carries it because the github design does.
|
|
@@ -367,7 +380,14 @@ function routeNote(subset, triggers, knownFlows) {
|
|
|
367
380
|
|
|
368
381
|
const phrase = triggers?.comment?.phrase;
|
|
369
382
|
const body = subset.note;
|
|
370
|
-
|
|
383
|
+
const phraseHit = typeof phrase === "string" && typeof body === "string" && body.includes(phrase);
|
|
384
|
+
// A reply WITHOUT the phrase resumes a parked issue (issue #625, parked.mjs): only on an issue (a pull request's key
|
|
385
|
+
// needs its head branch, which costs a token to read, so its reply keeps the phrase), only when the comment
|
|
386
|
+
// trigger arms run.resume and is not a command, and only when the forge says the issue carries the marker. The
|
|
387
|
+
// author gate above already ran: this widens WHICH comments start a job, never WHO may. The marker is a hint; the
|
|
388
|
+
// worker refuses the job before any spend unless the key's `parked` sidecar exists.
|
|
389
|
+
const parkedReply = !phraseHit && typeof phrase === "string" && typeof body === "string" && triggers?.comment?.resume === true && triggers?.comment?.command === undefined && subset.noteableType !== "MergeRequest" && hasParkedLabel(subset.target?.labels);
|
|
390
|
+
if (!phraseHit && !parkedReply) {
|
|
371
391
|
return { enqueue: false, reason: "no-trigger-phrase" };
|
|
372
392
|
}
|
|
373
393
|
// On a COMMAND rule (issue #189) the flow-resolution block below -- default flow, the `<phrase> <flow>`
|
|
@@ -393,7 +413,7 @@ function routeNote(subset, triggers, knownFlows) {
|
|
|
393
413
|
enqueue: true,
|
|
394
414
|
...(command !== undefined ? { command } : { flow }),
|
|
395
415
|
packages: triggers.comment.packages,
|
|
396
|
-
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
|
|
416
|
+
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
|
|
397
417
|
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
398
418
|
skillsDir: triggers.comment.skillsDir,
|
|
399
419
|
secrets: triggers.comment.secrets,
|
|
@@ -401,8 +421,10 @@ function routeNote(subset, triggers, knownFlows) {
|
|
|
401
421
|
waitFor: triggers.comment.waitFor,
|
|
402
422
|
instructions: triggers.comment.instructions,
|
|
403
423
|
resume: triggers.comment.resume,
|
|
424
|
+
awaitInput: triggers.comment.awaitInput,
|
|
404
425
|
replicas: triggers.comment.replicas,
|
|
405
426
|
matched: { index: triggers.comment.index, type: "comment", phrase },
|
|
427
|
+
...(parkedReply && { parkedReply: true }),
|
|
406
428
|
target: buildTarget(subset, targetType),
|
|
407
429
|
// The invoking comment rides the job as DATA (CONST-ISSUE-TEXT-IS-DATA). No author_association
|
|
408
430
|
// counterpart exists on GitLab: the approval is the resolved access level, which is a harness
|
package/src/filter.mjs
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
*/
|
|
48
48
|
import { findCloseRule } from "./close.mjs";
|
|
49
49
|
import { escapeRegExp, firstMatchingRule, labelSet, matchedLabel, matchesRule } from "./predicate.mjs";
|
|
50
|
+
import { hasParkedLabel } from "@edgehero/pi-dispatch/parked";
|
|
50
51
|
|
|
51
52
|
const AUTHOR_ALLOWLIST = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
|
52
53
|
const LABEL_ACTIONS = new Set(["opened", "labeled", "reopened"]);
|
|
@@ -157,6 +158,9 @@ export function filter(eventName, subset, cfg, selfId, deliveryId, closerAuthori
|
|
|
157
158
|
// dropped exclusion runs the job WITH the tool, the destructive absence the loader's own sweep
|
|
158
159
|
// refuses a misspelling for, arriving through the plumbing instead of the spelling.
|
|
159
160
|
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
161
|
+
// REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
|
|
162
|
+
// the job without its servers.
|
|
163
|
+
...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
|
|
160
164
|
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
161
165
|
// that named only a model would have its provider's absence decide whether the model survives,
|
|
162
166
|
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
@@ -182,6 +186,11 @@ export function filter(eventName, subset, cfg, selfId, deliveryId, closerAuthori
|
|
|
182
186
|
// Conditional like packages/image, and for the same reason: an unflagged job's data must stay
|
|
183
187
|
// byte-identical to today's, so the key is absent rather than present-and-undefined.
|
|
184
188
|
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
|
|
189
|
+
// REQ-AWAIT-INPUT (issue #625): whether the job may end awaiting a person's answer. Present only when armed,
|
|
190
|
+
// resume's rule, so an unflagged job's data stays byte-identical.
|
|
191
|
+
...(resolved.awaitInput === true ? { awaitInput: true } : {}),
|
|
192
|
+
// A phrase-free reply to a parked issue (issue #625): the worker gates it on the `parked` sidecar before any spend.
|
|
193
|
+
...(resolved.parkedReply === true ? { parkedReply: true } : {}),
|
|
185
194
|
// How many independent sandboxes race this flow (REQ-REPLICA-RUNS). At JOB level like the rest, never
|
|
186
195
|
// inside `trigger`, and for the sharpest version of that reason: this is the caller's fanout count --
|
|
187
196
|
// receiver.mjs reads it to decide how many times to enqueue -- and a descriptive context object copied
|
|
@@ -221,7 +230,7 @@ function routeIssueLabel(subset, triggers) {
|
|
|
221
230
|
// A command rule (issue #189) skips flow resolution entirely: the label match IS the dispatch.
|
|
222
231
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
223
232
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
224
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
233
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
225
234
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
226
235
|
skillsDir: rule.skillsDir,
|
|
227
236
|
secrets: rule.secrets,
|
|
@@ -229,6 +238,7 @@ function routeIssueLabel(subset, triggers) {
|
|
|
229
238
|
waitFor: rule.waitFor,
|
|
230
239
|
instructions: rule.instructions,
|
|
231
240
|
resume: rule.resume,
|
|
241
|
+
awaitInput: rule.awaitInput,
|
|
232
242
|
replicas: rule.replicas,
|
|
233
243
|
matched: { index: rule.index, type: "label", label: matchedLabel(L, rule.predicate) },
|
|
234
244
|
target: { type: "issue", number: subset.issue?.number, title: subset.issue?.title, body: subset.issue?.body },
|
|
@@ -247,7 +257,14 @@ function routeComment(subset, triggers, knownFlows) {
|
|
|
247
257
|
}
|
|
248
258
|
const phrase = triggers.comment?.phrase;
|
|
249
259
|
const body = subset.comment?.body;
|
|
250
|
-
|
|
260
|
+
const phraseHit = typeof phrase === "string" && typeof body === "string" && body.includes(phrase);
|
|
261
|
+
// A reply WITHOUT the phrase resumes a parked issue (issue #625, parked.mjs): only on an issue (a pull request's key
|
|
262
|
+
// needs its head branch, which costs a token to read, so its reply keeps the phrase), only when the comment
|
|
263
|
+
// trigger arms run.resume and is not a command, and only when the forge says the issue carries the marker. The
|
|
264
|
+
// author gate above already ran: this widens WHICH comments start a job, never WHO may. The marker is a hint; the
|
|
265
|
+
// worker refuses the job before any spend unless the key's `parked` sidecar exists.
|
|
266
|
+
const parkedReply = !phraseHit && typeof phrase === "string" && typeof body === "string" && triggers.comment?.resume === true && triggers.comment?.command === undefined && subset.issue?.pull_request !== true && hasParkedLabel(subset.issue?.labels);
|
|
267
|
+
if (!phraseHit && !parkedReply) {
|
|
251
268
|
return { enqueue: false, reason: "no-trigger-phrase" };
|
|
252
269
|
}
|
|
253
270
|
// On a COMMAND rule (issue #189) the whole flow-resolution block below -- default flow, the `<phrase>
|
|
@@ -282,7 +299,7 @@ function routeComment(subset, triggers, knownFlows) {
|
|
|
282
299
|
// The single comment trigger IS the matched rule here, so its opt-in is the job's. A `<phrase>
|
|
283
300
|
// <flow>` override changes WHICH flow runs, never which triggers.json entry authorized it.
|
|
284
301
|
packages: triggers.comment.packages,
|
|
285
|
-
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
|
|
302
|
+
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
|
|
286
303
|
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
287
304
|
skillsDir: triggers.comment.skillsDir,
|
|
288
305
|
secrets: triggers.comment.secrets,
|
|
@@ -290,8 +307,10 @@ function routeComment(subset, triggers, knownFlows) {
|
|
|
290
307
|
waitFor: triggers.comment.waitFor,
|
|
291
308
|
instructions: triggers.comment.instructions,
|
|
292
309
|
resume: triggers.comment.resume,
|
|
310
|
+
awaitInput: triggers.comment.awaitInput,
|
|
293
311
|
replicas: triggers.comment.replicas,
|
|
294
312
|
matched: { index: triggers.comment.index, type: "comment", phrase },
|
|
313
|
+
...(parkedReply && { parkedReply: true }),
|
|
295
314
|
// The invoking comment rides on the trigger: body and author_association are both named by
|
|
296
315
|
// INT-WEBHOOK-PAYLOAD-SUBSET, and the body stays DATA all the way down (CONST-ISSUE-TEXT-IS-DATA).
|
|
297
316
|
comment: { body: subset.comment.body, author_association: subset.comment.author_association },
|
|
@@ -381,7 +400,7 @@ function routePullRequest(subset, triggers, action) {
|
|
|
381
400
|
// A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
|
|
382
401
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
383
402
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
384
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
403
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
385
404
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
386
405
|
skillsDir: rule.skillsDir,
|
|
387
406
|
secrets: rule.secrets,
|
|
@@ -389,6 +408,7 @@ function routePullRequest(subset, triggers, action) {
|
|
|
389
408
|
waitFor: rule.waitFor,
|
|
390
409
|
instructions: rule.instructions,
|
|
391
410
|
resume: rule.resume,
|
|
411
|
+
awaitInput: rule.awaitInput,
|
|
392
412
|
replicas: rule.replicas,
|
|
393
413
|
matched: { index: rule.index, type: "pull_request", action },
|
|
394
414
|
...(isReview ? { review: { id: review.id, body: review.body, state: review.state, author_association: review.author_association } } : {}),
|
|
@@ -498,7 +518,7 @@ function routeClose(rules, number, closerAuthorized, matchedFor, targetFor) {
|
|
|
498
518
|
// A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
|
|
499
519
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
500
520
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
501
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
521
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
502
522
|
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
503
523
|
skillsDir: rule.skillsDir,
|
|
504
524
|
secrets: rule.secrets,
|
|
@@ -506,6 +526,7 @@ function routeClose(rules, number, closerAuthorized, matchedFor, targetFor) {
|
|
|
506
526
|
waitFor: rule.waitFor,
|
|
507
527
|
instructions: rule.instructions,
|
|
508
528
|
resume: rule.resume,
|
|
529
|
+
awaitInput: rule.awaitInput,
|
|
509
530
|
replicas: rule.replicas,
|
|
510
531
|
matched: matchedFor(rule),
|
|
511
532
|
target: targetFor(),
|