@agent-relay/factory 0.1.62 → 0.1.63
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 +117 -15
- package/dist/cli/exit-codes.d.ts +81 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +122 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/fleet.d.ts +22 -2
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +152 -33
- package/dist/cli/fleet.js.map +1 -1
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/schema.d.ts +156 -52
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +17 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/dispatch/templates.d.ts +7 -0
- package/dist/dispatch/templates.d.ts.map +1 -1
- package/dist/dispatch/templates.js +64 -12
- package/dist/dispatch/templates.js.map +1 -1
- package/dist/dispatch/work-unit-identity.d.ts +19 -0
- package/dist/dispatch/work-unit-identity.d.ts.map +1 -0
- package/dist/dispatch/work-unit-identity.js +40 -0
- package/dist/dispatch/work-unit-identity.js.map +1 -0
- package/dist/environments/stack-descriptor.d.ts +3 -3
- package/dist/fleet/control-plane-circuit.d.ts +57 -0
- package/dist/fleet/control-plane-circuit.d.ts.map +1 -0
- package/dist/fleet/control-plane-circuit.js +219 -0
- package/dist/fleet/control-plane-circuit.js.map +1 -0
- 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 +11 -7
- package/dist/fleet/internal-fleet-client.js.map +1 -1
- package/dist/fleet/relay-fleet-client.d.ts +3 -0
- package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
- package/dist/fleet/relay-fleet-client.js +97 -18
- package/dist/fleet/relay-fleet-client.js.map +1 -1
- package/dist/github/writeback-paths.d.ts +7 -0
- package/dist/github/writeback-paths.d.ts.map +1 -0
- package/dist/github/writeback-paths.js +28 -0
- package/dist/github/writeback-paths.js.map +1 -0
- package/dist/hosted/orchestrator.d.ts.map +1 -1
- package/dist/hosted/orchestrator.js +2 -0
- package/dist/hosted/orchestrator.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/intake/index.d.ts +2 -1
- package/dist/intake/index.d.ts.map +1 -1
- package/dist/intake/index.js +2 -1
- package/dist/intake/index.js.map +1 -1
- package/dist/intake/notion-manifest.d.ts +48 -0
- package/dist/intake/notion-manifest.d.ts.map +1 -0
- package/dist/intake/notion-manifest.js +258 -0
- package/dist/intake/notion-manifest.js.map +1 -0
- package/dist/intake/notion-relay-claim.d.ts +5 -4
- package/dist/intake/notion-relay-claim.d.ts.map +1 -1
- package/dist/intake/notion-relay-claim.js +48 -7
- package/dist/intake/notion-relay-claim.js.map +1 -1
- package/dist/intake/notion.d.ts +6 -3
- package/dist/intake/notion.d.ts.map +1 -1
- package/dist/intake/notion.js +63 -13
- package/dist/intake/notion.js.map +1 -1
- package/dist/issue-key-match.d.ts +6 -0
- package/dist/issue-key-match.d.ts.map +1 -1
- package/dist/issue-key-match.js +6 -1
- package/dist/issue-key-match.js.map +1 -1
- package/dist/mount/local-mount-preflight.d.ts.map +1 -1
- package/dist/mount/local-mount-preflight.js +6 -2
- package/dist/mount/local-mount-preflight.js.map +1 -1
- package/dist/mount/mount-auth-error.d.ts +16 -3
- package/dist/mount/mount-auth-error.d.ts.map +1 -1
- package/dist/mount/mount-auth-error.js +34 -7
- package/dist/mount/mount-auth-error.js.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts +14 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.js +107 -6
- package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
- package/dist/mount/relayfile-github-connection-write.d.ts +24 -2
- package/dist/mount/relayfile-github-connection-write.d.ts.map +1 -1
- package/dist/mount/relayfile-github-connection-write.js +90 -1
- package/dist/mount/relayfile-github-connection-write.js.map +1 -1
- package/dist/node/factory-node.d.ts.map +1 -1
- package/dist/node/factory-node.js +18 -4
- package/dist/node/factory-node.js.map +1 -1
- package/dist/observability/events.d.ts +52 -52
- package/dist/orchestrator/batch-tracker.d.ts +25 -0
- package/dist/orchestrator/batch-tracker.d.ts.map +1 -1
- package/dist/orchestrator/batch-tracker.js +48 -2
- package/dist/orchestrator/batch-tracker.js.map +1 -1
- package/dist/orchestrator/factory.d.ts +36 -2
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +383 -31
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/orchestrator/index.d.ts +1 -1
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/ports/fleet.d.ts +6 -0
- package/dist/ports/fleet.d.ts.map +1 -1
- package/dist/ports/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/mount.d.ts +39 -0
- package/dist/ports/mount.d.ts.map +1 -1
- package/dist/ports/state.d.ts +15 -1
- package/dist/ports/state.d.ts.map +1 -1
- package/dist/state/document-store.d.ts +32 -0
- package/dist/state/document-store.d.ts.map +1 -0
- package/dist/state/document-store.js +7 -0
- package/dist/state/document-store.js.map +1 -0
- package/dist/state/file-state-store.d.ts +16 -2
- package/dist/state/file-state-store.d.ts.map +1 -1
- package/dist/state/file-state-store.js +45 -242
- package/dist/state/file-state-store.js.map +1 -1
- package/dist/state/watch-state-document.d.ts +5 -0
- package/dist/state/watch-state-document.d.ts.map +1 -0
- package/dist/state/watch-state-document.js +444 -0
- package/dist/state/watch-state-document.js.map +1 -0
- package/dist/testing/deadline.d.ts +11 -0
- package/dist/testing/deadline.d.ts.map +1 -0
- package/dist/testing/deadline.js +23 -0
- package/dist/testing/deadline.js.map +1 -0
- package/dist/testing/fakes.d.ts +2 -0
- package/dist/testing/fakes.d.ts.map +1 -1
- package/dist/testing/fakes.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/triage/heuristic.d.ts +5 -0
- package/dist/triage/heuristic.d.ts.map +1 -1
- package/dist/triage/heuristic.js +42 -3
- package/dist/triage/heuristic.js.map +1 -1
- package/dist/triage/index.d.ts +1 -1
- package/dist/triage/index.d.ts.map +1 -1
- package/dist/triage/index.js +1 -1
- package/dist/triage/index.js.map +1 -1
- package/dist/triage/schema.d.ts +73 -55
- package/dist/triage/schema.d.ts.map +1 -1
- package/dist/triage/schema.js +2 -1
- package/dist/triage/schema.js.map +1 -1
- package/dist/types.d.ts +19 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/writeback/github.d.ts +22 -7
- package/dist/writeback/github.d.ts.map +1 -1
- package/dist/writeback/github.js +86 -16
- package/dist/writeback/github.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/package.json +13 -8
package/dist/cli/fleet.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
5
|
import readline from 'node:readline/promises';
|
|
6
6
|
import { ensureCloudSession } from '@agent-relay/cloud';
|
|
7
7
|
import { stringifyLogValue } from '../logging.js';
|
|
8
8
|
import { resolveLocalFactoryConfig } from '../config/local-clone-paths.js';
|
|
9
9
|
import { initializeFactory } from './init.js';
|
|
10
|
-
import {
|
|
10
|
+
import { FACTORY_EXIT, exitCodeForDispatchResult, exitCodeForError, exitCodeForIterationReport, exitCodeForLoopReports, exitCodeForRelayDispatch, } from './exit-codes.js';
|
|
11
|
+
import { FileStateStore, RelayfileCloudMountClient, checkFactoryLoopLiveness, closeProbePr, createFactory, createFleet, createFactoryCloudEventV1, ensureRelayBroker, FactoryCloudReporter, FileFactoryCloudEventOutbox, defaultGhRunner, explicitLinkedIssueKey, githubIssuePathParts, githubWatchStatePath, isAllowedFactoryGithubDraft, isAllowedFactoryGithubArtifactDraft, isInFactoryScope, parseGithubFactoryIssue, publishFactoryMountHealth, heldAgentsFromRegistry, parseOwnedBrokerAgentExitTimeoutMs, parseStandaloneBabysitTarget, readStandalonePullRequest, readLinearIssueWithCanonicalFallback, reapFactoryOrphansOnce, reapFactoryEnvironmentsOnce, readFactoryInFlightRegistry, readFactoryLoopHeartbeat, resolveFactoryStates, stateResolutionFromIds, standaloneBabysitterAgentName, renderAgentTask, resolveFactoryWorkspace, } from '../index.js';
|
|
11
12
|
import { resolveTestGuidance } from '../dispatch/test-guidance.js';
|
|
12
13
|
import { FakeFleetClient, FakeMountClient } from '../testing/index.js';
|
|
13
14
|
import { GitAgentWorktreeManager } from '../git/agent-worktree.js';
|
|
@@ -18,7 +19,7 @@ import { checkMountStaleness } from '../mount/relayfile-binary.js';
|
|
|
18
19
|
import { MountAuthScopeError } from '../mount/mount-auth-error.js';
|
|
19
20
|
import { resolveRelayWorkspaceKey } from '../fleet/relay-workspace-key.js';
|
|
20
21
|
import { isMeaningfullyBehind, readFactoryVersionInfo, } from '../version-info.js';
|
|
21
|
-
import { GhCliIssuePublisher, RelayChannelNotionClaimStore, RelayChannelNotionContractPublisher, loadNotionIntakeManifest, runNotionIntake, } from '../intake/index.js';
|
|
22
|
+
import { GhCliIssuePublisher, NotionApiFactoryTasksClient, RelayChannelNotionClaimStore, RelayChannelNotionContractPublisher, generateFactoryTasksManifest, loadNotionIntakeManifest, runNotionIntake, } from '../intake/index.js';
|
|
22
23
|
const autoDetectedIssueSources = new WeakSet();
|
|
23
24
|
export async function runFleetCli(argv, deps = {}) {
|
|
24
25
|
const out = deps.stdout ?? process.stdout;
|
|
@@ -51,6 +52,23 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
51
52
|
await initializeFactory({ repo: command.repo, workspaceId: command.workspaceId, stdout: out, stderr: err });
|
|
52
53
|
return 0;
|
|
53
54
|
}
|
|
55
|
+
if (command.kind === 'notion-manifest') {
|
|
56
|
+
const notion = deps.notionFactoryTasks ?? new NotionApiFactoryTasksClient({
|
|
57
|
+
token: (deps.env ?? process.env).NOTION_API_KEY ?? '',
|
|
58
|
+
});
|
|
59
|
+
const manifest = await generateFactoryTasksManifest({
|
|
60
|
+
client: notion,
|
|
61
|
+
...(command.dataSourceId ? { dataSourceId: command.dataSourceId } : {}),
|
|
62
|
+
...(command.mountRoot ? { mountRoot: command.mountRoot } : {}),
|
|
63
|
+
...(command.workerMountRoot ? { workerMountRoot: command.workerMountRoot } : {}),
|
|
64
|
+
...(command.workerMountTransport
|
|
65
|
+
? { workerMountTransport: command.workerMountTransport }
|
|
66
|
+
: {}),
|
|
67
|
+
...(command.statePath ? { statePath: command.statePath } : {}),
|
|
68
|
+
});
|
|
69
|
+
writeJson(out, manifest);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
54
72
|
if (command.kind === 'notion-intake') {
|
|
55
73
|
const manifest = await loadNotionIntakeManifest(command.manifestPath);
|
|
56
74
|
if (!globals.dryRun) {
|
|
@@ -133,7 +151,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
133
151
|
const workspaceId = (await (deps.resolveWorkspace ?? resolveFactoryWorkspace)()).workspaceId;
|
|
134
152
|
mount = deps.mount ?? await (deps.cloudMountFromConfig ?? RelayfileCloudMountClient.fromConfig)({
|
|
135
153
|
workspaceId,
|
|
136
|
-
isAllowedDraft: (path, _content, opts) =>
|
|
154
|
+
isAllowedDraft: (path, _content, opts) => isAllowedFactoryGithubArtifactDraft(path, opts),
|
|
137
155
|
});
|
|
138
156
|
await prepareFactoryIntegrations(command, mount, undefined, globals, deps, workspaceId, err);
|
|
139
157
|
githubWrite = mount.githubWrite;
|
|
@@ -264,10 +282,17 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
264
282
|
// A GitHub-only workspace has no /linear/states catalog and uses labels
|
|
265
283
|
// for lifecycle state, so it must not depend on that provider at startup.
|
|
266
284
|
const stateResolution = await resolveStatesForIssueSource(mount, loaded.config, deps.resolveStates);
|
|
267
|
-
const stateStore =
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
285
|
+
const stateStore = deps.stateStoreFactory
|
|
286
|
+
? await deps.stateStoreFactory(loaded.config)
|
|
287
|
+
: new FileStateStore({
|
|
288
|
+
batchSize: loaded.config.batchSize,
|
|
289
|
+
watchStatePath: githubWatchStatePath(loaded.config.loop.registryPath),
|
|
290
|
+
});
|
|
291
|
+
// An injected backend is an explicit dispatch-admission dependency.
|
|
292
|
+
// It must prove readiness before Factory construction; a host adapter
|
|
293
|
+
// may never turn an unreadable durable store into an empty document.
|
|
294
|
+
if (deps.stateStoreFactory)
|
|
295
|
+
await stateStore.assertReady();
|
|
271
296
|
reporter = deps.reporter ?? await buildFactoryCloudReporter({
|
|
272
297
|
config: loaded.config,
|
|
273
298
|
backend: globals.backend,
|
|
@@ -299,7 +324,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
299
324
|
reporter,
|
|
300
325
|
worktrees: globals.backend === 'internal' ? new GitAgentWorktreeManager() : undefined,
|
|
301
326
|
});
|
|
302
|
-
return await runFactoryCommand(command, factory, mount, fleet, loaded.config, globals, out, deps, workspaceId, acceptableMountIds);
|
|
327
|
+
return await runFactoryCommand(command, factory, mount, fleet, loaded.config, globals, out, deps, workspaceId, acceptableMountIds, stateStore.backend ? { backend: stateStore.backend } : undefined);
|
|
303
328
|
}
|
|
304
329
|
}
|
|
305
330
|
return 1;
|
|
@@ -319,7 +344,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
319
344
|
}
|
|
320
345
|
}
|
|
321
346
|
err.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
322
|
-
return
|
|
347
|
+
return exitCodeForError(error);
|
|
323
348
|
}
|
|
324
349
|
finally {
|
|
325
350
|
try {
|
|
@@ -393,6 +418,32 @@ function parseIntakeCommand(args) {
|
|
|
393
418
|
const [source, manifestPath, ...rest] = args;
|
|
394
419
|
if (source !== 'notion')
|
|
395
420
|
throw new Error('factory intake currently requires the notion source');
|
|
421
|
+
if (manifestPath === 'generate') {
|
|
422
|
+
const parsed = parseFlags(rest);
|
|
423
|
+
const allowed = new Set([
|
|
424
|
+
'data-source',
|
|
425
|
+
'mount-root',
|
|
426
|
+
'worker-mount-root',
|
|
427
|
+
'worker-mount-transport',
|
|
428
|
+
'state-path',
|
|
429
|
+
]);
|
|
430
|
+
const unexpected = Object.keys(parsed).find((key) => !allowed.has(key));
|
|
431
|
+
if (unexpected)
|
|
432
|
+
throw new Error(`Unknown Factory Tasks manifest option: --${unexpected}`);
|
|
433
|
+
const workerMountTransport = parsed['worker-mount-transport'];
|
|
434
|
+
if (workerMountTransport !== undefined &&
|
|
435
|
+
workerMountTransport !== 'local' && workerMountTransport !== 'relay-channel') {
|
|
436
|
+
throw new Error('--worker-mount-transport must be local or relay-channel');
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
kind: 'notion-manifest',
|
|
440
|
+
...(parsed['data-source'] ? { dataSourceId: parsed['data-source'] } : {}),
|
|
441
|
+
...(parsed['mount-root'] ? { mountRoot: parsed['mount-root'] } : {}),
|
|
442
|
+
...(parsed['worker-mount-root'] ? { workerMountRoot: parsed['worker-mount-root'] } : {}),
|
|
443
|
+
...(workerMountTransport ? { workerMountTransport } : {}),
|
|
444
|
+
...(parsed['state-path'] ? { statePath: parsed['state-path'] } : {}),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
396
447
|
if (!manifestPath)
|
|
397
448
|
throw new Error('factory intake notion requires a manifest path');
|
|
398
449
|
if (rest.length > 0)
|
|
@@ -489,7 +540,7 @@ export function parseGlobalOptions(argv) {
|
|
|
489
540
|
}
|
|
490
541
|
return { globals, args };
|
|
491
542
|
}
|
|
492
|
-
async function runFactoryCommand(command, factory, mount, fleet, config, globals, out, deps = {}, workspaceId = config.workspaceId ?? '', acceptableMountIds) {
|
|
543
|
+
async function runFactoryCommand(command, factory, mount, fleet, config, globals, out, deps = {}, workspaceId = config.workspaceId ?? '', acceptableMountIds, stateStoreStatus) {
|
|
493
544
|
const mountFn = resolveLocalMountFn(deps, mount);
|
|
494
545
|
const mountStderr = deps.stderr ?? process.stderr;
|
|
495
546
|
const debugMountRefreshes = (deps.env ?? process.env).FACTORY_LOG_LEVEL?.toLowerCase() === 'debug';
|
|
@@ -528,15 +579,20 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
528
579
|
// would only spawn agents against a read-denied mirror. Fail fast with the
|
|
529
580
|
// remediation and resolve the command with a non-zero code.
|
|
530
581
|
const warmMount = () => warmStartPathMounts(mount, mountFn, workspaceId, config, acceptableMountIds, mountStderr, debugMountRefreshes);
|
|
582
|
+
// Returns the code the failure classifies to, so the inline branch below
|
|
583
|
+
// can surface it directly. The background branch reads the same value
|
|
584
|
+
// back off the waiter; both must report one code for one failure.
|
|
531
585
|
const handleWarmMountError = (error) => {
|
|
532
586
|
if (error instanceof MountAuthScopeError) {
|
|
587
|
+
const code = exitCodeForError(error);
|
|
533
588
|
mountStderr.write(`${error.message}\n`);
|
|
534
589
|
mountStderr.write('[factory] aborting startup: local mount cannot obtain its filesystem scopes.\n');
|
|
535
|
-
void flushAndResolve(
|
|
536
|
-
return;
|
|
590
|
+
void flushAndResolve(code);
|
|
591
|
+
return code;
|
|
537
592
|
}
|
|
538
593
|
const message = error instanceof Error ? error.message : String(error);
|
|
539
594
|
mountStderr.write(`[factory] warning: background relayfile mount warmup failed: ${message}\n`);
|
|
595
|
+
return FACTORY_EXIT.FAILED;
|
|
540
596
|
};
|
|
541
597
|
const removeSignalHandlers = installFactoryStopSignalHandlers(factory, {
|
|
542
598
|
exit: (code) => {
|
|
@@ -556,14 +612,14 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
556
612
|
mountStderr.write('[factory] aborting startup: Relayfile workspace mirror could not be resolved.\n');
|
|
557
613
|
if (stoppedBySignal)
|
|
558
614
|
return await waiter.promise;
|
|
559
|
-
return
|
|
615
|
+
return FACTORY_EXIT.FAILED;
|
|
560
616
|
}
|
|
561
617
|
}
|
|
562
618
|
catch (error) {
|
|
563
|
-
handleWarmMountError(error);
|
|
619
|
+
const code = handleWarmMountError(error);
|
|
564
620
|
if (stoppedBySignal)
|
|
565
621
|
return await waiter.promise;
|
|
566
|
-
return
|
|
622
|
+
return code;
|
|
567
623
|
}
|
|
568
624
|
}
|
|
569
625
|
else {
|
|
@@ -585,12 +641,13 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
585
641
|
}
|
|
586
642
|
if (command.action === 'run-once') {
|
|
587
643
|
await ensureWorkspaceMount(mount, mountFn, workspaceId, acceptableMountIds, mountStderr);
|
|
588
|
-
|
|
589
|
-
|
|
644
|
+
const report = await factory.runOnce({ dryRun: globals.dryRun });
|
|
645
|
+
writeJson(out, report);
|
|
646
|
+
return exitCodeForIterationReport(report);
|
|
590
647
|
}
|
|
591
648
|
if (command.action === 'status') {
|
|
592
649
|
const versionInfo = await safeFactoryVersionInfo(deps.versionInfo, Boolean(deps.versionInfo) || !hasInjectedFactoryRuntime(deps));
|
|
593
|
-
writeJson(out, await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.registryPath, config.loop.heartbeatStaleMs, versionInfo));
|
|
650
|
+
writeJson(out, await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.registryPath, config.loop.heartbeatStaleMs, versionInfo, stateStoreStatus));
|
|
594
651
|
return 0;
|
|
595
652
|
}
|
|
596
653
|
if (command.action === 'loop-status') {
|
|
@@ -611,18 +668,20 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
611
668
|
const removeSignalHandlers = installFactoryStopSignalHandlers(factory, {
|
|
612
669
|
processLike: deps.stopSignalProcessLike,
|
|
613
670
|
});
|
|
671
|
+
let loopCode = FACTORY_EXIT.OK;
|
|
614
672
|
try {
|
|
615
673
|
const reports = await factory.runLoop({ dryRun: globals.dryRun });
|
|
674
|
+
loopCode = exitCodeForLoopReports(reports);
|
|
616
675
|
writeJson(out, {
|
|
617
676
|
reports,
|
|
618
|
-
status: await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.registryPath, config.loop.heartbeatStaleMs),
|
|
677
|
+
status: await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.registryPath, config.loop.heartbeatStaleMs, undefined, stateStoreStatus),
|
|
619
678
|
});
|
|
620
679
|
}
|
|
621
680
|
finally {
|
|
622
681
|
removeSignalHandlers();
|
|
623
682
|
await factory.stop();
|
|
624
683
|
}
|
|
625
|
-
return
|
|
684
|
+
return loopCode;
|
|
626
685
|
}
|
|
627
686
|
if (command.kind === 'factory-canary') {
|
|
628
687
|
const report = await factory.runOnce({ dryRun: true });
|
|
@@ -647,15 +706,16 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
647
706
|
try {
|
|
648
707
|
const result = await factory.dispatch(decision, { dryRun: false });
|
|
649
708
|
writeJson(out, result);
|
|
650
|
-
await factory.waitForDispatchTerminal(result.issue);
|
|
651
|
-
return
|
|
709
|
+
const terminalPhase = await factory.waitForDispatchTerminal(result.issue);
|
|
710
|
+
return exitCodeForRelayDispatch(result, terminalPhase);
|
|
652
711
|
}
|
|
653
712
|
finally {
|
|
654
713
|
await factory.stop();
|
|
655
714
|
}
|
|
656
715
|
}
|
|
657
|
-
|
|
658
|
-
|
|
716
|
+
const dispatched = await factory.dispatch(decision, { dryRun: globals.dryRun });
|
|
717
|
+
writeJson(out, dispatched);
|
|
718
|
+
return exitCodeForDispatchResult(dispatched);
|
|
659
719
|
}
|
|
660
720
|
async function warmStartPathMounts(mount, mountFn, workspaceId, config, acceptableMountIds, stderr = process.stderr, debug = process.env.FACTORY_LOG_LEVEL?.toLowerCase() === 'debug') {
|
|
661
721
|
const result = await ensureWorkspaceMount(mount, mountFn, workspaceId, acceptableMountIds, stderr);
|
|
@@ -829,7 +889,7 @@ async function ensureMountPath(mountFn, workspaceId, path, mountOpts, stderr, lo
|
|
|
829
889
|
function resolveIntegrationsMountRoot(mount) {
|
|
830
890
|
return mount.getLocalMountRoot?.() ?? resolve(process.cwd(), '.integrations');
|
|
831
891
|
}
|
|
832
|
-
async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, registryPath, heartbeatStaleMs, versionInfo) {
|
|
892
|
+
async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, registryPath, heartbeatStaleMs, versionInfo, stateStoreStatus) {
|
|
833
893
|
const status = factory.status();
|
|
834
894
|
const [heartbeat, registry] = await Promise.all([
|
|
835
895
|
readFactoryLoopHeartbeat(heartbeatPath),
|
|
@@ -851,6 +911,9 @@ async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, regis
|
|
|
851
911
|
const readinessReconcile = liveness.ok
|
|
852
912
|
? heartbeat?.readinessReconcile
|
|
853
913
|
: observableStatus.readinessReconcile;
|
|
914
|
+
const fleetControlPlane = liveness.ok
|
|
915
|
+
? heartbeat?.fleetControlPlane
|
|
916
|
+
: observableStatus.fleetControlPlane;
|
|
854
917
|
const eventListener = liveness.ok
|
|
855
918
|
? heartbeat?.eventListener ?? {
|
|
856
919
|
state: 'unknown',
|
|
@@ -862,13 +925,23 @@ async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, regis
|
|
|
862
925
|
};
|
|
863
926
|
const health = mount.getLocalMountHealth?.();
|
|
864
927
|
if (!health)
|
|
865
|
-
return {
|
|
928
|
+
return {
|
|
929
|
+
...observableStatus,
|
|
930
|
+
...versionInfo,
|
|
931
|
+
...(stateStoreStatus ? { stateStore: stateStoreStatus } : {}),
|
|
932
|
+
heldAgents,
|
|
933
|
+
eventListener,
|
|
934
|
+
readinessReconcile,
|
|
935
|
+
fleetControlPlane,
|
|
936
|
+
};
|
|
866
937
|
return {
|
|
867
938
|
...observableStatus,
|
|
868
939
|
...versionInfo,
|
|
940
|
+
...(stateStoreStatus ? { stateStore: stateStoreStatus } : {}),
|
|
869
941
|
heldAgents,
|
|
870
942
|
eventListener,
|
|
871
943
|
readinessReconcile,
|
|
944
|
+
fleetControlPlane,
|
|
872
945
|
localMountDegraded: health.degraded,
|
|
873
946
|
...(health.reason ? { localMountDegradedReason: health.reason } : {}),
|
|
874
947
|
...(health.localDir ? { localMountRoot: health.localDir } : {}),
|
|
@@ -1194,7 +1267,9 @@ async function buildFleet(globals, loaded, deps) {
|
|
|
1194
1267
|
return new FakeFleetClient();
|
|
1195
1268
|
const cwd = process.cwd();
|
|
1196
1269
|
const env = deps.env ?? process.env;
|
|
1197
|
-
const connectionPath =
|
|
1270
|
+
const connectionPath = globals.backend === 'internal'
|
|
1271
|
+
? resolveFactoryBrokerConnectionPath(cwd, env, loaded?.config.fleetHealth.requireDedicatedBroker ?? false)
|
|
1272
|
+
: resolveBrokerConnectionPath(cwd, env);
|
|
1198
1273
|
// An injected createFleet owns fleet construction entirely (tests), so skip the
|
|
1199
1274
|
// real broker bootstrap.
|
|
1200
1275
|
if (deps.createFleet) {
|
|
@@ -1271,6 +1346,51 @@ export function resolveBrokerConnectionPath(startCwd = process.cwd(), env = proc
|
|
|
1271
1346
|
current = parent;
|
|
1272
1347
|
}
|
|
1273
1348
|
}
|
|
1349
|
+
export function resolveFactoryBrokerConnectionPath(startCwd = process.cwd(), env = process.env, requireDedicatedBroker = false) {
|
|
1350
|
+
const connectionPath = resolveBrokerConnectionPath(startCwd, env);
|
|
1351
|
+
if (!requireDedicatedBroker)
|
|
1352
|
+
return connectionPath;
|
|
1353
|
+
const explicitStateDir = env.AGENT_RELAY_STATE_DIR?.trim();
|
|
1354
|
+
if (!explicitStateDir) {
|
|
1355
|
+
throw new Error('fleetHealth.requireDedicatedBroker requires AGENT_RELAY_STATE_DIR to name Factory\'s isolated relay state directory');
|
|
1356
|
+
}
|
|
1357
|
+
const inheritedConnectionPath = resolveBrokerConnectionPath(startCwd, {
|
|
1358
|
+
...env,
|
|
1359
|
+
AGENT_RELAY_STATE_DIR: '',
|
|
1360
|
+
});
|
|
1361
|
+
// Always reject the checkout-local path, even if broker discovery currently
|
|
1362
|
+
// walks past it and finds an ancestor connection. An interactive relay
|
|
1363
|
+
// started in this checkout can claim the local path later.
|
|
1364
|
+
const sharedStateDirs = [
|
|
1365
|
+
join(resolve(startCwd), '.agentworkforce', 'relay'),
|
|
1366
|
+
...(inheritedConnectionPath ? [dirname(inheritedConnectionPath)] : []),
|
|
1367
|
+
];
|
|
1368
|
+
if (sharedStateDirs.some((stateDir) => sameFilesystemPath(explicitStateDir, stateDir))) {
|
|
1369
|
+
throw new Error('AGENT_RELAY_STATE_DIR resolves to the project broker; choose a separate state directory for Factory');
|
|
1370
|
+
}
|
|
1371
|
+
return connectionPath;
|
|
1372
|
+
}
|
|
1373
|
+
function sameFilesystemPath(left, right) {
|
|
1374
|
+
return canonicalPath(left) === canonicalPath(right);
|
|
1375
|
+
}
|
|
1376
|
+
function canonicalPath(path) {
|
|
1377
|
+
const absolute = resolve(path);
|
|
1378
|
+
const missingSegments = [];
|
|
1379
|
+
let existingAncestor = absolute;
|
|
1380
|
+
while (!existsSync(existingAncestor)) {
|
|
1381
|
+
const parent = dirname(existingAncestor);
|
|
1382
|
+
if (parent === existingAncestor)
|
|
1383
|
+
return absolute;
|
|
1384
|
+
missingSegments.unshift(basename(existingAncestor));
|
|
1385
|
+
existingAncestor = parent;
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
return resolve(realpathSync.native(existingAncestor), ...missingSegments);
|
|
1389
|
+
}
|
|
1390
|
+
catch {
|
|
1391
|
+
return absolute;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1274
1394
|
async function prepareFactoryIntegrations(command, mount, config, globals, deps, workspaceId, err) {
|
|
1275
1395
|
const connections = mount.integrationConnections;
|
|
1276
1396
|
if (!connections)
|
|
@@ -1479,13 +1599,10 @@ async function isAllowedFactoryDraft(path, content, opts, mount, config) {
|
|
|
1479
1599
|
if (/^\/slack\/channels\/[^/]+\/messages\/.+/u.test(path)) {
|
|
1480
1600
|
return true;
|
|
1481
1601
|
}
|
|
1482
|
-
if (isAllowedFactoryGithubDraft(path, opts))
|
|
1602
|
+
if (await isAllowedFactoryGithubDraft(path, content, opts, mount, config))
|
|
1483
1603
|
return true;
|
|
1484
|
-
}
|
|
1485
1604
|
return false;
|
|
1486
1605
|
}
|
|
1487
|
-
const isFactoryGithubWritebackPath = (path) => /^\/github\/repos\/[^/]+\/[^/]+\/(?:pull-requests\/factory-[^/]+\.json|refs\/(?:factory\.json|refs%2Fheads%2Ffactory%2F[^/]+\.json)|pulls\/[1-9]\d*\/close\.json)$/iu.test(path);
|
|
1488
|
-
const isAllowedFactoryGithubDraft = (path, opts) => opts?.guarded === true && isFactoryGithubWritebackPath(path);
|
|
1489
1606
|
const scopeIssueFromDraftContent = (content) => ({
|
|
1490
1607
|
title: typeof asRecord(content)?.title === 'string' ? asRecord(content)?.title : '',
|
|
1491
1608
|
team: typeof asRecord(asRecord(content)?.team)?.key === 'string'
|
|
@@ -1933,6 +2050,8 @@ Commands:
|
|
|
1933
2050
|
close-probe <PR> Probe/close a PR for an issue
|
|
1934
2051
|
featuremap check Validate .agentworkforce/features/manifest.yaml
|
|
1935
2052
|
intake notion <file> Normalize mounted Notion specs into Factory work
|
|
2053
|
+
intake notion generate
|
|
2054
|
+
Emit a manifest for Ready for Agent Factory Tasks
|
|
1936
2055
|
fleet <command> Low-level fleet commands: spawn, roster, release
|
|
1937
2056
|
|
|
1938
2057
|
Options:
|