@agent-relay/factory 0.1.60 → 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 +140 -16
- 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 +23 -2
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +201 -42
- 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 +175 -52
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +24 -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 +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- 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 +30 -0
- package/dist/orchestrator/batch-tracker.d.ts.map +1 -1
- package/dist/orchestrator/batch-tracker.js +50 -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 +863 -98
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/orchestrator/index.d.ts +2 -2
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +2 -2
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/reaper.d.ts +16 -2
- package/dist/orchestrator/reaper.d.ts.map +1 -1
- package/dist/orchestrator/reaper.js +79 -5
- package/dist/orchestrator/reaper.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 +35 -6
- 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 +20 -3
- package/dist/state/file-state-store.d.ts.map +1 -1
- package/dist/state/file-state-store.js +89 -245
- package/dist/state/file-state-store.js.map +1 -1
- package/dist/state/in-memory-state-store.d.ts +2 -1
- package/dist/state/in-memory-state-store.d.ts.map +1 -1
- package/dist/state/in-memory-state-store.js +12 -3
- 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 +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 +51 -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;
|
|
@@ -201,8 +219,13 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
201
219
|
registryPath: loaded.config.loop.registryPath,
|
|
202
220
|
staleMs: loaded.config.loop.heartbeatStaleMs,
|
|
203
221
|
fleet,
|
|
222
|
+
includeHeld: command.includeHeld,
|
|
223
|
+
logger: streamLogger(err),
|
|
224
|
+
});
|
|
225
|
+
writeJson(out, {
|
|
226
|
+
...processes,
|
|
227
|
+
environments: await safeEnvironmentReap(Boolean(loaded.config.environments.kubernetes), deps.reapEnvironments),
|
|
204
228
|
});
|
|
205
|
-
writeJson(out, { ...processes, environments: await safeEnvironmentReap(deps.reapEnvironments) });
|
|
206
229
|
return 0;
|
|
207
230
|
}
|
|
208
231
|
// Derive the workspace from the cloud session when it isn't pinned in
|
|
@@ -259,10 +282,17 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
259
282
|
// A GitHub-only workspace has no /linear/states catalog and uses labels
|
|
260
283
|
// for lifecycle state, so it must not depend on that provider at startup.
|
|
261
284
|
const stateResolution = await resolveStatesForIssueSource(mount, loaded.config, deps.resolveStates);
|
|
262
|
-
const stateStore =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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();
|
|
266
296
|
reporter = deps.reporter ?? await buildFactoryCloudReporter({
|
|
267
297
|
config: loaded.config,
|
|
268
298
|
backend: globals.backend,
|
|
@@ -294,7 +324,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
294
324
|
reporter,
|
|
295
325
|
worktrees: globals.backend === 'internal' ? new GitAgentWorktreeManager() : undefined,
|
|
296
326
|
});
|
|
297
|
-
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);
|
|
298
328
|
}
|
|
299
329
|
}
|
|
300
330
|
return 1;
|
|
@@ -314,7 +344,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
314
344
|
}
|
|
315
345
|
}
|
|
316
346
|
err.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
317
|
-
return
|
|
347
|
+
return exitCodeForError(error);
|
|
318
348
|
}
|
|
319
349
|
finally {
|
|
320
350
|
try {
|
|
@@ -388,6 +418,32 @@ function parseIntakeCommand(args) {
|
|
|
388
418
|
const [source, manifestPath, ...rest] = args;
|
|
389
419
|
if (source !== 'notion')
|
|
390
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
|
+
}
|
|
391
447
|
if (!manifestPath)
|
|
392
448
|
throw new Error('factory intake notion requires a manifest path');
|
|
393
449
|
if (rest.length > 0)
|
|
@@ -484,7 +540,7 @@ export function parseGlobalOptions(argv) {
|
|
|
484
540
|
}
|
|
485
541
|
return { globals, args };
|
|
486
542
|
}
|
|
487
|
-
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) {
|
|
488
544
|
const mountFn = resolveLocalMountFn(deps, mount);
|
|
489
545
|
const mountStderr = deps.stderr ?? process.stderr;
|
|
490
546
|
const debugMountRefreshes = (deps.env ?? process.env).FACTORY_LOG_LEVEL?.toLowerCase() === 'debug';
|
|
@@ -523,15 +579,20 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
523
579
|
// would only spawn agents against a read-denied mirror. Fail fast with the
|
|
524
580
|
// remediation and resolve the command with a non-zero code.
|
|
525
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.
|
|
526
585
|
const handleWarmMountError = (error) => {
|
|
527
586
|
if (error instanceof MountAuthScopeError) {
|
|
587
|
+
const code = exitCodeForError(error);
|
|
528
588
|
mountStderr.write(`${error.message}\n`);
|
|
529
589
|
mountStderr.write('[factory] aborting startup: local mount cannot obtain its filesystem scopes.\n');
|
|
530
|
-
void flushAndResolve(
|
|
531
|
-
return;
|
|
590
|
+
void flushAndResolve(code);
|
|
591
|
+
return code;
|
|
532
592
|
}
|
|
533
593
|
const message = error instanceof Error ? error.message : String(error);
|
|
534
594
|
mountStderr.write(`[factory] warning: background relayfile mount warmup failed: ${message}\n`);
|
|
595
|
+
return FACTORY_EXIT.FAILED;
|
|
535
596
|
};
|
|
536
597
|
const removeSignalHandlers = installFactoryStopSignalHandlers(factory, {
|
|
537
598
|
exit: (code) => {
|
|
@@ -551,14 +612,14 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
551
612
|
mountStderr.write('[factory] aborting startup: Relayfile workspace mirror could not be resolved.\n');
|
|
552
613
|
if (stoppedBySignal)
|
|
553
614
|
return await waiter.promise;
|
|
554
|
-
return
|
|
615
|
+
return FACTORY_EXIT.FAILED;
|
|
555
616
|
}
|
|
556
617
|
}
|
|
557
618
|
catch (error) {
|
|
558
|
-
handleWarmMountError(error);
|
|
619
|
+
const code = handleWarmMountError(error);
|
|
559
620
|
if (stoppedBySignal)
|
|
560
621
|
return await waiter.promise;
|
|
561
|
-
return
|
|
622
|
+
return code;
|
|
562
623
|
}
|
|
563
624
|
}
|
|
564
625
|
else {
|
|
@@ -580,12 +641,13 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
580
641
|
}
|
|
581
642
|
if (command.action === 'run-once') {
|
|
582
643
|
await ensureWorkspaceMount(mount, mountFn, workspaceId, acceptableMountIds, mountStderr);
|
|
583
|
-
|
|
584
|
-
|
|
644
|
+
const report = await factory.runOnce({ dryRun: globals.dryRun });
|
|
645
|
+
writeJson(out, report);
|
|
646
|
+
return exitCodeForIterationReport(report);
|
|
585
647
|
}
|
|
586
648
|
if (command.action === 'status') {
|
|
587
649
|
const versionInfo = await safeFactoryVersionInfo(deps.versionInfo, Boolean(deps.versionInfo) || !hasInjectedFactoryRuntime(deps));
|
|
588
|
-
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));
|
|
589
651
|
return 0;
|
|
590
652
|
}
|
|
591
653
|
if (command.action === 'loop-status') {
|
|
@@ -606,18 +668,20 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
606
668
|
const removeSignalHandlers = installFactoryStopSignalHandlers(factory, {
|
|
607
669
|
processLike: deps.stopSignalProcessLike,
|
|
608
670
|
});
|
|
671
|
+
let loopCode = FACTORY_EXIT.OK;
|
|
609
672
|
try {
|
|
610
673
|
const reports = await factory.runLoop({ dryRun: globals.dryRun });
|
|
674
|
+
loopCode = exitCodeForLoopReports(reports);
|
|
611
675
|
writeJson(out, {
|
|
612
676
|
reports,
|
|
613
|
-
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),
|
|
614
678
|
});
|
|
615
679
|
}
|
|
616
680
|
finally {
|
|
617
681
|
removeSignalHandlers();
|
|
618
682
|
await factory.stop();
|
|
619
683
|
}
|
|
620
|
-
return
|
|
684
|
+
return loopCode;
|
|
621
685
|
}
|
|
622
686
|
if (command.kind === 'factory-canary') {
|
|
623
687
|
const report = await factory.runOnce({ dryRun: true });
|
|
@@ -642,15 +706,16 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
642
706
|
try {
|
|
643
707
|
const result = await factory.dispatch(decision, { dryRun: false });
|
|
644
708
|
writeJson(out, result);
|
|
645
|
-
await factory.waitForDispatchTerminal(result.issue);
|
|
646
|
-
return
|
|
709
|
+
const terminalPhase = await factory.waitForDispatchTerminal(result.issue);
|
|
710
|
+
return exitCodeForRelayDispatch(result, terminalPhase);
|
|
647
711
|
}
|
|
648
712
|
finally {
|
|
649
713
|
await factory.stop();
|
|
650
714
|
}
|
|
651
715
|
}
|
|
652
|
-
|
|
653
|
-
|
|
716
|
+
const dispatched = await factory.dispatch(decision, { dryRun: globals.dryRun });
|
|
717
|
+
writeJson(out, dispatched);
|
|
718
|
+
return exitCodeForDispatchResult(dispatched);
|
|
654
719
|
}
|
|
655
720
|
async function warmStartPathMounts(mount, mountFn, workspaceId, config, acceptableMountIds, stderr = process.stderr, debug = process.env.FACTORY_LOG_LEVEL?.toLowerCase() === 'debug') {
|
|
656
721
|
const result = await ensureWorkspaceMount(mount, mountFn, workspaceId, acceptableMountIds, stderr);
|
|
@@ -824,19 +889,31 @@ async function ensureMountPath(mountFn, workspaceId, path, mountOpts, stderr, lo
|
|
|
824
889
|
function resolveIntegrationsMountRoot(mount) {
|
|
825
890
|
return mount.getLocalMountRoot?.() ?? resolve(process.cwd(), '.integrations');
|
|
826
891
|
}
|
|
827
|
-
async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, registryPath, heartbeatStaleMs, versionInfo) {
|
|
892
|
+
async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, registryPath, heartbeatStaleMs, versionInfo, stateStoreStatus) {
|
|
828
893
|
const status = factory.status();
|
|
829
894
|
const [heartbeat, registry] = await Promise.all([
|
|
830
895
|
readFactoryLoopHeartbeat(heartbeatPath),
|
|
831
896
|
readFactoryInFlightRegistry(registryPath),
|
|
832
897
|
]);
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
898
|
+
// A registry stamped for a different heartbeat file belongs to another
|
|
899
|
+
// daemon instance; neither its dispatch nor held-agent view applies here.
|
|
900
|
+
const registryMatchesHeartbeat = !registry?.heartbeatPath || registry.heartbeatPath === heartbeatPath;
|
|
901
|
+
const registryDispatches = registryMatchesHeartbeat ? inFlightDispatchesFromRegistry(registry) : [];
|
|
902
|
+
const registryHeldAgents = registryMatchesHeartbeat ? heldAgentsFromRegistry(registry) : [];
|
|
903
|
+
const heldAgents = registryHeldAgents.length > 0 ? registryHeldAgents : (status.heldAgents ?? []);
|
|
836
904
|
const observableStatus = registryDispatches.length > 0
|
|
837
905
|
? { ...status, inFlightDispatches: registryDispatches }
|
|
838
906
|
: status;
|
|
839
907
|
const liveness = checkFactoryLoopLiveness(heartbeat, { staleMs: heartbeatStaleMs });
|
|
908
|
+
// A live heartbeat belongs to the daemon, while `status` is a fresh local
|
|
909
|
+
// Factory instance. Do not misreport that local instance's `not-running`
|
|
910
|
+
// state when an older daemon heartbeat predates this health field.
|
|
911
|
+
const readinessReconcile = liveness.ok
|
|
912
|
+
? heartbeat?.readinessReconcile
|
|
913
|
+
: observableStatus.readinessReconcile;
|
|
914
|
+
const fleetControlPlane = liveness.ok
|
|
915
|
+
? heartbeat?.fleetControlPlane
|
|
916
|
+
: observableStatus.fleetControlPlane;
|
|
840
917
|
const eventListener = liveness.ok
|
|
841
918
|
? heartbeat?.eventListener ?? {
|
|
842
919
|
state: 'unknown',
|
|
@@ -848,11 +925,23 @@ async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, regis
|
|
|
848
925
|
};
|
|
849
926
|
const health = mount.getLocalMountHealth?.();
|
|
850
927
|
if (!health)
|
|
851
|
-
return {
|
|
928
|
+
return {
|
|
929
|
+
...observableStatus,
|
|
930
|
+
...versionInfo,
|
|
931
|
+
...(stateStoreStatus ? { stateStore: stateStoreStatus } : {}),
|
|
932
|
+
heldAgents,
|
|
933
|
+
eventListener,
|
|
934
|
+
readinessReconcile,
|
|
935
|
+
fleetControlPlane,
|
|
936
|
+
};
|
|
852
937
|
return {
|
|
853
938
|
...observableStatus,
|
|
854
939
|
...versionInfo,
|
|
940
|
+
...(stateStoreStatus ? { stateStore: stateStoreStatus } : {}),
|
|
941
|
+
heldAgents,
|
|
855
942
|
eventListener,
|
|
943
|
+
readinessReconcile,
|
|
944
|
+
fleetControlPlane,
|
|
856
945
|
localMountDegraded: health.degraded,
|
|
857
946
|
...(health.reason ? { localMountDegradedReason: health.reason } : {}),
|
|
858
947
|
...(health.localDir ? { localMountRoot: health.localDir } : {}),
|
|
@@ -990,7 +1079,18 @@ function parseFactoryCommand(args) {
|
|
|
990
1079
|
if (action === 'start') {
|
|
991
1080
|
return { kind: 'factory', action, ...parseFactoryStartFlags([issueOrPr, ...flags]) };
|
|
992
1081
|
}
|
|
993
|
-
if (action === '
|
|
1082
|
+
if (action === 'reap-orphans') {
|
|
1083
|
+
const values = [issueOrPr, ...flags].filter((value) => Boolean(value));
|
|
1084
|
+
const unexpected = values.find((value) => value !== '--include-held');
|
|
1085
|
+
if (unexpected)
|
|
1086
|
+
throw new Error(`Unknown factory reap-orphans option: ${unexpected}`);
|
|
1087
|
+
return {
|
|
1088
|
+
kind: 'factory',
|
|
1089
|
+
action,
|
|
1090
|
+
...(values.includes('--include-held') ? { includeHeld: true } : {}),
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
if (action === 'run-once' || action === 'loop' || action === 'status' || action === 'loop-status' || action === 'kill-loop') {
|
|
994
1094
|
return { kind: 'factory', action };
|
|
995
1095
|
}
|
|
996
1096
|
if (action === 'canary') {
|
|
@@ -1160,11 +1260,16 @@ function hasInjectedFactoryRuntime(deps) {
|
|
|
1160
1260
|
async function buildFleet(globals, loaded, deps) {
|
|
1161
1261
|
if (deps.fleet)
|
|
1162
1262
|
return deps.fleet;
|
|
1163
|
-
|
|
1263
|
+
// fixtureFiles are an explicit hermetic harness opt-in. Keep both backend
|
|
1264
|
+
// command paths off real fleet infrastructure so the durable relay CLI path
|
|
1265
|
+
// can be exercised end-to-end with deterministic fixtures too.
|
|
1266
|
+
if (hasExplicitFixtureFiles(loaded))
|
|
1164
1267
|
return new FakeFleetClient();
|
|
1165
1268
|
const cwd = process.cwd();
|
|
1166
1269
|
const env = deps.env ?? process.env;
|
|
1167
|
-
const connectionPath =
|
|
1270
|
+
const connectionPath = globals.backend === 'internal'
|
|
1271
|
+
? resolveFactoryBrokerConnectionPath(cwd, env, loaded?.config.fleetHealth.requireDedicatedBroker ?? false)
|
|
1272
|
+
: resolveBrokerConnectionPath(cwd, env);
|
|
1168
1273
|
// An injected createFleet owns fleet construction entirely (tests), so skip the
|
|
1169
1274
|
// real broker bootstrap.
|
|
1170
1275
|
if (deps.createFleet) {
|
|
@@ -1241,6 +1346,51 @@ export function resolveBrokerConnectionPath(startCwd = process.cwd(), env = proc
|
|
|
1241
1346
|
current = parent;
|
|
1242
1347
|
}
|
|
1243
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
|
+
}
|
|
1244
1394
|
async function prepareFactoryIntegrations(command, mount, config, globals, deps, workspaceId, err) {
|
|
1245
1395
|
const connections = mount.integrationConnections;
|
|
1246
1396
|
if (!connections)
|
|
@@ -1449,13 +1599,10 @@ async function isAllowedFactoryDraft(path, content, opts, mount, config) {
|
|
|
1449
1599
|
if (/^\/slack\/channels\/[^/]+\/messages\/.+/u.test(path)) {
|
|
1450
1600
|
return true;
|
|
1451
1601
|
}
|
|
1452
|
-
if (isAllowedFactoryGithubDraft(path, opts))
|
|
1602
|
+
if (await isAllowedFactoryGithubDraft(path, content, opts, mount, config))
|
|
1453
1603
|
return true;
|
|
1454
|
-
}
|
|
1455
1604
|
return false;
|
|
1456
1605
|
}
|
|
1457
|
-
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);
|
|
1458
|
-
const isAllowedFactoryGithubDraft = (path, opts) => opts?.guarded === true && isFactoryGithubWritebackPath(path);
|
|
1459
1606
|
const scopeIssueFromDraftContent = (content) => ({
|
|
1460
1607
|
title: typeof asRecord(content)?.title === 'string' ? asRecord(content)?.title : '',
|
|
1461
1608
|
team: typeof asRecord(asRecord(content)?.team)?.key === 'string'
|
|
@@ -1780,14 +1927,23 @@ function requireValue(args, index, flag) {
|
|
|
1780
1927
|
function writeJson(out, value) {
|
|
1781
1928
|
out.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
1782
1929
|
}
|
|
1783
|
-
async function safeEnvironmentReap(reap = reapFactoryEnvironmentsOnce) {
|
|
1930
|
+
async function safeEnvironmentReap(configured, reap = reapFactoryEnvironmentsOnce) {
|
|
1931
|
+
if (!configured) {
|
|
1932
|
+
return {
|
|
1933
|
+
applicable: false,
|
|
1934
|
+
reason: 'kubernetes environment provider is not configured',
|
|
1935
|
+
reaped: [],
|
|
1936
|
+
retained: [],
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1784
1939
|
try {
|
|
1785
|
-
return await reap();
|
|
1940
|
+
return { applicable: true, ...await reap() };
|
|
1786
1941
|
}
|
|
1787
1942
|
catch (error) {
|
|
1788
1943
|
// Process cleanup remains useful on hosts without kubectl or an active
|
|
1789
1944
|
// cluster. Surface the skipped backstop without failing the whole command.
|
|
1790
1945
|
return {
|
|
1946
|
+
applicable: true,
|
|
1791
1947
|
reaped: [],
|
|
1792
1948
|
retained: [],
|
|
1793
1949
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -1885,7 +2041,8 @@ Commands:
|
|
|
1885
2041
|
loop Run the bounded loop configured in factory.config.json
|
|
1886
2042
|
loop-status Print heartbeat/liveness status for the loop
|
|
1887
2043
|
kill-loop Send SIGTERM to the heartbeat pid
|
|
1888
|
-
reap-orphans
|
|
2044
|
+
reap-orphans [--include-held]
|
|
2045
|
+
Report stale processes and held agents; opt in to release held agents past deadline
|
|
1889
2046
|
canary <KEY|path> Check that a known issue is dispatch-ready
|
|
1890
2047
|
triage <KEY|path> Triage one issue and print the decision
|
|
1891
2048
|
dispatch <KEY|path> Triage and dispatch one issue
|
|
@@ -1893,6 +2050,8 @@ Commands:
|
|
|
1893
2050
|
close-probe <PR> Probe/close a PR for an issue
|
|
1894
2051
|
featuremap check Validate .agentworkforce/features/manifest.yaml
|
|
1895
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
|
|
1896
2055
|
fleet <command> Low-level fleet commands: spawn, roster, release
|
|
1897
2056
|
|
|
1898
2057
|
Options:
|