@agent-relay/factory 0.1.7 → 0.1.8
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 +118 -28
- 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).
|
|
@@ -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);
|
|
@@ -3012,7 +3020,7 @@ export class FactoryLoop {
|
|
|
3012
3020
|
return;
|
|
3013
3021
|
}
|
|
3014
3022
|
const root = await this.#slack.postThread({
|
|
3015
|
-
channel: this.#config.slack.channel,
|
|
3023
|
+
channel: await this.#slackChannelDir() ?? this.#config.slack.channel,
|
|
3016
3024
|
text: [
|
|
3017
3025
|
`${record.issue.key}: factory agents dispatched.`,
|
|
3018
3026
|
`State: ${result.stateId ?? 'dispatching'}`,
|
|
@@ -3073,7 +3081,7 @@ export class FactoryLoop {
|
|
|
3073
3081
|
}
|
|
3074
3082
|
const issue = await this.#readIssue(decision.issue.path);
|
|
3075
3083
|
const root = await this.#slack.postThread({
|
|
3076
|
-
channel: this.#config.slack.channel,
|
|
3084
|
+
channel: await this.#slackChannelDir() ?? this.#config.slack.channel,
|
|
3077
3085
|
text: [
|
|
3078
3086
|
`${decision.issue.key}: factory triage escalation for ${issue?.title ?? decision.issue.key}`,
|
|
3079
3087
|
`Reason: ${reason}`,
|
|
@@ -3092,7 +3100,7 @@ export class FactoryLoop {
|
|
|
3092
3100
|
if (this.#slackWatchers.has(key)) {
|
|
3093
3101
|
return;
|
|
3094
3102
|
}
|
|
3095
|
-
const channelDir = this.#config.slack.channel;
|
|
3103
|
+
const channelDir = await this.#slackChannelDir() ?? this.#config.slack.channel;
|
|
3096
3104
|
const messagesPrefix = slackChannelMessagesPrefix(channelDir);
|
|
3097
3105
|
const preExistingPaths = new Set();
|
|
3098
3106
|
const seenReplies = new Set();
|
|
@@ -3311,13 +3319,78 @@ export class FactoryLoop {
|
|
|
3311
3319
|
#integrationsMountRoot() {
|
|
3312
3320
|
return resolve(process.cwd(), '.integrations');
|
|
3313
3321
|
}
|
|
3322
|
+
async #slackChannelDir() {
|
|
3323
|
+
if (!this.#config.slack) {
|
|
3324
|
+
return undefined;
|
|
3325
|
+
}
|
|
3326
|
+
if (this.#resolvedSlackChannelDir) {
|
|
3327
|
+
return this.#resolvedSlackChannelDir;
|
|
3328
|
+
}
|
|
3329
|
+
if (this.#slackChannelDirRefresh) {
|
|
3330
|
+
return this.#slackChannelDirRefresh;
|
|
3331
|
+
}
|
|
3332
|
+
this.#slackChannelDirRefresh = this.#resolveSlackChannelDir()
|
|
3333
|
+
.finally(() => {
|
|
3334
|
+
this.#slackChannelDirRefresh = undefined;
|
|
3335
|
+
});
|
|
3336
|
+
return this.#slackChannelDirRefresh;
|
|
3337
|
+
}
|
|
3338
|
+
async #resolveSlackChannelDir() {
|
|
3339
|
+
const configured = this.#config.slack?.channel.trim();
|
|
3340
|
+
if (!configured) {
|
|
3341
|
+
return undefined;
|
|
3342
|
+
}
|
|
3343
|
+
const configuredSegment = slackChannelSegment(configured);
|
|
3344
|
+
const configuredAliases = slackChannelAliases(configured);
|
|
3345
|
+
if (configuredAliases.size === 0) {
|
|
3346
|
+
return undefined;
|
|
3347
|
+
}
|
|
3348
|
+
let paths;
|
|
3349
|
+
try {
|
|
3350
|
+
paths = await this.#mount.listTree('/slack/channels');
|
|
3351
|
+
}
|
|
3352
|
+
catch (error) {
|
|
3353
|
+
this.#logger.warn?.('[factory] unable to resolve Slack channel from mount; using configured channel value', {
|
|
3354
|
+
channel: configured,
|
|
3355
|
+
error,
|
|
3356
|
+
});
|
|
3357
|
+
this.#resolvedSlackChannelDir = configuredSegment;
|
|
3358
|
+
return this.#resolvedSlackChannelDir;
|
|
3359
|
+
}
|
|
3360
|
+
const channelDirs = [...new Set(paths
|
|
3361
|
+
.map((path) => path.match(/^\/slack\/channels\/([^/]+)/u)?.[1])
|
|
3362
|
+
.filter((channelDir) => Boolean(channelDir)))]
|
|
3363
|
+
.sort();
|
|
3364
|
+
const matches = channelDirs.filter((channelDir) => {
|
|
3365
|
+
const aliases = slackChannelAliases(channelDir);
|
|
3366
|
+
return [...aliases].some((alias) => configuredAliases.has(alias));
|
|
3367
|
+
});
|
|
3368
|
+
if (matches.length === 0) {
|
|
3369
|
+
this.#logger.warn?.('[factory] Slack channel was not found in mount; using configured channel value', {
|
|
3370
|
+
channel: configured,
|
|
3371
|
+
});
|
|
3372
|
+
this.#resolvedSlackChannelDir = configuredSegment;
|
|
3373
|
+
return this.#resolvedSlackChannelDir;
|
|
3374
|
+
}
|
|
3375
|
+
const exact = matches.find((channelDir) => slackChannelSegment(channelDir).toLowerCase() === configuredSegment.toLowerCase());
|
|
3376
|
+
this.#resolvedSlackChannelDir = exact ?? matches[0];
|
|
3377
|
+
if (matches.length > 1 && !exact) {
|
|
3378
|
+
this.#logger.warn?.('[factory] Slack channel name matched multiple mount directories; using first match', {
|
|
3379
|
+
channel: configured,
|
|
3380
|
+
selected: this.#resolvedSlackChannelDir,
|
|
3381
|
+
matches,
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
return this.#resolvedSlackChannelDir;
|
|
3385
|
+
}
|
|
3314
3386
|
async #slackDispatchThreadFor(record) {
|
|
3315
3387
|
if (!this.#config.slack) {
|
|
3316
3388
|
return undefined;
|
|
3317
3389
|
}
|
|
3318
3390
|
const threadId = await this.#state.getSlackThread(this.#workspaceId, issueKey(record.issue));
|
|
3391
|
+
const channel = await this.#slackChannelDir() ?? this.#config.slack.channel;
|
|
3319
3392
|
return threadId
|
|
3320
|
-
? { channel
|
|
3393
|
+
? { channel, threadId, mountRoot: this.#integrationsMountRoot() }
|
|
3321
3394
|
: undefined;
|
|
3322
3395
|
}
|
|
3323
3396
|
async #runCompletionMergeGate(issue) {
|
|
@@ -3701,8 +3774,25 @@ function githubMirrorRepoForIssue(issue) {
|
|
|
3701
3774
|
return githubRepoFromUrl(sourceUrlLine?.slice(GITHUB_MIRROR_SOURCE_PREFIX.length));
|
|
3702
3775
|
}
|
|
3703
3776
|
function githubRepoFromUrl(url) {
|
|
3704
|
-
const
|
|
3705
|
-
|
|
3777
|
+
for (const candidate of githubUrlCandidates(url)) {
|
|
3778
|
+
const match = candidate.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/\d+(?:[/?#].*)?$/iu);
|
|
3779
|
+
if (match?.[1] && match[2]) {
|
|
3780
|
+
return `${match[1]}/${match[2]}`;
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
return undefined;
|
|
3784
|
+
}
|
|
3785
|
+
function githubUrlCandidates(value) {
|
|
3786
|
+
if (!value) {
|
|
3787
|
+
return [];
|
|
3788
|
+
}
|
|
3789
|
+
const candidates = new Set();
|
|
3790
|
+
const trimmed = value.trim();
|
|
3791
|
+
candidates.add(trimmed.replace(/^<|>$/gu, ''));
|
|
3792
|
+
for (const match of trimmed.matchAll(/https:\/\/github\.com\/[^\s<>)\]]+/giu)) {
|
|
3793
|
+
candidates.add(match[0].replace(/[),.;]+$/gu, ''));
|
|
3794
|
+
}
|
|
3795
|
+
return [...candidates];
|
|
3706
3796
|
}
|
|
3707
3797
|
function labelRoutesForIssue(issue, config) {
|
|
3708
3798
|
const labels = uniqueNormalizedLabels(issue.labels).filter((label) => !isShapeLabel(label));
|