@edgehero/pi-dispatch-receiver 4.3.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/package.json +2 -2
- package/src/azure-members.mjs +6 -3
- package/src/boot-retry.mjs +1 -1
- package/src/cli.mjs +9 -1
- package/src/config.mjs +26 -56
- package/src/filter-azure.mjs +150 -99
- package/src/filter-forgejo.mjs +148 -88
- package/src/filter-gitlab.mjs +149 -90
- package/src/filter.mjs +168 -113
- package/src/forgejo-members.mjs +4 -3
- package/src/github-members.mjs +2 -1
- package/src/gitlab-members.mjs +2 -1
- package/src/held.mjs +120 -0
- package/src/poller.mjs +62 -15
- package/src/predicate.mjs +37 -0
- package/src/receiver.mjs +290 -60
- package/src/start.mjs +26 -5
- package/src/steer.mjs +221 -0
- package/src/trigger-groups.mjs +57 -0
- package/src/verify-azure.mjs +1 -1
- package/src/verify-gitlab.mjs +5 -4
- package/src/verify-watch.mjs +40 -0
- package/src/verify.mjs +1 -1
- package/src/watch-pull.mjs +94 -0
- package/src/watch-puller.mjs +82 -0
- package/src/watch.mjs +60 -0
- package/src/http-body.mjs +0 -73
package/src/filter-forgejo.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* a paid run, and it must be visible that it was seen and refused.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import { escapeRegExp, firstMatchingRule, labelSet, matchedLabel, matchesRule } from "./predicate.mjs";
|
|
36
|
+
import { escapeRegExp, firstMatchingRule, heldCandidateOr, labelSet, matchedLabel, matchesRule, steerModeOf } from "./predicate.mjs";
|
|
37
37
|
import { findCloseRule } from "./close.mjs";
|
|
38
38
|
import { isRecognizedAction, mapAction } from "./forgejo-subset.mjs";
|
|
39
39
|
import { hasParkedLabel } from "@edgehero/pi-dispatch/parked";
|
|
@@ -46,7 +46,8 @@ export const PR_ACTIONS = new Set(["labeled", "opened", "synchronize", "reopened
|
|
|
46
46
|
*
|
|
47
47
|
* `subset` is a `parseForgejoSubset` projection; `triggers` is the forgejo rule group; `knownFlows` is the
|
|
48
48
|
* whole file's flow vocabulary; `authorized` is the receiver's resolved verdict; `deliveryId` is the
|
|
49
|
-
* `X-GitHub-Delivery` GUID, which Forgejo emits
|
|
49
|
+
* `X-GitHub-Delivery` GUID, which Forgejo emits once per delivery. Forgejo never resends a delivery on its own,
|
|
50
|
+
* and a replay from its UI is a new delivery with a new GUID (receiver.mjs, WHAT A 503 MEANS).
|
|
50
51
|
*/
|
|
51
52
|
export function filterForgejo(eventName, subset, triggers, knownFlows, selfId, authorized, deliveryId) {
|
|
52
53
|
// (0) Fail-closed on identity. MUST precede the self compare -- see header.
|
|
@@ -108,76 +109,101 @@ export function filterForgejo(eventName, subset, triggers, knownFlows, selfId, a
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
// A drop is returned as it is, except a phrase-free reply that failed ONLY for the missing parked marker (issue #638,
|
|
113
|
+
// DES-AWAIT-INPUT-ENDS-THE-JOB): that one also carries `heldCandidate`, the job the parked-reply route would have
|
|
114
|
+
// built, through the SAME `build` below, so the receiver can hold it for a run that is parking right now and the job
|
|
115
|
+
// that run starts is byte for byte the one the marker would have produced. Still a drop: nothing here enqueues it.
|
|
116
|
+
if (!resolved.enqueue) return heldCandidateOr(resolved, build);
|
|
117
|
+
return build(resolved);
|
|
112
118
|
|
|
113
|
-
// The job
|
|
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
|
-
...(resolved.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
119
|
+
// The job from a resolved route. One function for every route and for the held candidate (issue #638), declared
|
|
120
|
+
// here so it closes over this delivery's subset and ids.
|
|
121
|
+
function build(resolved) {
|
|
122
|
+
// The job literal mirrors the other two forges' exactly -- `repo`, `target`, `flow`, the three
|
|
123
|
+
// conditional execution knobs, and a descriptive `trigger`. `sender.login` is in the subset but is NOT
|
|
124
|
+
// carried here: it exists for the permission lookup and nowhere else, and this object is copied verbatim
|
|
125
|
+
// into /job/event.json, which must stay free of personal data.
|
|
126
|
+
const job = {
|
|
127
|
+
repo: subset.repository?.full_name,
|
|
128
|
+
target: resolved.target,
|
|
129
|
+
// EXACTLY ONE of flow/command, decided by the matched rule (issue #189; the shared parser enforces
|
|
130
|
+
// the exclusivity at load). A command rule dispatches a registered pi extension command instead of
|
|
131
|
+
// resolving a flow, and its job must carry NO flow key at all. The spread keeps a flow rule's
|
|
132
|
+
// literal byte-identical to what it always was -- the same move filter.mjs makes.
|
|
133
|
+
...(resolved.command !== undefined ? { command: resolved.command } : { flow: resolved.flow }),
|
|
134
|
+
...(resolved.packages !== undefined ? { packages: resolved.packages } : {}),
|
|
135
|
+
...(resolved.image !== undefined ? { image: resolved.image } : {}),
|
|
136
|
+
// #227. A SEPARATE spread, and this is the whole of the bug it replaces: folded into the `image`
|
|
137
|
+
// conditional above, a trigger that named a venue but no image had its venue silently dropped here --
|
|
138
|
+
// it loaded, validated, reached this function on the rule, and then ran on the default. That is
|
|
139
|
+
// exactly the destructive absence `validateBackend`'s near-miss sweep refuses a misspelling for,
|
|
140
|
+
// arriving through the plumbing instead of the spelling, one file downstream of the guard.
|
|
141
|
+
...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
|
|
142
|
+
// #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
|
|
143
|
+
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
144
|
+
// Issue #800, each its own spread by the same rule: a dropped list runs the job with what the file takes away.
|
|
145
|
+
...(resolved.excludeExtensions !== undefined ? { excludeExtensions: resolved.excludeExtensions } : {}),
|
|
146
|
+
...(resolved.excludeExtensionTools !== undefined ? { excludeExtensionTools: resolved.excludeExtensionTools } : {}),
|
|
147
|
+
// REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
|
|
148
|
+
// the job without its servers.
|
|
149
|
+
...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
|
|
150
|
+
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
151
|
+
// that named only a model would have its provider's absence decide whether the model survives,
|
|
152
|
+
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
153
|
+
...(resolved.provider !== undefined ? { provider: resolved.provider } : {}),
|
|
154
|
+
...(resolved.model !== undefined ? { model: resolved.model } : {}),
|
|
155
|
+
...(resolved.maxTurns !== undefined ? { maxTurns: resolved.maxTurns } : {}),
|
|
156
|
+
...(resolved.models !== undefined ? { models: resolved.models } : {}), ...(resolved.fallbackModels !== undefined ? { fallbackModels: resolved.fallbackModels } : {}),
|
|
157
|
+
// #501. Its own spread, the rule above: a dropped cap runs the job under the deployment's cap, or none,
|
|
158
|
+
// while the file reads as narrowed.
|
|
159
|
+
...(resolved.maxCostUsd !== undefined ? { maxCostUsd: resolved.maxCostUsd } : {}),
|
|
160
|
+
// The trigger's injected skills dir (REQ-PER-TRIGGER-SKILLS), at JOB level beside image/packages and
|
|
161
|
+
// NEVER inside `trigger`. That placement is sharpest here of all: `trigger` is carried into
|
|
162
|
+
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
|
|
163
|
+
// basename(folder) restraint already exists to prevent.
|
|
164
|
+
...(resolved.skillsDir !== undefined ? { skillsDir: resolved.skillsDir } : {}),
|
|
165
|
+
// REQ-TRIGGER-SECRETS. Conditional exactly like skillsDir above, so a trigger that binds no
|
|
166
|
+
// secret enqueues the job data it always has. The map holds REFERENCES, never values: the
|
|
167
|
+
// receiver has no resolver and reaches no vault -- the worker resolves them pre-spend.
|
|
168
|
+
...(resolved.secrets !== undefined ? { secrets: resolved.secrets } : {}),
|
|
169
|
+
...(resolved.secretsProfile !== undefined ? { secretsProfile: resolved.secretsProfile } : {}),
|
|
170
|
+
...(resolved.waitFor !== undefined ? { waitFor: resolved.waitFor } : {}),
|
|
171
|
+
...(resolved.instructions !== undefined ? { instructions: resolved.instructions } : {}),
|
|
172
|
+
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
|
|
173
|
+
// REQ-AWAIT-INPUT (issue #625): whether the job may end awaiting a person's answer. Present only when armed,
|
|
174
|
+
// resume's rule, so an unflagged job's data stays byte-identical.
|
|
175
|
+
...(resolved.awaitInput === true ? { awaitInput: true } : {}),
|
|
176
|
+
...(resolved.gates !== undefined ? { gates: resolved.gates, ...(resolved.gateTimeout !== undefined ? { gateTimeout: resolved.gateTimeout } : {}), ...(resolved.gateBudgetRaiseUsd !== undefined ? { gateBudgetRaiseUsd: resolved.gateBudgetRaiseUsd } : {}) } : {}),
|
|
177
|
+
// Issue #644: the progress tool and the forge status comment, each present only when armed, awaitInput's rule.
|
|
178
|
+
...(resolved.progress === true ? { progress: true } : {}),
|
|
179
|
+
...(resolved.statusComment === true ? { statusComment: true } : {}),
|
|
180
|
+
// Issue #645: durable runs, present only when armed, the same rule.
|
|
181
|
+
...(resolved.recover === true ? { recover: true } : {}),
|
|
182
|
+
// Issue #646: steering a running job, present only when armed, the same rule.
|
|
183
|
+
...(resolved.steer === true ? { steer: true } : {}),
|
|
184
|
+
// Issue #646: how this phrase comment steers a job of its trigger that is already running, set only on the comment
|
|
185
|
+
// route of a steer-armed trigger (never a parked reply, never a command). The receiver reads it to try the run's
|
|
186
|
+
// inbox before it enqueues, and `enqueueForgeJob` names its fields, so it never reaches a queue. At JOB level, never
|
|
187
|
+
// inside `trigger`, which is copied verbatim into /job/event.json.
|
|
188
|
+
...(resolved.steerMode !== undefined ? { steerMode: resolved.steerMode } : {}),
|
|
189
|
+
// A phrase-free reply to a parked issue (issue #625): the worker gates it on the `parked` sidecar before any spend.
|
|
190
|
+
...(resolved.parkedReply === true ? { parkedReply: true } : {}),
|
|
191
|
+
// How many independent sandboxes race this flow (REQ-REPLICA-RUNS). At JOB level and conditional for the
|
|
192
|
+
// reasons filter.mjs states in full: receiver.mjs reads this to decide how many times to enqueue, and an
|
|
193
|
+
// unflagged job's data must stay byte-identical to today's.
|
|
194
|
+
...(resolved.replicas !== undefined ? { replicas: resolved.replicas } : {}),
|
|
195
|
+
trigger: {
|
|
196
|
+
event: eventName,
|
|
197
|
+
// Forgejo's OWN word, not our translation of it. The run record should say what the forge said.
|
|
198
|
+
action: raw,
|
|
199
|
+
deliveryId,
|
|
200
|
+
sender: { id: subset.sender.id },
|
|
201
|
+
matched: resolved.matched,
|
|
202
|
+
...(resolved.comment ? { comment: resolved.comment } : {}),
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
return { enqueue: true, job };
|
|
206
|
+
}
|
|
181
207
|
}
|
|
182
208
|
|
|
183
209
|
/** Issue label path: the label predicate selects, and the resolved permission has already gated. */
|
|
@@ -192,15 +218,19 @@ function routeIssueLabel(subset, triggers) {
|
|
|
192
218
|
// A command rule (issue #189) skips flow resolution entirely: the label match IS the dispatch.
|
|
193
219
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
194
220
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
195
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
196
|
-
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
221
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, excludeExtensions: rule.excludeExtensions, excludeExtensionTools: rule.excludeExtensionTools, mcp: rule.mcp,
|
|
222
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, fallbackModels: rule.fallbackModels, maxCostUsd: rule.maxCostUsd,
|
|
197
223
|
skillsDir: rule.skillsDir,
|
|
198
224
|
secrets: rule.secrets,
|
|
199
225
|
secretsProfile: rule.secretsProfile,
|
|
200
226
|
waitFor: rule.waitFor,
|
|
201
227
|
instructions: rule.instructions,
|
|
202
228
|
resume: rule.resume,
|
|
203
|
-
awaitInput: rule.awaitInput,
|
|
229
|
+
awaitInput: rule.awaitInput, gates: rule.gates, gateTimeout: rule.gateTimeout, gateBudgetRaiseUsd: rule.gateBudgetRaiseUsd,
|
|
230
|
+
progress: rule.progress,
|
|
231
|
+
statusComment: rule.statusComment,
|
|
232
|
+
recover: rule.recover,
|
|
233
|
+
steer: rule.steer,
|
|
204
234
|
replicas: rule.replicas,
|
|
205
235
|
matched: { index: rule.index, type: "label", label: matchedLabel(L, rule.predicate) },
|
|
206
236
|
target: { type: "issue", number: subset.issue?.number, title: subset.issue?.title, body: subset.issue?.body },
|
|
@@ -208,7 +238,7 @@ function routeIssueLabel(subset, triggers) {
|
|
|
208
238
|
}
|
|
209
239
|
|
|
210
240
|
/** Comment path. The authority gate above has already run, so this only has to match the phrase. */
|
|
211
|
-
function routeComment(subset, triggers, knownFlows) {
|
|
241
|
+
function routeComment(subset, triggers, knownFlows, assumeParked = false) {
|
|
212
242
|
const phrase = triggers.comment?.phrase;
|
|
213
243
|
const body = subset.comment?.body;
|
|
214
244
|
const phraseHit = typeof phrase === "string" && typeof body === "string" && body.includes(phrase);
|
|
@@ -217,8 +247,17 @@ function routeComment(subset, triggers, knownFlows) {
|
|
|
217
247
|
// trigger arms run.resume and is not a command, and only when the forge says the issue carries the marker. The
|
|
218
248
|
// author gate above already ran: this widens WHICH comments start a job, never WHO may. The marker is a hint; the
|
|
219
249
|
// worker refuses the job before any spend unless the key's `parked` sidecar exists.
|
|
220
|
-
const
|
|
250
|
+
const parkedCandidate = !phraseHit && typeof phrase === "string" && typeof body === "string" && triggers.comment?.resume === true && triggers.comment?.command === undefined && subset.isPull !== true;
|
|
251
|
+
const parkedReply = parkedCandidate && (assumeParked || hasParkedLabel(subset.issue?.labels));
|
|
221
252
|
if (!phraseHit && !parkedReply) {
|
|
253
|
+
// Issue #638: a reply that fails ONLY for the missing marker (the run asked its question and is still parking, so
|
|
254
|
+
// the label is not on yet) is still a drop, but it carries the route the marker would have taken, built by this
|
|
255
|
+
// same function with the marker assumed (so every other rule above still decided it). The receiver holds it for a
|
|
256
|
+
// run that is parking on this target, or drops it exactly as today.
|
|
257
|
+
if (parkedCandidate) {
|
|
258
|
+
const held = routeComment(subset, triggers, knownFlows, true);
|
|
259
|
+
if (held.enqueue) return { enqueue: false, reason: "no-trigger-phrase", heldResolved: held };
|
|
260
|
+
}
|
|
222
261
|
return { enqueue: false, reason: "no-trigger-phrase" };
|
|
223
262
|
}
|
|
224
263
|
// On a COMMAND rule (issue #189) the flow-resolution block below -- default flow, the `<phrase> <flow>`
|
|
@@ -250,18 +289,27 @@ function routeComment(subset, triggers, knownFlows) {
|
|
|
250
289
|
enqueue: true,
|
|
251
290
|
...(command !== undefined ? { command } : { flow }),
|
|
252
291
|
packages: triggers.comment.packages,
|
|
253
|
-
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
|
|
254
|
-
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
292
|
+
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, excludeExtensions: triggers.comment.excludeExtensions, excludeExtensionTools: triggers.comment.excludeExtensionTools, mcp: triggers.comment.mcp,
|
|
293
|
+
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, fallbackModels: triggers.comment.fallbackModels, maxCostUsd: triggers.comment.maxCostUsd,
|
|
255
294
|
skillsDir: triggers.comment.skillsDir,
|
|
256
295
|
secrets: triggers.comment.secrets,
|
|
257
296
|
secretsProfile: triggers.comment.secretsProfile,
|
|
258
297
|
waitFor: triggers.comment.waitFor,
|
|
259
298
|
instructions: triggers.comment.instructions,
|
|
260
299
|
resume: triggers.comment.resume,
|
|
261
|
-
awaitInput: triggers.comment.awaitInput,
|
|
300
|
+
awaitInput: triggers.comment.awaitInput, gates: triggers.comment.gates, gateTimeout: triggers.comment.gateTimeout, gateBudgetRaiseUsd: triggers.comment.gateBudgetRaiseUsd,
|
|
301
|
+
progress: triggers.comment.progress,
|
|
302
|
+
statusComment: triggers.comment.statusComment,
|
|
303
|
+
recover: triggers.comment.recover,
|
|
304
|
+
steer: triggers.comment.steer,
|
|
262
305
|
replicas: triggers.comment.replicas,
|
|
263
306
|
matched: { index: triggers.comment.index, type: "comment", phrase },
|
|
264
307
|
...(parkedReply && { parkedReply: true }),
|
|
308
|
+
// Issue #646: a phrase comment on a steer-armed trigger may steer a run of it that is already going. Only with the
|
|
309
|
+
// phrase (a parked reply is an answer to a parked run, never a steer) and never on a command rule (the loader refuses
|
|
310
|
+
// `steer` beside `command`; checked again here so a drifted file cannot reach a run). The author gate above already
|
|
311
|
+
// ran: this changes where an admitted comment goes, never who may send one. The `later` grammar is `steerModeOf`'s.
|
|
312
|
+
...(phraseHit && command === undefined && triggers.comment.steer === true && { steerMode: steerModeOf(body, phrase) }),
|
|
265
313
|
// The invoking comment rides on the trigger. No author_association: Forgejo has none, and the
|
|
266
314
|
// authority that admitted this comment was resolved from the API, not read off the body.
|
|
267
315
|
comment: { body: subset.comment.body },
|
|
@@ -284,15 +332,19 @@ function routePullRequest(subset, triggers, action) {
|
|
|
284
332
|
// A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
|
|
285
333
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
286
334
|
packages: rule.packages,
|
|
287
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
288
|
-
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
335
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, excludeExtensions: rule.excludeExtensions, excludeExtensionTools: rule.excludeExtensionTools, mcp: rule.mcp,
|
|
336
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, fallbackModels: rule.fallbackModels, maxCostUsd: rule.maxCostUsd,
|
|
289
337
|
skillsDir: rule.skillsDir,
|
|
290
338
|
secrets: rule.secrets,
|
|
291
339
|
secretsProfile: rule.secretsProfile,
|
|
292
340
|
waitFor: rule.waitFor,
|
|
293
341
|
instructions: rule.instructions,
|
|
294
342
|
resume: rule.resume,
|
|
295
|
-
awaitInput: rule.awaitInput,
|
|
343
|
+
awaitInput: rule.awaitInput, gates: rule.gates, gateTimeout: rule.gateTimeout, gateBudgetRaiseUsd: rule.gateBudgetRaiseUsd,
|
|
344
|
+
progress: rule.progress,
|
|
345
|
+
statusComment: rule.statusComment,
|
|
346
|
+
recover: rule.recover,
|
|
347
|
+
steer: rule.steer,
|
|
296
348
|
replicas: rule.replicas,
|
|
297
349
|
matched: { index: rule.index, type: "pull_request", action },
|
|
298
350
|
target: {
|
|
@@ -341,15 +393,19 @@ function routeIssueClose(subset, triggers, authorized) {
|
|
|
341
393
|
enqueue: true,
|
|
342
394
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
343
395
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
344
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
345
|
-
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
396
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, excludeExtensions: rule.excludeExtensions, excludeExtensionTools: rule.excludeExtensionTools, mcp: rule.mcp,
|
|
397
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, fallbackModels: rule.fallbackModels, maxCostUsd: rule.maxCostUsd,
|
|
346
398
|
skillsDir: rule.skillsDir,
|
|
347
399
|
secrets: rule.secrets,
|
|
348
400
|
secretsProfile: rule.secretsProfile,
|
|
349
401
|
waitFor: rule.waitFor,
|
|
350
402
|
instructions: rule.instructions,
|
|
351
403
|
resume: rule.resume,
|
|
352
|
-
awaitInput: rule.awaitInput,
|
|
404
|
+
awaitInput: rule.awaitInput, gates: rule.gates, gateTimeout: rule.gateTimeout, gateBudgetRaiseUsd: rule.gateBudgetRaiseUsd,
|
|
405
|
+
progress: rule.progress,
|
|
406
|
+
statusComment: rule.statusComment,
|
|
407
|
+
recover: rule.recover,
|
|
408
|
+
steer: rule.steer,
|
|
353
409
|
replicas: rule.replicas,
|
|
354
410
|
// `once: true` rides `matched` ONLY when armed (spreading false is a no-op), so the enqueue path
|
|
355
411
|
// can see a disarm is owed without re-reading the rule table -- and a plain close rule's matched
|
|
@@ -379,15 +435,19 @@ function routePullRequestClose(subset, triggers, authorized) {
|
|
|
379
435
|
enqueue: true,
|
|
380
436
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
381
437
|
packages: rule.packages,
|
|
382
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
383
|
-
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
438
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, excludeExtensions: rule.excludeExtensions, excludeExtensionTools: rule.excludeExtensionTools, mcp: rule.mcp,
|
|
439
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, fallbackModels: rule.fallbackModels, maxCostUsd: rule.maxCostUsd,
|
|
384
440
|
skillsDir: rule.skillsDir,
|
|
385
441
|
secrets: rule.secrets,
|
|
386
442
|
secretsProfile: rule.secretsProfile,
|
|
387
443
|
waitFor: rule.waitFor,
|
|
388
444
|
instructions: rule.instructions,
|
|
389
445
|
resume: rule.resume,
|
|
390
|
-
awaitInput: rule.awaitInput,
|
|
446
|
+
awaitInput: rule.awaitInput, gates: rule.gates, gateTimeout: rule.gateTimeout, gateBudgetRaiseUsd: rule.gateBudgetRaiseUsd,
|
|
447
|
+
progress: rule.progress,
|
|
448
|
+
statusComment: rule.statusComment,
|
|
449
|
+
recover: rule.recover,
|
|
450
|
+
steer: rule.steer,
|
|
391
451
|
replicas: rule.replicas,
|
|
392
452
|
// No `number` on a PR close's matched record -- parallel to routePullRequest's, where the item
|
|
393
453
|
// number is the target's business; the issue route carries it because the github design does.
|