@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.
Files changed (166) hide show
  1. package/README.md +117 -15
  2. package/dist/cli/exit-codes.d.ts +81 -0
  3. package/dist/cli/exit-codes.d.ts.map +1 -0
  4. package/dist/cli/exit-codes.js +122 -0
  5. package/dist/cli/exit-codes.js.map +1 -0
  6. package/dist/cli/fleet.d.ts +26 -2
  7. package/dist/cli/fleet.d.ts.map +1 -1
  8. package/dist/cli/fleet.js +183 -40
  9. package/dist/cli/fleet.js.map +1 -1
  10. package/dist/cli/index.d.ts +3 -0
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/cli/index.js +2 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/config/schema.d.ts +156 -52
  15. package/dist/config/schema.d.ts.map +1 -1
  16. package/dist/config/schema.js +17 -5
  17. package/dist/config/schema.js.map +1 -1
  18. package/dist/dispatch/templates.d.ts +7 -0
  19. package/dist/dispatch/templates.d.ts.map +1 -1
  20. package/dist/dispatch/templates.js +64 -12
  21. package/dist/dispatch/templates.js.map +1 -1
  22. package/dist/dispatch/work-unit-identity.d.ts +19 -0
  23. package/dist/dispatch/work-unit-identity.d.ts.map +1 -0
  24. package/dist/dispatch/work-unit-identity.js +40 -0
  25. package/dist/dispatch/work-unit-identity.js.map +1 -0
  26. package/dist/environments/stack-descriptor.d.ts +3 -3
  27. package/dist/fleet/control-plane-circuit.d.ts +57 -0
  28. package/dist/fleet/control-plane-circuit.d.ts.map +1 -0
  29. package/dist/fleet/control-plane-circuit.js +231 -0
  30. package/dist/fleet/control-plane-circuit.js.map +1 -0
  31. package/dist/fleet/internal-fleet-client.d.ts +2 -1
  32. package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
  33. package/dist/fleet/internal-fleet-client.js +11 -7
  34. package/dist/fleet/internal-fleet-client.js.map +1 -1
  35. package/dist/fleet/relay-fleet-client.d.ts +3 -0
  36. package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
  37. package/dist/fleet/relay-fleet-client.js +97 -18
  38. package/dist/fleet/relay-fleet-client.js.map +1 -1
  39. package/dist/github/writeback-paths.d.ts +7 -0
  40. package/dist/github/writeback-paths.d.ts.map +1 -0
  41. package/dist/github/writeback-paths.js +28 -0
  42. package/dist/github/writeback-paths.js.map +1 -0
  43. package/dist/hosted/orchestrator.d.ts.map +1 -1
  44. package/dist/hosted/orchestrator.js +2 -0
  45. package/dist/hosted/orchestrator.js.map +1 -1
  46. package/dist/index.d.ts +10 -4
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +6 -3
  49. package/dist/index.js.map +1 -1
  50. package/dist/intake/index.d.ts +2 -1
  51. package/dist/intake/index.d.ts.map +1 -1
  52. package/dist/intake/index.js +2 -1
  53. package/dist/intake/index.js.map +1 -1
  54. package/dist/intake/notion-manifest.d.ts +48 -0
  55. package/dist/intake/notion-manifest.d.ts.map +1 -0
  56. package/dist/intake/notion-manifest.js +258 -0
  57. package/dist/intake/notion-manifest.js.map +1 -0
  58. package/dist/intake/notion-relay-claim.d.ts +5 -4
  59. package/dist/intake/notion-relay-claim.d.ts.map +1 -1
  60. package/dist/intake/notion-relay-claim.js +48 -7
  61. package/dist/intake/notion-relay-claim.js.map +1 -1
  62. package/dist/intake/notion.d.ts +6 -3
  63. package/dist/intake/notion.d.ts.map +1 -1
  64. package/dist/intake/notion.js +63 -13
  65. package/dist/intake/notion.js.map +1 -1
  66. package/dist/issue-key-match.d.ts +6 -0
  67. package/dist/issue-key-match.d.ts.map +1 -1
  68. package/dist/issue-key-match.js +6 -1
  69. package/dist/issue-key-match.js.map +1 -1
  70. package/dist/mount/local-mount-preflight.d.ts.map +1 -1
  71. package/dist/mount/local-mount-preflight.js +6 -2
  72. package/dist/mount/local-mount-preflight.js.map +1 -1
  73. package/dist/mount/mount-auth-error.d.ts +16 -3
  74. package/dist/mount/mount-auth-error.d.ts.map +1 -1
  75. package/dist/mount/mount-auth-error.js +34 -7
  76. package/dist/mount/mount-auth-error.js.map +1 -1
  77. package/dist/mount/relayfile-cloud-mount-client.d.ts +36 -1
  78. package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
  79. package/dist/mount/relayfile-cloud-mount-client.js +136 -6
  80. package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
  81. package/dist/mount/relayfile-github-connection-write.d.ts +24 -2
  82. package/dist/mount/relayfile-github-connection-write.d.ts.map +1 -1
  83. package/dist/mount/relayfile-github-connection-write.js +90 -1
  84. package/dist/mount/relayfile-github-connection-write.js.map +1 -1
  85. package/dist/node/factory-node.d.ts.map +1 -1
  86. package/dist/node/factory-node.js +18 -4
  87. package/dist/node/factory-node.js.map +1 -1
  88. package/dist/observability/cloud-reporter.d.ts +1 -0
  89. package/dist/observability/cloud-reporter.d.ts.map +1 -1
  90. package/dist/observability/cloud-reporter.js +182 -50
  91. package/dist/observability/cloud-reporter.js.map +1 -1
  92. package/dist/observability/events.d.ts +52 -52
  93. package/dist/orchestrator/batch-tracker.d.ts +25 -0
  94. package/dist/orchestrator/batch-tracker.d.ts.map +1 -1
  95. package/dist/orchestrator/batch-tracker.js +48 -2
  96. package/dist/orchestrator/batch-tracker.js.map +1 -1
  97. package/dist/orchestrator/factory.d.ts +36 -2
  98. package/dist/orchestrator/factory.d.ts.map +1 -1
  99. package/dist/orchestrator/factory.js +1208 -82
  100. package/dist/orchestrator/factory.js.map +1 -1
  101. package/dist/orchestrator/index.d.ts +1 -1
  102. package/dist/orchestrator/index.d.ts.map +1 -1
  103. package/dist/orchestrator/index.js +1 -1
  104. package/dist/orchestrator/index.js.map +1 -1
  105. package/dist/ports/fleet.d.ts +6 -0
  106. package/dist/ports/fleet.d.ts.map +1 -1
  107. package/dist/ports/index.d.ts +1 -1
  108. package/dist/ports/index.d.ts.map +1 -1
  109. package/dist/ports/mount.d.ts +39 -0
  110. package/dist/ports/mount.d.ts.map +1 -1
  111. package/dist/ports/state.d.ts +75 -4
  112. package/dist/ports/state.d.ts.map +1 -1
  113. package/dist/state/document-store.d.ts +33 -0
  114. package/dist/state/document-store.d.ts.map +1 -0
  115. package/dist/state/document-store.js +7 -0
  116. package/dist/state/document-store.js.map +1 -0
  117. package/dist/state/file-state-store.d.ts +32 -7
  118. package/dist/state/file-state-store.d.ts.map +1 -1
  119. package/dist/state/file-state-store.js +169 -250
  120. package/dist/state/file-state-store.js.map +1 -1
  121. package/dist/state/in-memory-state-store.d.ts +13 -2
  122. package/dist/state/in-memory-state-store.d.ts.map +1 -1
  123. package/dist/state/in-memory-state-store.js +87 -4
  124. package/dist/state/in-memory-state-store.js.map +1 -1
  125. package/dist/state/watch-state-document.d.ts +5 -0
  126. package/dist/state/watch-state-document.d.ts.map +1 -0
  127. package/dist/state/watch-state-document.js +486 -0
  128. package/dist/state/watch-state-document.js.map +1 -0
  129. package/dist/testing/deadline.d.ts +11 -0
  130. package/dist/testing/deadline.d.ts.map +1 -0
  131. package/dist/testing/deadline.js +23 -0
  132. package/dist/testing/deadline.js.map +1 -0
  133. package/dist/testing/fakes.d.ts +2 -0
  134. package/dist/testing/fakes.d.ts.map +1 -1
  135. package/dist/testing/fakes.js.map +1 -1
  136. package/dist/testing/index.d.ts +1 -0
  137. package/dist/testing/index.d.ts.map +1 -1
  138. package/dist/testing/index.js +1 -0
  139. package/dist/testing/index.js.map +1 -1
  140. package/dist/trajectory.d.ts +25 -0
  141. package/dist/trajectory.d.ts.map +1 -0
  142. package/dist/trajectory.js +51 -0
  143. package/dist/trajectory.js.map +1 -0
  144. package/dist/triage/heuristic.d.ts +5 -0
  145. package/dist/triage/heuristic.d.ts.map +1 -1
  146. package/dist/triage/heuristic.js +42 -3
  147. package/dist/triage/heuristic.js.map +1 -1
  148. package/dist/triage/index.d.ts +1 -1
  149. package/dist/triage/index.d.ts.map +1 -1
  150. package/dist/triage/index.js +1 -1
  151. package/dist/triage/index.js.map +1 -1
  152. package/dist/triage/schema.d.ts +73 -55
  153. package/dist/triage/schema.d.ts.map +1 -1
  154. package/dist/triage/schema.js +2 -1
  155. package/dist/triage/schema.js.map +1 -1
  156. package/dist/types.d.ts +19 -3
  157. package/dist/types.d.ts.map +1 -1
  158. package/dist/writeback/github.d.ts +22 -7
  159. package/dist/writeback/github.d.ts.map +1 -1
  160. package/dist/writeback/github.js +86 -16
  161. package/dist/writeback/github.js.map +1 -1
  162. package/dist/writeback/index.d.ts +1 -1
  163. package/dist/writeback/index.d.ts.map +1 -1
  164. package/dist/writeback/index.js +1 -1
  165. package/dist/writeback/index.js.map +1 -1
  166. 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 { FileStateStore, RelayfileCloudMountClient, checkFactoryLoopLiveness, closeProbePr, createFactory, createFleet, createFactoryCloudEventV1, ensureRelayBroker, FactoryCloudReporter, FileFactoryCloudEventOutbox, defaultGhRunner, explicitLinkedIssueKey, githubIssuePathParts, githubWatchStatePath, isInFactoryScope, parseGithubFactoryIssue, publishFactoryMountHealth, heldAgentsFromRegistry, parseOwnedBrokerAgentExitTimeoutMs, parseStandaloneBabysitTarget, readStandalonePullRequest, readLinearIssueWithCanonicalFallback, reapFactoryOrphansOnce, reapFactoryEnvironmentsOnce, readFactoryInFlightRegistry, readFactoryLoopHeartbeat, resolveFactoryStates, stateResolutionFromIds, standaloneBabysitterAgentName, renderAgentTask, resolveFactoryWorkspace, } from '../index.js';
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) => isAllowedFactoryGithubDraft(path, 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 = new FileStateStore({
268
- batchSize: loaded.config.batchSize,
269
- watchStatePath: githubWatchStatePath(loaded.config.loop.registryPath),
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 1;
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(1);
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 1;
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 1;
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
- writeJson(out, await factory.runOnce({ dryRun: globals.dryRun }));
589
- return 0;
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 0;
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 0;
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
- writeJson(out, await factory.dispatch(decision, { dryRun: globals.dryRun }));
658
- return 0;
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 { ...observableStatus, ...versionInfo, heldAgents, eventListener, readinessReconcile };
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
- if (hasInjectedFactoryRuntime(input.deps) && !input.deps.cloudSessionProvider)
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 ?? resolveFactoryWorkspace)();
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
- const cloudFetch = async (_request, init) => session.client.fetch('/api/v1/factory/events', init);
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: session.auth.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: async () => session.client.snapshot().accessToken,
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 = resolveBrokerConnectionPath(cwd, env);
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: