@edgehero/pi-dispatch-receiver 4.3.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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-gitlab.mjs
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* `changes.labels` matches nothing.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
import { firstMatchingRule, labelSet, matchedLabel } from "./predicate.mjs";
|
|
43
|
+
import { firstMatchingRule, heldCandidateOr, labelSet, matchedLabel, steerModeOf } from "./predicate.mjs";
|
|
44
44
|
import { findCloseRule } from "./close.mjs";
|
|
45
45
|
import { hasParkedLabel } from "@edgehero/pi-dispatch/parked";
|
|
46
46
|
|
|
@@ -109,79 +109,104 @@ export function filterGitLab(subset, triggers, knownFlows, selfId, authorized, d
|
|
|
109
109
|
return { enqueue: false, reason: "unhandled-event" };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
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);
|
|
113
118
|
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
// (4) Build the job from the INT-GITLAB-PAYLOAD-SUBSET fields only. `user.username` is deliberately
|
|
123
|
+
// NOT carried: it exists so the receiver could resolve an access level, and it is personal data with no
|
|
124
|
+
// downstream reader (no-pii-in-logs). `projectId` rides the job because every GitLab API path the
|
|
125
|
+
// worker needs takes the numeric id -- a `group/subgroup/project` path has no fixed segment count, and
|
|
126
|
+
// splitting one is how a nested project silently becomes its own parent group.
|
|
127
|
+
const job = {
|
|
128
|
+
repo: subset.project?.path,
|
|
129
|
+
projectId: subset.project?.id,
|
|
130
|
+
target: resolved.target,
|
|
131
|
+
// EXACTLY ONE of flow/command, decided by the matched rule (issue #189; the shared parser enforces
|
|
132
|
+
// the exclusivity at load). A command rule dispatches a registered pi extension command instead of
|
|
133
|
+
// resolving a flow, and its job must carry NO flow key at all. The spread keeps a flow rule's
|
|
134
|
+
// literal byte-identical to what it always was -- the same move filter.mjs makes.
|
|
135
|
+
...(resolved.command !== undefined ? { command: resolved.command } : { flow: resolved.flow }),
|
|
136
|
+
...(resolved.packages !== undefined ? { packages: resolved.packages } : {}),
|
|
137
|
+
...(resolved.image !== undefined ? { image: resolved.image } : {}),
|
|
138
|
+
// #227. A SEPARATE spread, and this is the whole of the bug it replaces: folded into the `image`
|
|
139
|
+
// conditional above, a trigger that named a venue but no image had its venue silently dropped here --
|
|
140
|
+
// it loaded, validated, reached this function on the rule, and then ran on the default. That is
|
|
141
|
+
// exactly the destructive absence `validateBackend`'s near-miss sweep refuses a misspelling for,
|
|
142
|
+
// arriving through the plumbing instead of the spelling, one file downstream of the guard.
|
|
143
|
+
...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
|
|
144
|
+
// #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
|
|
145
|
+
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
146
|
+
// Issue #800, each its own spread by the same rule: a dropped list runs the job with what the file takes away.
|
|
147
|
+
...(resolved.excludeExtensions !== undefined ? { excludeExtensions: resolved.excludeExtensions } : {}),
|
|
148
|
+
...(resolved.excludeExtensionTools !== undefined ? { excludeExtensionTools: resolved.excludeExtensionTools } : {}),
|
|
149
|
+
// REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
|
|
150
|
+
// the job without its servers.
|
|
151
|
+
...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
|
|
152
|
+
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
153
|
+
// that named only a model would have its provider's absence decide whether the model survives,
|
|
154
|
+
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
155
|
+
...(resolved.provider !== undefined ? { provider: resolved.provider } : {}),
|
|
156
|
+
...(resolved.model !== undefined ? { model: resolved.model } : {}),
|
|
157
|
+
...(resolved.maxTurns !== undefined ? { maxTurns: resolved.maxTurns } : {}),
|
|
158
|
+
...(resolved.models !== undefined ? { models: resolved.models } : {}), ...(resolved.fallbackModels !== undefined ? { fallbackModels: resolved.fallbackModels } : {}),
|
|
159
|
+
// #501. Its own spread, the rule above: a dropped cap runs the job under the deployment's cap, or none,
|
|
160
|
+
// while the file reads as narrowed.
|
|
161
|
+
...(resolved.maxCostUsd !== undefined ? { maxCostUsd: resolved.maxCostUsd } : {}),
|
|
162
|
+
// The trigger's injected skills dir (REQ-PER-TRIGGER-SKILLS), at JOB level beside image/packages and
|
|
163
|
+
// NEVER inside `trigger`. That placement is sharpest here of all: `trigger` is carried into
|
|
164
|
+
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
|
|
165
|
+
// basename(folder) restraint already exists to prevent.
|
|
166
|
+
...(resolved.skillsDir !== undefined ? { skillsDir: resolved.skillsDir } : {}),
|
|
167
|
+
// REQ-TRIGGER-SECRETS. Conditional exactly like skillsDir above, so a trigger that binds no
|
|
168
|
+
// secret enqueues the job data it always has. The map holds REFERENCES, never values: the
|
|
169
|
+
// receiver has no resolver and reaches no vault -- the worker resolves them pre-spend.
|
|
170
|
+
...(resolved.secrets !== undefined ? { secrets: resolved.secrets } : {}),
|
|
171
|
+
...(resolved.secretsProfile !== undefined ? { secretsProfile: resolved.secretsProfile } : {}),
|
|
172
|
+
...(resolved.waitFor !== undefined ? { waitFor: resolved.waitFor } : {}),
|
|
173
|
+
...(resolved.instructions !== undefined ? { instructions: resolved.instructions } : {}),
|
|
174
|
+
// Conditional like packages/image, and for the same reason: an unflagged job's data must stay
|
|
175
|
+
// byte-identical to today's, so the key is absent rather than present-and-undefined.
|
|
176
|
+
...(resolved.resume !== undefined ? { resume: resolved.resume } : {}),
|
|
177
|
+
// REQ-AWAIT-INPUT (issue #625): whether the job may end awaiting a person's answer. Present only when armed,
|
|
178
|
+
// resume's rule, so an unflagged job's data stays byte-identical.
|
|
179
|
+
...(resolved.awaitInput === true ? { awaitInput: true } : {}),
|
|
180
|
+
...(resolved.gates !== undefined ? { gates: resolved.gates, ...(resolved.gateTimeout !== undefined ? { gateTimeout: resolved.gateTimeout } : {}), ...(resolved.gateBudgetRaiseUsd !== undefined ? { gateBudgetRaiseUsd: resolved.gateBudgetRaiseUsd } : {}) } : {}),
|
|
181
|
+
// Issue #644: the progress tool and the forge status comment, each present only when armed, awaitInput's rule.
|
|
182
|
+
...(resolved.progress === true ? { progress: true } : {}),
|
|
183
|
+
...(resolved.statusComment === true ? { statusComment: true } : {}),
|
|
184
|
+
// Issue #645: durable runs, present only when armed, the same rule.
|
|
185
|
+
...(resolved.recover === true ? { recover: true } : {}),
|
|
186
|
+
// Issue #646: steering a running job, present only when armed, the same rule.
|
|
187
|
+
...(resolved.steer === true ? { steer: true } : {}),
|
|
188
|
+
// Issue #646: how this phrase comment steers a job of its trigger that is already running, set only on the comment
|
|
189
|
+
// route of a steer-armed trigger (never a parked reply, never a command). The receiver reads it to try the run's
|
|
190
|
+
// inbox before it enqueues, and `enqueueForgeJob` names its fields, so it never reaches a queue. At JOB level, never
|
|
191
|
+
// inside `trigger`, which is copied verbatim into /job/event.json.
|
|
192
|
+
...(resolved.steerMode !== undefined ? { steerMode: resolved.steerMode } : {}),
|
|
193
|
+
// A phrase-free reply to a parked issue (issue #625): the worker gates it on the `parked` sidecar before any spend.
|
|
194
|
+
...(resolved.parkedReply === true ? { parkedReply: true } : {}),
|
|
195
|
+
// How many independent sandboxes race this flow (REQ-REPLICA-RUNS). At JOB level and conditional for the
|
|
196
|
+
// reasons filter.mjs states in full: receiver.mjs reads this to decide how many times to enqueue, and an
|
|
197
|
+
// unflagged job's data must stay byte-identical to today's.
|
|
198
|
+
...(resolved.replicas !== undefined ? { replicas: resolved.replicas } : {}),
|
|
199
|
+
trigger: {
|
|
200
|
+
event: kind,
|
|
201
|
+
action: subset.action,
|
|
202
|
+
deliveryId,
|
|
203
|
+
sender: { id: subset.user.id },
|
|
204
|
+
matched: resolved.matched,
|
|
205
|
+
...(resolved.comment ? { comment: resolved.comment } : {}),
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
return { enqueue: true, job };
|
|
209
|
+
}
|
|
185
210
|
}
|
|
186
211
|
|
|
187
212
|
/**
|
|
@@ -202,15 +227,19 @@ function routeLabel(subset, triggers, targetType) {
|
|
|
202
227
|
// A command rule (issue #189) skips flow resolution entirely: the label match IS the dispatch.
|
|
203
228
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
204
229
|
packages: rule.packages,
|
|
205
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
206
|
-
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
230
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, excludeExtensions: rule.excludeExtensions, excludeExtensionTools: rule.excludeExtensionTools, mcp: rule.mcp,
|
|
231
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, fallbackModels: rule.fallbackModels, maxCostUsd: rule.maxCostUsd,
|
|
207
232
|
skillsDir: rule.skillsDir,
|
|
208
233
|
secrets: rule.secrets,
|
|
209
234
|
secretsProfile: rule.secretsProfile,
|
|
210
235
|
waitFor: rule.waitFor,
|
|
211
236
|
instructions: rule.instructions,
|
|
212
237
|
resume: rule.resume,
|
|
213
|
-
awaitInput: rule.awaitInput,
|
|
238
|
+
awaitInput: rule.awaitInput, gates: rule.gates, gateTimeout: rule.gateTimeout, gateBudgetRaiseUsd: rule.gateBudgetRaiseUsd,
|
|
239
|
+
progress: rule.progress,
|
|
240
|
+
statusComment: rule.statusComment,
|
|
241
|
+
recover: rule.recover,
|
|
242
|
+
steer: rule.steer,
|
|
214
243
|
replicas: rule.replicas,
|
|
215
244
|
matched: { index: rule.index, type: "label", label: matchedLabel(added, rule.predicate) },
|
|
216
245
|
target: buildTarget(subset, targetType),
|
|
@@ -268,15 +297,19 @@ function mrResult(subset, rule, matched) {
|
|
|
268
297
|
// A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
|
|
269
298
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
270
299
|
packages: rule.packages,
|
|
271
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
272
|
-
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
300
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, excludeExtensions: rule.excludeExtensions, excludeExtensionTools: rule.excludeExtensionTools, mcp: rule.mcp,
|
|
301
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, fallbackModels: rule.fallbackModels, maxCostUsd: rule.maxCostUsd,
|
|
273
302
|
skillsDir: rule.skillsDir,
|
|
274
303
|
secrets: rule.secrets,
|
|
275
304
|
secretsProfile: rule.secretsProfile,
|
|
276
305
|
waitFor: rule.waitFor,
|
|
277
306
|
instructions: rule.instructions,
|
|
278
307
|
resume: rule.resume,
|
|
279
|
-
awaitInput: rule.awaitInput,
|
|
308
|
+
awaitInput: rule.awaitInput, gates: rule.gates, gateTimeout: rule.gateTimeout, gateBudgetRaiseUsd: rule.gateBudgetRaiseUsd,
|
|
309
|
+
progress: rule.progress,
|
|
310
|
+
statusComment: rule.statusComment,
|
|
311
|
+
recover: rule.recover,
|
|
312
|
+
steer: rule.steer,
|
|
280
313
|
replicas: rule.replicas,
|
|
281
314
|
matched,
|
|
282
315
|
target: buildTarget(subset, "pull_request"),
|
|
@@ -316,15 +349,19 @@ function routeIssueClose(subset, triggers, authorized) {
|
|
|
316
349
|
enqueue: true,
|
|
317
350
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
318
351
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
319
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
320
|
-
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
352
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, excludeExtensions: rule.excludeExtensions, excludeExtensionTools: rule.excludeExtensionTools, mcp: rule.mcp,
|
|
353
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, fallbackModels: rule.fallbackModels, maxCostUsd: rule.maxCostUsd,
|
|
321
354
|
skillsDir: rule.skillsDir,
|
|
322
355
|
secrets: rule.secrets,
|
|
323
356
|
secretsProfile: rule.secretsProfile,
|
|
324
357
|
waitFor: rule.waitFor,
|
|
325
358
|
instructions: rule.instructions,
|
|
326
359
|
resume: rule.resume,
|
|
327
|
-
awaitInput: rule.awaitInput,
|
|
360
|
+
awaitInput: rule.awaitInput, gates: rule.gates, gateTimeout: rule.gateTimeout, gateBudgetRaiseUsd: rule.gateBudgetRaiseUsd,
|
|
361
|
+
progress: rule.progress,
|
|
362
|
+
statusComment: rule.statusComment,
|
|
363
|
+
recover: rule.recover,
|
|
364
|
+
steer: rule.steer,
|
|
328
365
|
replicas: rule.replicas,
|
|
329
366
|
// `once: true` rides `matched` ONLY when armed (spreading false is a no-op), so the enqueue path
|
|
330
367
|
// can see a disarm is owed without re-reading the rule table -- and a plain close rule's matched
|
|
@@ -352,15 +389,19 @@ function routeMergeRequestClose(subset, triggers, authorized) {
|
|
|
352
389
|
enqueue: true,
|
|
353
390
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
354
391
|
packages: rule.packages,
|
|
355
|
-
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
|
|
356
|
-
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
392
|
+
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, excludeExtensions: rule.excludeExtensions, excludeExtensionTools: rule.excludeExtensionTools, mcp: rule.mcp,
|
|
393
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, fallbackModels: rule.fallbackModels, maxCostUsd: rule.maxCostUsd,
|
|
357
394
|
skillsDir: rule.skillsDir,
|
|
358
395
|
secrets: rule.secrets,
|
|
359
396
|
secretsProfile: rule.secretsProfile,
|
|
360
397
|
waitFor: rule.waitFor,
|
|
361
398
|
instructions: rule.instructions,
|
|
362
399
|
resume: rule.resume,
|
|
363
|
-
awaitInput: rule.awaitInput,
|
|
400
|
+
awaitInput: rule.awaitInput, gates: rule.gates, gateTimeout: rule.gateTimeout, gateBudgetRaiseUsd: rule.gateBudgetRaiseUsd,
|
|
401
|
+
progress: rule.progress,
|
|
402
|
+
statusComment: rule.statusComment,
|
|
403
|
+
recover: rule.recover,
|
|
404
|
+
steer: rule.steer,
|
|
364
405
|
replicas: rule.replicas,
|
|
365
406
|
// No `number` on an MR close's matched record -- parallel to routeMergeRequest's, where the item
|
|
366
407
|
// number is the target's business; the issue route carries it because the github design does.
|
|
@@ -375,7 +416,7 @@ function routeMergeRequestClose(subset, triggers, authorized) {
|
|
|
375
416
|
* issue branch for something that is already a merge request: wrong work, no error, and it looks like a
|
|
376
417
|
* successful run.
|
|
377
418
|
*/
|
|
378
|
-
function routeNote(subset, triggers, knownFlows) {
|
|
419
|
+
function routeNote(subset, triggers, knownFlows, assumeParked = false) {
|
|
379
420
|
if (subset.action !== "create") return { enqueue: false, reason: "unhandled-event" };
|
|
380
421
|
|
|
381
422
|
const phrase = triggers?.comment?.phrase;
|
|
@@ -386,8 +427,17 @@ function routeNote(subset, triggers, knownFlows) {
|
|
|
386
427
|
// trigger arms run.resume and is not a command, and only when the forge says the issue carries the marker. The
|
|
387
428
|
// author gate above already ran: this widens WHICH comments start a job, never WHO may. The marker is a hint; the
|
|
388
429
|
// worker refuses the job before any spend unless the key's `parked` sidecar exists.
|
|
389
|
-
const
|
|
430
|
+
const parkedCandidate = !phraseHit && typeof phrase === "string" && typeof body === "string" && triggers?.comment?.resume === true && triggers?.comment?.command === undefined && subset.noteableType !== "MergeRequest";
|
|
431
|
+
const parkedReply = parkedCandidate && (assumeParked || hasParkedLabel(subset.target?.labels));
|
|
390
432
|
if (!phraseHit && !parkedReply) {
|
|
433
|
+
// Issue #638: a reply that fails ONLY for the missing marker (the run asked its question and is still parking, so
|
|
434
|
+
// the label is not on yet) is still a drop, but it carries the route the marker would have taken, built by this
|
|
435
|
+
// same function with the marker assumed (so every other rule above still decided it). The receiver holds it for a
|
|
436
|
+
// run that is parking on this target, or drops it exactly as today.
|
|
437
|
+
if (parkedCandidate) {
|
|
438
|
+
const held = routeNote(subset, triggers, knownFlows, true);
|
|
439
|
+
if (held.enqueue) return { enqueue: false, reason: "no-trigger-phrase", heldResolved: held };
|
|
440
|
+
}
|
|
391
441
|
return { enqueue: false, reason: "no-trigger-phrase" };
|
|
392
442
|
}
|
|
393
443
|
// On a COMMAND rule (issue #189) the flow-resolution block below -- default flow, the `<phrase> <flow>`
|
|
@@ -413,18 +463,27 @@ function routeNote(subset, triggers, knownFlows) {
|
|
|
413
463
|
enqueue: true,
|
|
414
464
|
...(command !== undefined ? { command } : { flow }),
|
|
415
465
|
packages: triggers.comment.packages,
|
|
416
|
-
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
|
|
417
|
-
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
466
|
+
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, excludeExtensions: triggers.comment.excludeExtensions, excludeExtensionTools: triggers.comment.excludeExtensionTools, mcp: triggers.comment.mcp,
|
|
467
|
+
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, fallbackModels: triggers.comment.fallbackModels, maxCostUsd: triggers.comment.maxCostUsd,
|
|
418
468
|
skillsDir: triggers.comment.skillsDir,
|
|
419
469
|
secrets: triggers.comment.secrets,
|
|
420
470
|
secretsProfile: triggers.comment.secretsProfile,
|
|
421
471
|
waitFor: triggers.comment.waitFor,
|
|
422
472
|
instructions: triggers.comment.instructions,
|
|
423
473
|
resume: triggers.comment.resume,
|
|
424
|
-
awaitInput: triggers.comment.awaitInput,
|
|
474
|
+
awaitInput: triggers.comment.awaitInput, gates: triggers.comment.gates, gateTimeout: triggers.comment.gateTimeout, gateBudgetRaiseUsd: triggers.comment.gateBudgetRaiseUsd,
|
|
475
|
+
progress: triggers.comment.progress,
|
|
476
|
+
statusComment: triggers.comment.statusComment,
|
|
477
|
+
recover: triggers.comment.recover,
|
|
478
|
+
steer: triggers.comment.steer,
|
|
425
479
|
replicas: triggers.comment.replicas,
|
|
426
480
|
matched: { index: triggers.comment.index, type: "comment", phrase },
|
|
427
481
|
...(parkedReply && { parkedReply: true }),
|
|
482
|
+
// Issue #646: a phrase comment on a steer-armed trigger may steer a run of it that is already going. Only with the
|
|
483
|
+
// phrase (a parked reply is an answer to a parked run, never a steer) and never on a command rule (the loader refuses
|
|
484
|
+
// `steer` beside `command`; checked again here so a drifted file cannot reach a run). The author gate above already
|
|
485
|
+
// ran: this changes where an admitted comment goes, never who may send one. The `later` grammar is `steerModeOf`'s.
|
|
486
|
+
...(phraseHit && command === undefined && triggers?.comment?.steer === true && { steerMode: steerModeOf(body, phrase) }),
|
|
428
487
|
target: buildTarget(subset, targetType),
|
|
429
488
|
// The invoking comment rides the job as DATA (CONST-ISSUE-TEXT-IS-DATA). No author_association
|
|
430
489
|
// counterpart exists on GitLab: the approval is the resolved access level, which is a harness
|