@agent-relay/factory 0.1.7 → 0.1.9
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 +7 -0
- package/dist/cli/fleet.js +2 -2
- package/dist/cli/fleet.js.map +1 -1
- package/dist/fleet/create-fleet.d.ts +1 -0
- package/dist/fleet/create-fleet.d.ts.map +1 -1
- package/dist/fleet/create-fleet.js +1 -0
- package/dist/fleet/create-fleet.js.map +1 -1
- package/dist/fleet/ensure-relay-broker.d.ts +2 -0
- package/dist/fleet/ensure-relay-broker.d.ts.map +1 -1
- package/dist/fleet/ensure-relay-broker.js +8 -12
- package/dist/fleet/ensure-relay-broker.js.map +1 -1
- package/dist/fleet/internal-fleet-client.d.ts +2 -1
- package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
- package/dist/fleet/internal-fleet-client.js +13 -14
- package/dist/fleet/internal-fleet-client.js.map +1 -1
- package/dist/fleet/relay-workspace-key.d.ts +8 -0
- package/dist/fleet/relay-workspace-key.d.ts.map +1 -0
- package/dist/fleet/relay-workspace-key.js +18 -0
- package/dist/fleet/relay-workspace-key.js.map +1 -0
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +211 -35
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/writeback/index.d.ts +1 -1
- package/dist/writeback/index.d.ts.map +1 -1
- package/dist/writeback/index.js +1 -1
- package/dist/writeback/index.js.map +1 -1
- package/dist/writeback/linear.d.ts +2 -0
- package/dist/writeback/linear.d.ts.map +1 -1
- package/dist/writeback/linear.js +17 -10
- package/dist/writeback/linear.js.map +1 -1
- package/dist/writeback/slack.d.ts +2 -0
- package/dist/writeback/slack.d.ts.map +1 -1
- package/dist/writeback/slack.js +5 -5
- package/dist/writeback/slack.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import { isInFactoryScope } from '../safety/factory-scope.js';
|
|
|
10
10
|
import { deriveDescriptorsFromMount, prescriptiveInstructions, } from '@agent-relay/integration-prompts';
|
|
11
11
|
import { renderAgentTask } from '../dispatch/templates.js';
|
|
12
12
|
import { HeuristicTriage, TieredTriage, babysitterSpec, isShapeLabel, scopeFromLabels } from '../triage/index.js';
|
|
13
|
-
import { MountGithubRead, MountLinearWriteback, MountSlackWriteback } from '../writeback/index.js';
|
|
13
|
+
import { MountGithubRead, MountLinearWriteback, MountSlackWriteback, slackChannelAliases, slackChannelSegment } from '../writeback/index.js';
|
|
14
14
|
import { asRecord, parseJsonContent, stableHash, wrappedPayload } from '../writeback/shared.js';
|
|
15
15
|
import { issueKey } from './batch-tracker.js';
|
|
16
16
|
import { findAgentProcessByName, readProcessIdentity } from './process-identity.js';
|
|
@@ -98,6 +98,8 @@ export class FactoryLoop {
|
|
|
98
98
|
#resumeInFlight = new Map();
|
|
99
99
|
#slackWatchers = new Map();
|
|
100
100
|
#slackWatcherStarts = new Map();
|
|
101
|
+
#resolvedSlackChannelDir;
|
|
102
|
+
#slackChannelDirRefresh;
|
|
101
103
|
// Agents we've already logged an ambiguous-PID-lookup warning for, so the
|
|
102
104
|
// reaper doesn't spam the same benign "ambiguous process lookup" line on every
|
|
103
105
|
// poll (a joined/cloud agent has no local PID to resolve — expected).
|
|
@@ -1137,7 +1139,7 @@ export class FactoryLoop {
|
|
|
1137
1139
|
const escalationReason = triageEscalationReason(decision);
|
|
1138
1140
|
if (escalationReason) {
|
|
1139
1141
|
await this.#escalateTriageToSlack(decision, escalationReason, dryRun);
|
|
1140
|
-
this.#
|
|
1142
|
+
this.#recordTriageEscalation(decision, escalationReason);
|
|
1141
1143
|
return { issue: decision.issue, agents: [], dryRun };
|
|
1142
1144
|
}
|
|
1143
1145
|
// TODO(AR-274 follow-up): short-circuit LLM triage once label-derived
|
|
@@ -1326,7 +1328,7 @@ export class FactoryLoop {
|
|
|
1326
1328
|
const escalationReason = triageEscalationReason(decision);
|
|
1327
1329
|
if (escalationReason) {
|
|
1328
1330
|
await this.#escalateTriageToSlack(decision, escalationReason, this.#config.dryRun);
|
|
1329
|
-
this.#
|
|
1331
|
+
this.#recordTriageEscalation(decision, escalationReason);
|
|
1330
1332
|
return;
|
|
1331
1333
|
}
|
|
1332
1334
|
if (batch.canStart()) {
|
|
@@ -2513,12 +2515,15 @@ export class FactoryLoop {
|
|
|
2513
2515
|
});
|
|
2514
2516
|
if (this.#slack && this.#config.slack && !await this.#shouldSkipSlackWriteback('merge-done-thread')) {
|
|
2515
2517
|
try {
|
|
2516
|
-
const
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2518
|
+
const channel = await this.#slackChannelDir();
|
|
2519
|
+
if (channel) {
|
|
2520
|
+
const root = await this.#slack.postThread({
|
|
2521
|
+
channel,
|
|
2522
|
+
text: `${issue.key}: PR merged; Linear state set to Done.`,
|
|
2523
|
+
});
|
|
2524
|
+
await this.#slack.reply(root.threadId, `${issue.key}: Linear state set to Done.`);
|
|
2525
|
+
this.#recordSlackWritebackSuccess('merge-done-thread');
|
|
2526
|
+
}
|
|
2522
2527
|
}
|
|
2523
2528
|
catch (error) {
|
|
2524
2529
|
this.#markSlackWritebackFailure('merge-done-thread', error);
|
|
@@ -2752,21 +2757,24 @@ export class FactoryLoop {
|
|
|
2752
2757
|
}
|
|
2753
2758
|
if (this.#slack && this.#config.slack && !await this.#shouldSkipSlackWriteback('completion-thread')) {
|
|
2754
2759
|
try {
|
|
2755
|
-
const
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
? `${record.issue.key}:
|
|
2763
|
-
:
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2760
|
+
const channel = await this.#slackChannelDir();
|
|
2761
|
+
if (channel) {
|
|
2762
|
+
const merged = opts.completionReason === 'pr-merged';
|
|
2763
|
+
const completionText = merged
|
|
2764
|
+
? `${record.issue.key}: PR merged; Linear state set to ${statusLabel}.`
|
|
2765
|
+
: `${record.issue.key}: factory agents completed${humanReview ? '; awaiting human review' : ''}.\nStatus: ${statusLabel}\nMerge policy: ${this.#config.mergePolicy}`;
|
|
2766
|
+
const stateText = merged
|
|
2767
|
+
? `${record.issue.key}: PR merged; Linear state set to ${statusLabel}.`
|
|
2768
|
+
: humanReview
|
|
2769
|
+
? `${record.issue.key}: awaiting human review; Linear state set to ${statusLabel}.`
|
|
2770
|
+
: `${record.issue.key}: Linear state set to ${statusLabel}.`;
|
|
2771
|
+
const root = await this.#slack.postThread({
|
|
2772
|
+
channel,
|
|
2773
|
+
text: completionText,
|
|
2774
|
+
});
|
|
2775
|
+
await this.#slack.reply(root.threadId, stateText);
|
|
2776
|
+
this.#recordSlackWritebackSuccess('completion-thread');
|
|
2777
|
+
}
|
|
2770
2778
|
}
|
|
2771
2779
|
catch (error) {
|
|
2772
2780
|
this.#markSlackWritebackFailure('completion-thread', error);
|
|
@@ -2813,6 +2821,13 @@ export class FactoryLoop {
|
|
|
2813
2821
|
#increment(name) {
|
|
2814
2822
|
this.#counters[name] = (this.#counters[name] ?? 0) + 1;
|
|
2815
2823
|
}
|
|
2824
|
+
#recordTriageEscalation(decision, reason) {
|
|
2825
|
+
this.#increment('triageEscalations');
|
|
2826
|
+
this.#logger.warn?.('[factory] triage escalation required', {
|
|
2827
|
+
issue: decision.issue,
|
|
2828
|
+
reason,
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2816
2831
|
async #shouldSkipSlackWriteback(context) {
|
|
2817
2832
|
if (!this.#config.slack)
|
|
2818
2833
|
return false;
|
|
@@ -3012,7 +3027,7 @@ export class FactoryLoop {
|
|
|
3012
3027
|
return;
|
|
3013
3028
|
}
|
|
3014
3029
|
const root = await this.#slack.postThread({
|
|
3015
|
-
channel: this.#config.slack.channel,
|
|
3030
|
+
channel: await this.#slackChannelDir() ?? this.#config.slack.channel,
|
|
3016
3031
|
text: [
|
|
3017
3032
|
`${record.issue.key}: factory agents dispatched.`,
|
|
3018
3033
|
`State: ${result.stateId ?? 'dispatching'}`,
|
|
@@ -3073,11 +3088,11 @@ export class FactoryLoop {
|
|
|
3073
3088
|
}
|
|
3074
3089
|
const issue = await this.#readIssue(decision.issue.path);
|
|
3075
3090
|
const root = await this.#slack.postThread({
|
|
3076
|
-
channel: this.#config.slack.channel,
|
|
3091
|
+
channel: await this.#slackChannelDir() ?? this.#config.slack.channel,
|
|
3077
3092
|
text: [
|
|
3078
3093
|
`${decision.issue.key}: factory triage escalation for ${issue?.title ?? decision.issue.key}`,
|
|
3079
3094
|
`Reason: ${reason}`,
|
|
3080
|
-
`Question:
|
|
3095
|
+
`Question: ${triageEscalationQuestion(decision)}`,
|
|
3081
3096
|
].join('\n'),
|
|
3082
3097
|
});
|
|
3083
3098
|
await this.#state.setSlackThread(this.#workspaceId, issueKey(decision.issue), root.threadId);
|
|
@@ -3092,7 +3107,7 @@ export class FactoryLoop {
|
|
|
3092
3107
|
if (this.#slackWatchers.has(key)) {
|
|
3093
3108
|
return;
|
|
3094
3109
|
}
|
|
3095
|
-
const channelDir = this.#config.slack.channel;
|
|
3110
|
+
const channelDir = await this.#slackChannelDir() ?? this.#config.slack.channel;
|
|
3096
3111
|
const messagesPrefix = slackChannelMessagesPrefix(channelDir);
|
|
3097
3112
|
const preExistingPaths = new Set();
|
|
3098
3113
|
const seenReplies = new Set();
|
|
@@ -3268,17 +3283,24 @@ export class FactoryLoop {
|
|
|
3268
3283
|
}
|
|
3269
3284
|
}
|
|
3270
3285
|
async #routeSlackAnswerToImplementers(record, reply) {
|
|
3271
|
-
if (!this.#config.slack
|
|
3286
|
+
if (!this.#config.slack) {
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const text = reply.text.trim();
|
|
3290
|
+
if (!text) {
|
|
3291
|
+
this.#increment('slackAnswersIgnoredEmpty');
|
|
3272
3292
|
return;
|
|
3273
3293
|
}
|
|
3274
3294
|
const liveRecord = (await this.#batch()).getIssue(record.issue);
|
|
3275
3295
|
if (!liveRecord || liveRecord.dryRun) {
|
|
3296
|
+
if (isTriageEscalationWatchRecord(record)) {
|
|
3297
|
+
await this.#handleTriageEscalationSlackAnswer(record, text);
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3276
3300
|
this.#increment('slackAnswersIgnoredNoInFlight');
|
|
3277
3301
|
return;
|
|
3278
3302
|
}
|
|
3279
|
-
|
|
3280
|
-
if (!text) {
|
|
3281
|
-
this.#increment('slackAnswersIgnoredEmpty');
|
|
3303
|
+
if (!this.#fleet.sendInput) {
|
|
3282
3304
|
return;
|
|
3283
3305
|
}
|
|
3284
3306
|
// Route human replies to the implementers AND the babysitter — once a PR is
|
|
@@ -3296,6 +3318,49 @@ export class FactoryLoop {
|
|
|
3296
3318
|
this.#increment('slackAnswersInjected');
|
|
3297
3319
|
}
|
|
3298
3320
|
}
|
|
3321
|
+
async #handleTriageEscalationSlackAnswer(record, text) {
|
|
3322
|
+
const issue = await this.#readIssue(record.issue.path);
|
|
3323
|
+
if (!issue || !isInFactoryScope(issue, this.#config.safety) || !isRealLinearIssue(issue)) {
|
|
3324
|
+
this.#increment('slackTriageAnswersIgnoredIssueUnavailable');
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3327
|
+
if (!this.#states.isRole(issue.stateId, 'readyForAgent')) {
|
|
3328
|
+
this.#increment('slackTriageAnswersIgnoredIssueNotReady');
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
const batch = await this.#batch();
|
|
3332
|
+
if (batch.isInFlight(record.issue) || batch.isQueued(record.issue)) {
|
|
3333
|
+
this.#increment('slackTriageAnswersIgnoredAlreadyActive');
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
if (await this.#dispatchBlockReason(record.issue)) {
|
|
3337
|
+
this.#increment('slackTriageAnswersIgnoredBlocked');
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
const clarifiedIssue = issueWithSlackClarification(issue, text);
|
|
3341
|
+
const decision = await this.#triage.triage(clarifiedIssue, {
|
|
3342
|
+
config: this.#config,
|
|
3343
|
+
repoMap: repoMapFromConfig(this.#config),
|
|
3344
|
+
});
|
|
3345
|
+
const escalationReason = triageEscalationReason(decision);
|
|
3346
|
+
if (escalationReason) {
|
|
3347
|
+
this.#increment('slackTriageAnswersStillEscalated');
|
|
3348
|
+
this.#logger.warn?.('[factory] Slack triage answer still leaves issue escalated', {
|
|
3349
|
+
issue: record.issue,
|
|
3350
|
+
reason: escalationReason,
|
|
3351
|
+
});
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
if (batch.canStart()) {
|
|
3355
|
+
await this.dispatch(decision, { dryRun: this.#config.dryRun });
|
|
3356
|
+
this.#increment('slackTriageAnswersDispatched');
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
if (batch.queue(decision, this.#config.dryRun)) {
|
|
3360
|
+
this.#increment('slackTriageAnswersQueued');
|
|
3361
|
+
this.#emit('issue-queued', { issue: decision.issue });
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3299
3364
|
// Inject the human's Slack reply into the agent framed as the
|
|
3300
3365
|
// <integration-event> the spawn prompt tells it to expect (not an ambiguous
|
|
3301
3366
|
// "Slack reply for ..." keystroke), so the agent recognizes it as the awaited
|
|
@@ -3311,13 +3376,78 @@ export class FactoryLoop {
|
|
|
3311
3376
|
#integrationsMountRoot() {
|
|
3312
3377
|
return resolve(process.cwd(), '.integrations');
|
|
3313
3378
|
}
|
|
3379
|
+
async #slackChannelDir() {
|
|
3380
|
+
if (!this.#config.slack) {
|
|
3381
|
+
return undefined;
|
|
3382
|
+
}
|
|
3383
|
+
if (this.#resolvedSlackChannelDir) {
|
|
3384
|
+
return this.#resolvedSlackChannelDir;
|
|
3385
|
+
}
|
|
3386
|
+
if (this.#slackChannelDirRefresh) {
|
|
3387
|
+
return this.#slackChannelDirRefresh;
|
|
3388
|
+
}
|
|
3389
|
+
this.#slackChannelDirRefresh = this.#resolveSlackChannelDir()
|
|
3390
|
+
.finally(() => {
|
|
3391
|
+
this.#slackChannelDirRefresh = undefined;
|
|
3392
|
+
});
|
|
3393
|
+
return this.#slackChannelDirRefresh;
|
|
3394
|
+
}
|
|
3395
|
+
async #resolveSlackChannelDir() {
|
|
3396
|
+
const configured = this.#config.slack?.channel.trim();
|
|
3397
|
+
if (!configured) {
|
|
3398
|
+
return undefined;
|
|
3399
|
+
}
|
|
3400
|
+
const configuredSegment = slackChannelSegment(configured);
|
|
3401
|
+
const configuredAliases = slackChannelAliases(configured);
|
|
3402
|
+
if (configuredAliases.size === 0) {
|
|
3403
|
+
return undefined;
|
|
3404
|
+
}
|
|
3405
|
+
let paths;
|
|
3406
|
+
try {
|
|
3407
|
+
paths = await this.#mount.listTree('/slack/channels');
|
|
3408
|
+
}
|
|
3409
|
+
catch (error) {
|
|
3410
|
+
this.#logger.warn?.('[factory] unable to resolve Slack channel from mount; using configured channel value', {
|
|
3411
|
+
channel: configured,
|
|
3412
|
+
error,
|
|
3413
|
+
});
|
|
3414
|
+
this.#resolvedSlackChannelDir = configuredSegment;
|
|
3415
|
+
return this.#resolvedSlackChannelDir;
|
|
3416
|
+
}
|
|
3417
|
+
const channelDirs = [...new Set(paths
|
|
3418
|
+
.map((path) => path.match(/^\/slack\/channels\/([^/]+)/u)?.[1])
|
|
3419
|
+
.filter((channelDir) => Boolean(channelDir)))]
|
|
3420
|
+
.sort();
|
|
3421
|
+
const matches = channelDirs.filter((channelDir) => {
|
|
3422
|
+
const aliases = slackChannelAliases(channelDir);
|
|
3423
|
+
return [...aliases].some((alias) => configuredAliases.has(alias));
|
|
3424
|
+
});
|
|
3425
|
+
if (matches.length === 0) {
|
|
3426
|
+
this.#logger.warn?.('[factory] Slack channel was not found in mount; using configured channel value', {
|
|
3427
|
+
channel: configured,
|
|
3428
|
+
});
|
|
3429
|
+
this.#resolvedSlackChannelDir = configuredSegment;
|
|
3430
|
+
return this.#resolvedSlackChannelDir;
|
|
3431
|
+
}
|
|
3432
|
+
const exact = matches.find((channelDir) => slackChannelSegment(channelDir).toLowerCase() === configuredSegment.toLowerCase());
|
|
3433
|
+
this.#resolvedSlackChannelDir = exact ?? matches[0];
|
|
3434
|
+
if (matches.length > 1 && !exact) {
|
|
3435
|
+
this.#logger.warn?.('[factory] Slack channel name matched multiple mount directories; using first match', {
|
|
3436
|
+
channel: configured,
|
|
3437
|
+
selected: this.#resolvedSlackChannelDir,
|
|
3438
|
+
matches,
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
return this.#resolvedSlackChannelDir;
|
|
3442
|
+
}
|
|
3314
3443
|
async #slackDispatchThreadFor(record) {
|
|
3315
3444
|
if (!this.#config.slack) {
|
|
3316
3445
|
return undefined;
|
|
3317
3446
|
}
|
|
3318
3447
|
const threadId = await this.#state.getSlackThread(this.#workspaceId, issueKey(record.issue));
|
|
3448
|
+
const channel = await this.#slackChannelDir() ?? this.#config.slack.channel;
|
|
3319
3449
|
return threadId
|
|
3320
|
-
? { channel
|
|
3450
|
+
? { channel, threadId, mountRoot: this.#integrationsMountRoot() }
|
|
3321
3451
|
: undefined;
|
|
3322
3452
|
}
|
|
3323
3453
|
async #runCompletionMergeGate(issue) {
|
|
@@ -3701,8 +3831,25 @@ function githubMirrorRepoForIssue(issue) {
|
|
|
3701
3831
|
return githubRepoFromUrl(sourceUrlLine?.slice(GITHUB_MIRROR_SOURCE_PREFIX.length));
|
|
3702
3832
|
}
|
|
3703
3833
|
function githubRepoFromUrl(url) {
|
|
3704
|
-
const
|
|
3705
|
-
|
|
3834
|
+
for (const candidate of githubUrlCandidates(url)) {
|
|
3835
|
+
const match = candidate.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/\d+(?:[/?#].*)?$/iu);
|
|
3836
|
+
if (match?.[1] && match[2]) {
|
|
3837
|
+
return `${match[1]}/${match[2]}`;
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
return undefined;
|
|
3841
|
+
}
|
|
3842
|
+
function githubUrlCandidates(value) {
|
|
3843
|
+
if (!value) {
|
|
3844
|
+
return [];
|
|
3845
|
+
}
|
|
3846
|
+
const candidates = new Set();
|
|
3847
|
+
const trimmed = value.trim();
|
|
3848
|
+
candidates.add(trimmed.replace(/^<|>$/gu, ''));
|
|
3849
|
+
for (const match of trimmed.matchAll(/https:\/\/github\.com\/[^\s<>)\]]+/giu)) {
|
|
3850
|
+
candidates.add(match[0].replace(/[),.;]+$/gu, ''));
|
|
3851
|
+
}
|
|
3852
|
+
return [...candidates];
|
|
3706
3853
|
}
|
|
3707
3854
|
function labelRoutesForIssue(issue, config) {
|
|
3708
3855
|
const labels = uniqueNormalizedLabels(issue.labels).filter((label) => !isShapeLabel(label));
|
|
@@ -4558,6 +4705,35 @@ const triageEscalationReason = (decision) => {
|
|
|
4558
4705
|
}
|
|
4559
4706
|
return `${reasons.join(' and ')}${decision.rationale ? `: ${decision.rationale}` : ''}`;
|
|
4560
4707
|
};
|
|
4708
|
+
const triageEscalationQuestion = (decision) => {
|
|
4709
|
+
const routedRepos = decision.routes.map((route) => route.repo).filter(Boolean);
|
|
4710
|
+
if (routedRepos.length === 0) {
|
|
4711
|
+
return [
|
|
4712
|
+
'Which repository or repositories should handle this issue?',
|
|
4713
|
+
'Please include the intended approach and the acceptance criteria/tests the agent should satisfy.',
|
|
4714
|
+
].join(' ');
|
|
4715
|
+
}
|
|
4716
|
+
if (decision.thin) {
|
|
4717
|
+
return [
|
|
4718
|
+
`Factory matched ${routedRepos.join(', ')}.`,
|
|
4719
|
+
'Please clarify the concrete expected behavior, constraints, and acceptance criteria/tests before dispatch.',
|
|
4720
|
+
].join(' ');
|
|
4721
|
+
}
|
|
4722
|
+
return [
|
|
4723
|
+
`Factory matched ${routedRepos.join(', ')}, but triage confidence is low.`,
|
|
4724
|
+
'Please confirm the intended repo/approach or correct the route before dispatch.',
|
|
4725
|
+
].join(' ');
|
|
4726
|
+
};
|
|
4727
|
+
const isTriageEscalationWatchRecord = (record) => record.agents.size === 0 && record.invocationIds.size === 0 && triageEscalationReason(record.decision) !== undefined;
|
|
4728
|
+
const issueWithSlackClarification = (issue, text) => ({
|
|
4729
|
+
...issue,
|
|
4730
|
+
description: [
|
|
4731
|
+
issue.description,
|
|
4732
|
+
'',
|
|
4733
|
+
'Human clarification from Slack:',
|
|
4734
|
+
text,
|
|
4735
|
+
].join('\n'),
|
|
4736
|
+
});
|
|
4561
4737
|
const slackAnswerInput = (issue, text) => `Slack reply for ${issue.key}:\n${text}\r`;
|
|
4562
4738
|
// The human's Slack-thread reply, framed as an <integration-event> the agent is
|
|
4563
4739
|
// told (at spawn) to expect — a recognizable injected event, not an ambiguous
|