@agent-relay/factory 0.1.62 → 0.1.64
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 +26 -2
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +183 -40
- 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 +231 -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 +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -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 +36 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.js +136 -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/cloud-reporter.d.ts +1 -0
- package/dist/observability/cloud-reporter.d.ts.map +1 -1
- package/dist/observability/cloud-reporter.js +182 -50
- package/dist/observability/cloud-reporter.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 +1208 -82
- 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 +75 -4
- package/dist/ports/state.d.ts.map +1 -1
- package/dist/state/document-store.d.ts +33 -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 +32 -7
- package/dist/state/file-state-store.d.ts.map +1 -1
- package/dist/state/file-state-store.js +169 -250
- package/dist/state/file-state-store.js.map +1 -1
- package/dist/state/in-memory-state-store.d.ts +13 -2
- package/dist/state/in-memory-state-store.d.ts.map +1 -1
- package/dist/state/in-memory-state-store.js +87 -4
- package/dist/state/in-memory-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 +486 -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/trajectory.d.ts +25 -0
- package/dist/trajectory.d.ts.map +1 -0
- package/dist/trajectory.js +51 -0
- package/dist/trajectory.js.map +1 -0
- 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';
|
|
@@ -16,9 +17,10 @@ import { loadOrCreateFactoryInstanceId, resolveFactoryInstanceName } from '../ob
|
|
|
16
17
|
import { ensureFactoryIntegrations, inspectFactoryIntegration, openIntegrationUrl, } from '../mount/relayfile-integration-preflight.js';
|
|
17
18
|
import { checkMountStaleness } from '../mount/relayfile-binary.js';
|
|
18
19
|
import { MountAuthScopeError } from '../mount/mount-auth-error.js';
|
|
20
|
+
import { createHostedCloudAccessTokenProvider, hostedCloudAccessTokenUrl, resolveHostedCloudApiUrl, } from '../mount/relayfile-cloud-mount-client.js';
|
|
19
21
|
import { resolveRelayWorkspaceKey } from '../fleet/relay-workspace-key.js';
|
|
20
22
|
import { isMeaningfullyBehind, readFactoryVersionInfo, } from '../version-info.js';
|
|
21
|
-
import { GhCliIssuePublisher, RelayChannelNotionClaimStore, RelayChannelNotionContractPublisher, loadNotionIntakeManifest, runNotionIntake, } from '../intake/index.js';
|
|
23
|
+
import { GhCliIssuePublisher, NotionApiFactoryTasksClient, RelayChannelNotionClaimStore, RelayChannelNotionContractPublisher, generateFactoryTasksManifest, loadNotionIntakeManifest, runNotionIntake, } from '../intake/index.js';
|
|
22
24
|
const autoDetectedIssueSources = new WeakSet();
|
|
23
25
|
export async function runFleetCli(argv, deps = {}) {
|
|
24
26
|
const out = deps.stdout ?? process.stdout;
|
|
@@ -51,6 +53,23 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
51
53
|
await initializeFactory({ repo: command.repo, workspaceId: command.workspaceId, stdout: out, stderr: err });
|
|
52
54
|
return 0;
|
|
53
55
|
}
|
|
56
|
+
if (command.kind === 'notion-manifest') {
|
|
57
|
+
const notion = deps.notionFactoryTasks ?? new NotionApiFactoryTasksClient({
|
|
58
|
+
token: (deps.env ?? process.env).NOTION_API_KEY ?? '',
|
|
59
|
+
});
|
|
60
|
+
const manifest = await generateFactoryTasksManifest({
|
|
61
|
+
client: notion,
|
|
62
|
+
...(command.dataSourceId ? { dataSourceId: command.dataSourceId } : {}),
|
|
63
|
+
...(command.mountRoot ? { mountRoot: command.mountRoot } : {}),
|
|
64
|
+
...(command.workerMountRoot ? { workerMountRoot: command.workerMountRoot } : {}),
|
|
65
|
+
...(command.workerMountTransport
|
|
66
|
+
? { workerMountTransport: command.workerMountTransport }
|
|
67
|
+
: {}),
|
|
68
|
+
...(command.statePath ? { statePath: command.statePath } : {}),
|
|
69
|
+
});
|
|
70
|
+
writeJson(out, manifest);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
54
73
|
if (command.kind === 'notion-intake') {
|
|
55
74
|
const manifest = await loadNotionIntakeManifest(command.manifestPath);
|
|
56
75
|
if (!globals.dryRun) {
|
|
@@ -133,7 +152,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
133
152
|
const workspaceId = (await (deps.resolveWorkspace ?? resolveFactoryWorkspace)()).workspaceId;
|
|
134
153
|
mount = deps.mount ?? await (deps.cloudMountFromConfig ?? RelayfileCloudMountClient.fromConfig)({
|
|
135
154
|
workspaceId,
|
|
136
|
-
isAllowedDraft: (path, _content, opts) =>
|
|
155
|
+
isAllowedDraft: (path, _content, opts) => isAllowedFactoryGithubArtifactDraft(path, opts),
|
|
137
156
|
});
|
|
138
157
|
await prepareFactoryIntegrations(command, mount, undefined, globals, deps, workspaceId, err);
|
|
139
158
|
githubWrite = mount.githubWrite;
|
|
@@ -264,10 +283,17 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
264
283
|
// A GitHub-only workspace has no /linear/states catalog and uses labels
|
|
265
284
|
// for lifecycle state, so it must not depend on that provider at startup.
|
|
266
285
|
const stateResolution = await resolveStatesForIssueSource(mount, loaded.config, deps.resolveStates);
|
|
267
|
-
const stateStore =
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
286
|
+
const stateStore = deps.stateStoreFactory
|
|
287
|
+
? await deps.stateStoreFactory(loaded.config)
|
|
288
|
+
: new FileStateStore({
|
|
289
|
+
batchSize: loaded.config.batchSize,
|
|
290
|
+
watchStatePath: githubWatchStatePath(loaded.config.loop.registryPath),
|
|
291
|
+
});
|
|
292
|
+
// An injected backend is an explicit dispatch-admission dependency.
|
|
293
|
+
// It must prove readiness before Factory construction; a host adapter
|
|
294
|
+
// may never turn an unreadable durable store into an empty document.
|
|
295
|
+
if (deps.stateStoreFactory)
|
|
296
|
+
await stateStore.assertReady();
|
|
271
297
|
reporter = deps.reporter ?? await buildFactoryCloudReporter({
|
|
272
298
|
config: loaded.config,
|
|
273
299
|
backend: globals.backend,
|
|
@@ -299,7 +325,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
299
325
|
reporter,
|
|
300
326
|
worktrees: globals.backend === 'internal' ? new GitAgentWorktreeManager() : undefined,
|
|
301
327
|
});
|
|
302
|
-
return await runFactoryCommand(command, factory, mount, fleet, loaded.config, globals, out, deps, workspaceId, acceptableMountIds);
|
|
328
|
+
return await runFactoryCommand(command, factory, mount, fleet, loaded.config, globals, out, deps, workspaceId, acceptableMountIds, stateStore.backend ? { backend: stateStore.backend } : undefined);
|
|
303
329
|
}
|
|
304
330
|
}
|
|
305
331
|
return 1;
|
|
@@ -319,7 +345,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
319
345
|
}
|
|
320
346
|
}
|
|
321
347
|
err.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
322
|
-
return
|
|
348
|
+
return exitCodeForError(error);
|
|
323
349
|
}
|
|
324
350
|
finally {
|
|
325
351
|
try {
|
|
@@ -393,6 +419,32 @@ function parseIntakeCommand(args) {
|
|
|
393
419
|
const [source, manifestPath, ...rest] = args;
|
|
394
420
|
if (source !== 'notion')
|
|
395
421
|
throw new Error('factory intake currently requires the notion source');
|
|
422
|
+
if (manifestPath === 'generate') {
|
|
423
|
+
const parsed = parseFlags(rest);
|
|
424
|
+
const allowed = new Set([
|
|
425
|
+
'data-source',
|
|
426
|
+
'mount-root',
|
|
427
|
+
'worker-mount-root',
|
|
428
|
+
'worker-mount-transport',
|
|
429
|
+
'state-path',
|
|
430
|
+
]);
|
|
431
|
+
const unexpected = Object.keys(parsed).find((key) => !allowed.has(key));
|
|
432
|
+
if (unexpected)
|
|
433
|
+
throw new Error(`Unknown Factory Tasks manifest option: --${unexpected}`);
|
|
434
|
+
const workerMountTransport = parsed['worker-mount-transport'];
|
|
435
|
+
if (workerMountTransport !== undefined &&
|
|
436
|
+
workerMountTransport !== 'local' && workerMountTransport !== 'relay-channel') {
|
|
437
|
+
throw new Error('--worker-mount-transport must be local or relay-channel');
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
kind: 'notion-manifest',
|
|
441
|
+
...(parsed['data-source'] ? { dataSourceId: parsed['data-source'] } : {}),
|
|
442
|
+
...(parsed['mount-root'] ? { mountRoot: parsed['mount-root'] } : {}),
|
|
443
|
+
...(parsed['worker-mount-root'] ? { workerMountRoot: parsed['worker-mount-root'] } : {}),
|
|
444
|
+
...(workerMountTransport ? { workerMountTransport } : {}),
|
|
445
|
+
...(parsed['state-path'] ? { statePath: parsed['state-path'] } : {}),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
396
448
|
if (!manifestPath)
|
|
397
449
|
throw new Error('factory intake notion requires a manifest path');
|
|
398
450
|
if (rest.length > 0)
|
|
@@ -489,7 +541,7 @@ export function parseGlobalOptions(argv) {
|
|
|
489
541
|
}
|
|
490
542
|
return { globals, args };
|
|
491
543
|
}
|
|
492
|
-
async function runFactoryCommand(command, factory, mount, fleet, config, globals, out, deps = {}, workspaceId = config.workspaceId ?? '', acceptableMountIds) {
|
|
544
|
+
async function runFactoryCommand(command, factory, mount, fleet, config, globals, out, deps = {}, workspaceId = config.workspaceId ?? '', acceptableMountIds, stateStoreStatus) {
|
|
493
545
|
const mountFn = resolveLocalMountFn(deps, mount);
|
|
494
546
|
const mountStderr = deps.stderr ?? process.stderr;
|
|
495
547
|
const debugMountRefreshes = (deps.env ?? process.env).FACTORY_LOG_LEVEL?.toLowerCase() === 'debug';
|
|
@@ -528,15 +580,20 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
528
580
|
// would only spawn agents against a read-denied mirror. Fail fast with the
|
|
529
581
|
// remediation and resolve the command with a non-zero code.
|
|
530
582
|
const warmMount = () => warmStartPathMounts(mount, mountFn, workspaceId, config, acceptableMountIds, mountStderr, debugMountRefreshes);
|
|
583
|
+
// Returns the code the failure classifies to, so the inline branch below
|
|
584
|
+
// can surface it directly. The background branch reads the same value
|
|
585
|
+
// back off the waiter; both must report one code for one failure.
|
|
531
586
|
const handleWarmMountError = (error) => {
|
|
532
587
|
if (error instanceof MountAuthScopeError) {
|
|
588
|
+
const code = exitCodeForError(error);
|
|
533
589
|
mountStderr.write(`${error.message}\n`);
|
|
534
590
|
mountStderr.write('[factory] aborting startup: local mount cannot obtain its filesystem scopes.\n');
|
|
535
|
-
void flushAndResolve(
|
|
536
|
-
return;
|
|
591
|
+
void flushAndResolve(code);
|
|
592
|
+
return code;
|
|
537
593
|
}
|
|
538
594
|
const message = error instanceof Error ? error.message : String(error);
|
|
539
595
|
mountStderr.write(`[factory] warning: background relayfile mount warmup failed: ${message}\n`);
|
|
596
|
+
return FACTORY_EXIT.FAILED;
|
|
540
597
|
};
|
|
541
598
|
const removeSignalHandlers = installFactoryStopSignalHandlers(factory, {
|
|
542
599
|
exit: (code) => {
|
|
@@ -556,14 +613,14 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
556
613
|
mountStderr.write('[factory] aborting startup: Relayfile workspace mirror could not be resolved.\n');
|
|
557
614
|
if (stoppedBySignal)
|
|
558
615
|
return await waiter.promise;
|
|
559
|
-
return
|
|
616
|
+
return FACTORY_EXIT.FAILED;
|
|
560
617
|
}
|
|
561
618
|
}
|
|
562
619
|
catch (error) {
|
|
563
|
-
handleWarmMountError(error);
|
|
620
|
+
const code = handleWarmMountError(error);
|
|
564
621
|
if (stoppedBySignal)
|
|
565
622
|
return await waiter.promise;
|
|
566
|
-
return
|
|
623
|
+
return code;
|
|
567
624
|
}
|
|
568
625
|
}
|
|
569
626
|
else {
|
|
@@ -585,12 +642,13 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
585
642
|
}
|
|
586
643
|
if (command.action === 'run-once') {
|
|
587
644
|
await ensureWorkspaceMount(mount, mountFn, workspaceId, acceptableMountIds, mountStderr);
|
|
588
|
-
|
|
589
|
-
|
|
645
|
+
const report = await factory.runOnce({ dryRun: globals.dryRun });
|
|
646
|
+
writeJson(out, report);
|
|
647
|
+
return exitCodeForIterationReport(report);
|
|
590
648
|
}
|
|
591
649
|
if (command.action === 'status') {
|
|
592
650
|
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));
|
|
651
|
+
writeJson(out, await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.registryPath, config.loop.heartbeatStaleMs, versionInfo, stateStoreStatus));
|
|
594
652
|
return 0;
|
|
595
653
|
}
|
|
596
654
|
if (command.action === 'loop-status') {
|
|
@@ -611,18 +669,20 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
611
669
|
const removeSignalHandlers = installFactoryStopSignalHandlers(factory, {
|
|
612
670
|
processLike: deps.stopSignalProcessLike,
|
|
613
671
|
});
|
|
672
|
+
let loopCode = FACTORY_EXIT.OK;
|
|
614
673
|
try {
|
|
615
674
|
const reports = await factory.runLoop({ dryRun: globals.dryRun });
|
|
675
|
+
loopCode = exitCodeForLoopReports(reports);
|
|
616
676
|
writeJson(out, {
|
|
617
677
|
reports,
|
|
618
|
-
status: await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.registryPath, config.loop.heartbeatStaleMs),
|
|
678
|
+
status: await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.registryPath, config.loop.heartbeatStaleMs, undefined, stateStoreStatus),
|
|
619
679
|
});
|
|
620
680
|
}
|
|
621
681
|
finally {
|
|
622
682
|
removeSignalHandlers();
|
|
623
683
|
await factory.stop();
|
|
624
684
|
}
|
|
625
|
-
return
|
|
685
|
+
return loopCode;
|
|
626
686
|
}
|
|
627
687
|
if (command.kind === 'factory-canary') {
|
|
628
688
|
const report = await factory.runOnce({ dryRun: true });
|
|
@@ -647,15 +707,16 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
647
707
|
try {
|
|
648
708
|
const result = await factory.dispatch(decision, { dryRun: false });
|
|
649
709
|
writeJson(out, result);
|
|
650
|
-
await factory.waitForDispatchTerminal(result.issue);
|
|
651
|
-
return
|
|
710
|
+
const terminalPhase = await factory.waitForDispatchTerminal(result.issue);
|
|
711
|
+
return exitCodeForRelayDispatch(result, terminalPhase);
|
|
652
712
|
}
|
|
653
713
|
finally {
|
|
654
714
|
await factory.stop();
|
|
655
715
|
}
|
|
656
716
|
}
|
|
657
|
-
|
|
658
|
-
|
|
717
|
+
const dispatched = await factory.dispatch(decision, { dryRun: globals.dryRun });
|
|
718
|
+
writeJson(out, dispatched);
|
|
719
|
+
return exitCodeForDispatchResult(dispatched);
|
|
659
720
|
}
|
|
660
721
|
async function warmStartPathMounts(mount, mountFn, workspaceId, config, acceptableMountIds, stderr = process.stderr, debug = process.env.FACTORY_LOG_LEVEL?.toLowerCase() === 'debug') {
|
|
661
722
|
const result = await ensureWorkspaceMount(mount, mountFn, workspaceId, acceptableMountIds, stderr);
|
|
@@ -829,7 +890,7 @@ async function ensureMountPath(mountFn, workspaceId, path, mountOpts, stderr, lo
|
|
|
829
890
|
function resolveIntegrationsMountRoot(mount) {
|
|
830
891
|
return mount.getLocalMountRoot?.() ?? resolve(process.cwd(), '.integrations');
|
|
831
892
|
}
|
|
832
|
-
async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, registryPath, heartbeatStaleMs, versionInfo) {
|
|
893
|
+
async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, registryPath, heartbeatStaleMs, versionInfo, stateStoreStatus) {
|
|
833
894
|
const status = factory.status();
|
|
834
895
|
const [heartbeat, registry] = await Promise.all([
|
|
835
896
|
readFactoryLoopHeartbeat(heartbeatPath),
|
|
@@ -851,6 +912,9 @@ async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, regis
|
|
|
851
912
|
const readinessReconcile = liveness.ok
|
|
852
913
|
? heartbeat?.readinessReconcile
|
|
853
914
|
: observableStatus.readinessReconcile;
|
|
915
|
+
const fleetControlPlane = liveness.ok
|
|
916
|
+
? heartbeat?.fleetControlPlane
|
|
917
|
+
: observableStatus.fleetControlPlane;
|
|
854
918
|
const eventListener = liveness.ok
|
|
855
919
|
? heartbeat?.eventListener ?? {
|
|
856
920
|
state: 'unknown',
|
|
@@ -862,13 +926,23 @@ async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, regis
|
|
|
862
926
|
};
|
|
863
927
|
const health = mount.getLocalMountHealth?.();
|
|
864
928
|
if (!health)
|
|
865
|
-
return {
|
|
929
|
+
return {
|
|
930
|
+
...observableStatus,
|
|
931
|
+
...versionInfo,
|
|
932
|
+
...(stateStoreStatus ? { stateStore: stateStoreStatus } : {}),
|
|
933
|
+
heldAgents,
|
|
934
|
+
eventListener,
|
|
935
|
+
readinessReconcile,
|
|
936
|
+
fleetControlPlane,
|
|
937
|
+
};
|
|
866
938
|
return {
|
|
867
939
|
...observableStatus,
|
|
868
940
|
...versionInfo,
|
|
941
|
+
...(stateStoreStatus ? { stateStore: stateStoreStatus } : {}),
|
|
869
942
|
heldAgents,
|
|
870
943
|
eventListener,
|
|
871
944
|
readinessReconcile,
|
|
945
|
+
fleetControlPlane,
|
|
872
946
|
localMountDegraded: health.degraded,
|
|
873
947
|
...(health.reason ? { localMountDegradedReason: health.reason } : {}),
|
|
874
948
|
...(health.localDir ? { localMountRoot: health.localDir } : {}),
|
|
@@ -1135,10 +1209,18 @@ function factoryReportingMode(command) {
|
|
|
1135
1209
|
async function buildFactoryCloudReporter(input) {
|
|
1136
1210
|
if (!input.config.reporting.enabled)
|
|
1137
1211
|
return undefined;
|
|
1138
|
-
|
|
1212
|
+
const runtimeEnv = input.deps.env ?? process.env;
|
|
1213
|
+
// Hosted mode is a usable endpoint, not a defined-but-blank variable: an
|
|
1214
|
+
// empty value must fall through to the local-session path instead of
|
|
1215
|
+
// reaching createHostedCloudAccessTokenProvider, which throws on `new URL('')`.
|
|
1216
|
+
const hostedAccessTokenUrl = hostedCloudAccessTokenUrl(runtimeEnv);
|
|
1217
|
+
if (hasInjectedFactoryRuntime(input.deps)
|
|
1218
|
+
&& !input.deps.cloudSessionProvider
|
|
1219
|
+
&& !hostedAccessTokenUrl)
|
|
1139
1220
|
return undefined;
|
|
1140
1221
|
try {
|
|
1141
|
-
const activeWorkspace = await (input.deps.resolveWorkspace
|
|
1222
|
+
const activeWorkspace = await (input.deps.resolveWorkspace
|
|
1223
|
+
?? (() => resolveFactoryWorkspace(undefined, runtimeEnv)))();
|
|
1142
1224
|
const activeWorkspaceIds = new Set([
|
|
1143
1225
|
activeWorkspace.workspaceId,
|
|
1144
1226
|
activeWorkspace.cloudWorkspaceId,
|
|
@@ -1147,14 +1229,29 @@ async function buildFactoryCloudReporter(input) {
|
|
|
1147
1229
|
input.logger.warn?.('[factory] Cloud progress reporting skipped because the active account workspace differs from Factory config');
|
|
1148
1230
|
return undefined;
|
|
1149
1231
|
}
|
|
1150
|
-
const session = await (input.deps.cloudSessionProvider ?? ensureCloudSession)({ interactive: false });
|
|
1151
1232
|
const outboxPath = input.config.reporting.outboxPath
|
|
1152
1233
|
?? join(dirname(input.config.loop.registryPath), 'factory-cloud-events.json');
|
|
1153
1234
|
const instanceId = await loadOrCreateFactoryInstanceId(`${outboxPath}.instance-id`);
|
|
1154
1235
|
const instanceName = resolveFactoryInstanceName(input.config);
|
|
1155
|
-
|
|
1236
|
+
let apiUrl;
|
|
1237
|
+
let getAccessToken;
|
|
1238
|
+
let cloudFetch;
|
|
1239
|
+
if (hostedAccessTokenUrl) {
|
|
1240
|
+
apiUrl = resolveHostedCloudApiUrl(runtimeEnv);
|
|
1241
|
+
getAccessToken = createHostedCloudAccessTokenProvider({
|
|
1242
|
+
url: hostedAccessTokenUrl,
|
|
1243
|
+
fetchImpl: input.deps.cloudAccessTokenFetch ?? fetch,
|
|
1244
|
+
});
|
|
1245
|
+
cloudFetch = input.deps.cloudReporterFetch;
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
const session = await (input.deps.cloudSessionProvider ?? ensureCloudSession)({ interactive: false });
|
|
1249
|
+
apiUrl = session.auth.apiUrl;
|
|
1250
|
+
getAccessToken = async () => session.client.snapshot().accessToken;
|
|
1251
|
+
cloudFetch = async (_request, init) => session.client.fetch('/api/v1/factory/events', init);
|
|
1252
|
+
}
|
|
1156
1253
|
return new FactoryCloudReporter({
|
|
1157
|
-
apiUrl
|
|
1254
|
+
apiUrl,
|
|
1158
1255
|
instance: {
|
|
1159
1256
|
id: instanceId,
|
|
1160
1257
|
bootId: randomUUID(),
|
|
@@ -1167,8 +1264,8 @@ async function buildFactoryCloudReporter(input) {
|
|
|
1167
1264
|
},
|
|
1168
1265
|
},
|
|
1169
1266
|
outbox: new FileFactoryCloudEventOutbox({ path: outboxPath }),
|
|
1170
|
-
getAccessToken
|
|
1171
|
-
fetch: cloudFetch,
|
|
1267
|
+
getAccessToken,
|
|
1268
|
+
...(cloudFetch ? { fetch: cloudFetch } : {}),
|
|
1172
1269
|
logger: input.logger,
|
|
1173
1270
|
batchSize: input.config.reporting.batchSize,
|
|
1174
1271
|
requestTimeoutMs: input.config.reporting.requestTimeoutMs,
|
|
@@ -1194,7 +1291,9 @@ async function buildFleet(globals, loaded, deps) {
|
|
|
1194
1291
|
return new FakeFleetClient();
|
|
1195
1292
|
const cwd = process.cwd();
|
|
1196
1293
|
const env = deps.env ?? process.env;
|
|
1197
|
-
const connectionPath =
|
|
1294
|
+
const connectionPath = globals.backend === 'internal'
|
|
1295
|
+
? resolveFactoryBrokerConnectionPath(cwd, env, loaded?.config.fleetHealth.requireDedicatedBroker ?? false)
|
|
1296
|
+
: resolveBrokerConnectionPath(cwd, env);
|
|
1198
1297
|
// An injected createFleet owns fleet construction entirely (tests), so skip the
|
|
1199
1298
|
// real broker bootstrap.
|
|
1200
1299
|
if (deps.createFleet) {
|
|
@@ -1271,6 +1370,51 @@ export function resolveBrokerConnectionPath(startCwd = process.cwd(), env = proc
|
|
|
1271
1370
|
current = parent;
|
|
1272
1371
|
}
|
|
1273
1372
|
}
|
|
1373
|
+
export function resolveFactoryBrokerConnectionPath(startCwd = process.cwd(), env = process.env, requireDedicatedBroker = false) {
|
|
1374
|
+
const connectionPath = resolveBrokerConnectionPath(startCwd, env);
|
|
1375
|
+
if (!requireDedicatedBroker)
|
|
1376
|
+
return connectionPath;
|
|
1377
|
+
const explicitStateDir = env.AGENT_RELAY_STATE_DIR?.trim();
|
|
1378
|
+
if (!explicitStateDir) {
|
|
1379
|
+
throw new Error('fleetHealth.requireDedicatedBroker requires AGENT_RELAY_STATE_DIR to name Factory\'s isolated relay state directory');
|
|
1380
|
+
}
|
|
1381
|
+
const inheritedConnectionPath = resolveBrokerConnectionPath(startCwd, {
|
|
1382
|
+
...env,
|
|
1383
|
+
AGENT_RELAY_STATE_DIR: '',
|
|
1384
|
+
});
|
|
1385
|
+
// Always reject the checkout-local path, even if broker discovery currently
|
|
1386
|
+
// walks past it and finds an ancestor connection. An interactive relay
|
|
1387
|
+
// started in this checkout can claim the local path later.
|
|
1388
|
+
const sharedStateDirs = [
|
|
1389
|
+
join(resolve(startCwd), '.agentworkforce', 'relay'),
|
|
1390
|
+
...(inheritedConnectionPath ? [dirname(inheritedConnectionPath)] : []),
|
|
1391
|
+
];
|
|
1392
|
+
if (sharedStateDirs.some((stateDir) => sameFilesystemPath(explicitStateDir, stateDir))) {
|
|
1393
|
+
throw new Error('AGENT_RELAY_STATE_DIR resolves to the project broker; choose a separate state directory for Factory');
|
|
1394
|
+
}
|
|
1395
|
+
return connectionPath;
|
|
1396
|
+
}
|
|
1397
|
+
function sameFilesystemPath(left, right) {
|
|
1398
|
+
return canonicalPath(left) === canonicalPath(right);
|
|
1399
|
+
}
|
|
1400
|
+
function canonicalPath(path) {
|
|
1401
|
+
const absolute = resolve(path);
|
|
1402
|
+
const missingSegments = [];
|
|
1403
|
+
let existingAncestor = absolute;
|
|
1404
|
+
while (!existsSync(existingAncestor)) {
|
|
1405
|
+
const parent = dirname(existingAncestor);
|
|
1406
|
+
if (parent === existingAncestor)
|
|
1407
|
+
return absolute;
|
|
1408
|
+
missingSegments.unshift(basename(existingAncestor));
|
|
1409
|
+
existingAncestor = parent;
|
|
1410
|
+
}
|
|
1411
|
+
try {
|
|
1412
|
+
return resolve(realpathSync.native(existingAncestor), ...missingSegments);
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
return absolute;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1274
1418
|
async function prepareFactoryIntegrations(command, mount, config, globals, deps, workspaceId, err) {
|
|
1275
1419
|
const connections = mount.integrationConnections;
|
|
1276
1420
|
if (!connections)
|
|
@@ -1479,13 +1623,10 @@ async function isAllowedFactoryDraft(path, content, opts, mount, config) {
|
|
|
1479
1623
|
if (/^\/slack\/channels\/[^/]+\/messages\/.+/u.test(path)) {
|
|
1480
1624
|
return true;
|
|
1481
1625
|
}
|
|
1482
|
-
if (isAllowedFactoryGithubDraft(path, opts))
|
|
1626
|
+
if (await isAllowedFactoryGithubDraft(path, content, opts, mount, config))
|
|
1483
1627
|
return true;
|
|
1484
|
-
}
|
|
1485
1628
|
return false;
|
|
1486
1629
|
}
|
|
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
1630
|
const scopeIssueFromDraftContent = (content) => ({
|
|
1490
1631
|
title: typeof asRecord(content)?.title === 'string' ? asRecord(content)?.title : '',
|
|
1491
1632
|
team: typeof asRecord(asRecord(content)?.team)?.key === 'string'
|
|
@@ -1933,6 +2074,8 @@ Commands:
|
|
|
1933
2074
|
close-probe <PR> Probe/close a PR for an issue
|
|
1934
2075
|
featuremap check Validate .agentworkforce/features/manifest.yaml
|
|
1935
2076
|
intake notion <file> Normalize mounted Notion specs into Factory work
|
|
2077
|
+
intake notion generate
|
|
2078
|
+
Emit a manifest for Ready for Agent Factory Tasks
|
|
1936
2079
|
fleet <command> Low-level fleet commands: spawn, roster, release
|
|
1937
2080
|
|
|
1938
2081
|
Options:
|