@agent-relay/factory 0.1.62 → 0.1.63

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