@agent-relay/factory 0.1.58 → 0.1.62

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 (70) hide show
  1. package/README.md +36 -1
  2. package/dist/cli/fleet.d.ts +8 -1
  3. package/dist/cli/fleet.d.ts.map +1 -1
  4. package/dist/cli/fleet.js +172 -16
  5. package/dist/cli/fleet.js.map +1 -1
  6. package/dist/config/schema.d.ts +796 -273
  7. package/dist/config/schema.d.ts.map +1 -1
  8. package/dist/config/schema.js +44 -0
  9. package/dist/config/schema.js.map +1 -1
  10. package/dist/delivery/ticket-dispatch.d.ts +15 -0
  11. package/dist/delivery/ticket-dispatch.d.ts.map +1 -0
  12. package/dist/delivery/ticket-dispatch.js +20 -0
  13. package/dist/delivery/ticket-dispatch.js.map +1 -0
  14. package/dist/git/agent-worktree.d.ts.map +1 -1
  15. package/dist/git/agent-worktree.js +4 -0
  16. package/dist/git/agent-worktree.js.map +1 -1
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/issue-key-match.d.ts +6 -0
  22. package/dist/issue-key-match.d.ts.map +1 -1
  23. package/dist/issue-key-match.js +14 -0
  24. package/dist/issue-key-match.js.map +1 -1
  25. package/dist/mount/relayfile-github-connection-write.d.ts.map +1 -1
  26. package/dist/mount/relayfile-github-connection-write.js +6 -3
  27. package/dist/mount/relayfile-github-connection-write.js.map +1 -1
  28. package/dist/orchestrator/batch-tracker.d.ts +7 -1
  29. package/dist/orchestrator/batch-tracker.d.ts.map +1 -1
  30. package/dist/orchestrator/batch-tracker.js +2 -0
  31. package/dist/orchestrator/batch-tracker.js.map +1 -1
  32. package/dist/orchestrator/factory.d.ts.map +1 -1
  33. package/dist/orchestrator/factory.js +1940 -153
  34. package/dist/orchestrator/factory.js.map +1 -1
  35. package/dist/orchestrator/index.d.ts +1 -1
  36. package/dist/orchestrator/index.d.ts.map +1 -1
  37. package/dist/orchestrator/index.js +1 -1
  38. package/dist/orchestrator/index.js.map +1 -1
  39. package/dist/orchestrator/reaper.d.ts +16 -2
  40. package/dist/orchestrator/reaper.d.ts.map +1 -1
  41. package/dist/orchestrator/reaper.js +79 -5
  42. package/dist/orchestrator/reaper.js.map +1 -1
  43. package/dist/ports/fleet.d.ts +4 -0
  44. package/dist/ports/fleet.d.ts.map +1 -1
  45. package/dist/ports/mount.d.ts +2 -0
  46. package/dist/ports/mount.d.ts.map +1 -1
  47. package/dist/ports/state.d.ts +60 -1
  48. package/dist/ports/state.d.ts.map +1 -1
  49. package/dist/state/file-state-store.d.ts +9 -1
  50. package/dist/state/file-state-store.d.ts.map +1 -1
  51. package/dist/state/file-state-store.js +160 -2
  52. package/dist/state/file-state-store.js.map +1 -1
  53. package/dist/state/in-memory-state-store.d.ts +7 -1
  54. package/dist/state/in-memory-state-store.d.ts.map +1 -1
  55. package/dist/state/in-memory-state-store.js +71 -0
  56. package/dist/state/in-memory-state-store.js.map +1 -1
  57. package/dist/triage/schema.d.ts +36 -0
  58. package/dist/triage/schema.d.ts.map +1 -1
  59. package/dist/triage/schema.js +2 -0
  60. package/dist/triage/schema.js.map +1 -1
  61. package/dist/types.d.ts +64 -1
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/version-info.d.ts +21 -0
  64. package/dist/version-info.d.ts.map +1 -0
  65. package/dist/version-info.js +146 -0
  66. package/dist/version-info.js.map +1 -0
  67. package/dist/writeback/github.d.ts.map +1 -1
  68. package/dist/writeback/github.js +12 -0
  69. package/dist/writeback/github.js.map +1 -1
  70. package/package.json +1 -1
package/README.md CHANGED
@@ -135,14 +135,33 @@ After init, add the `factory` label to an open issue and run a dry run below.
135
135
  | `factory run-once` | One discover→triage→dispatch cycle, then exit. Honors `--dry-run`. |
136
136
  | `factory loop` | A bounded multi-iteration loop, then exit. |
137
137
  | `factory start --mode live` | Long-lived daemon — the production entrypoint. Runs until you stop it. |
138
- | `factory status` | Print current factory status as JSON. |
138
+ | `factory status` | Print current factory status as JSON, including held agents, lifecycle deadlines, and periodic readiness-reconcile health. |
139
139
  | `factory triage <KEY\|path>` | Triage one issue and print the decision. |
140
140
  | `factory dispatch <KEY\|path>` | Triage + dispatch one issue. Honors `--dry-run`. |
141
141
  | `factory babysit <PR\|PR-URL>` | Spawn a one-shot babysitter for an existing open PR, even when it was not created by Factory. |
142
142
  | `factory canary <KEY\|path>` | Assert a known "Ready for Agent" issue is dispatch-ready by the real dry-run triage path. Prints `{ok,issue,status,reason}`; exits non-zero (with the skip reason) if it isn't. |
143
+ | `factory reap-orphans [--include-held]` | Report stale factory processes and held agents. By default held agents are visible but retained; `--include-held` releases held agents whose configured deadline has elapsed. Kubernetes cleanup is reported as not applicable when no environment provider is configured. |
143
144
  | `factory featuremap check [--manifest <path>] [--base <ref>]` | Validate the repository feature/test manifest and optionally report advisory drift for unchanged entries whose locations changed. |
144
145
  | `factory intake notion <manifest>` | Normalize ready specs from a read-only Notion mount into GitHub lifecycle issues or exact-path fleet work. Honors `--dry-run`. |
145
146
 
147
+ `factory status` includes `inFlightDispatches`, grouped by issue with agent
148
+ names and the provider-claim state (`pending`, `verified`, or `degraded`). This
149
+ view is read from Factory's local in-flight registry, so it remains available
150
+ when GitHub lifecycle writeback is the degraded subsystem.
151
+
152
+ For a live daemon, `factory status` also includes `readinessReconcile`. Its
153
+ state advances from `retrying` to `degraded` after three consecutive periodic
154
+ discovery failures and returns to `healthy` after a successful checkpoint.
155
+ The same record includes the last error, attempt duration, and failure count.
156
+
157
+ Dispatch lifecycle writes are claim-critical. Factory applies the
158
+ `factory:in-progress` label/state before the dispatch comment, confirms the
159
+ GitHub label by provider read-back, and retries either write three times. An
160
+ exhausted write is logged at error level as dead-lettered, recorded as a
161
+ degraded claim in the registry, and fails the dispatch instead of reporting a
162
+ clean dispatch with missing GitHub state. Durable lifecycle recovery repeats
163
+ the same label-and-comment claim without respawning acknowledged agents.
164
+
146
165
  Global options work anywhere in the args: `--config <path>`, `--dry-run`,
147
166
  `--backend <internal|relay>`, and `--agent-exit-timeout <ms>`. The internal
148
167
  backend reuses a relay broker that's already running for your workspace, and
@@ -610,6 +629,22 @@ knobs include issue **routing** (`repos.byLabel` / `byProject` / `keywordRules`
610
629
  `mergePolicy` (defaults to `never`), per-role **model** overrides, and an optional
611
630
  **Slack** channel for status threads.
612
631
 
632
+ Every successfully placed agent team has a wall-clock release backstop. Configure
633
+ the window with `dispatch.agentHoldTimeoutMs`; it defaults to four hours. The
634
+ clock starts at the first successful agent placement, not while a dispatch waits
635
+ for capacity or while a released team waits for human clarification. If the issue
636
+ has not reached its configured `terminalState` before the deadline, Factory
637
+ releases the team with reason `held-past-deadline` and records the lifecycle as
638
+ abandoned so a restart cannot respawn it:
639
+
640
+ ```jsonc
641
+ {
642
+ "dispatch": {
643
+ "agentHoldTimeoutMs": 14400000
644
+ }
645
+ }
646
+ ```
647
+
613
648
  The full schema — every field and default — is validated by Zod at load time, so
614
649
  an invalid config fails fast with a field-level error. See
615
650
  [`src/config/schema.ts`](src/config/schema.ts) for the authoritative reference,
@@ -1,8 +1,9 @@
1
1
  import { ensureCloudSession, type CloudSession } from '@agent-relay/cloud';
2
2
  import { type LocalClonePathOptions } from '../config/local-clone-paths.js';
3
- import { createFactory, createFleet, ensureRelayBroker, reapFactoryEnvironmentsOnce, type Capability, type Factory, type FactoryEventReporter, type FactoryConfig, type FleetBackend, type FleetClient, type GhRunner, type FactoryStateResolution, type LocalMountOptions, type MountClient, type ProbeCloser, type RelayfileCloudMountClientConfig, type ResolvedFactoryWorkspace } from '../index.js';
3
+ import { createFactory, createFleet, ensureRelayBroker, reapFactoryEnvironmentsOnce, type Capability, type Factory, type FactoryEventReporter, type FactoryConfig, type FleetBackend, type FleetClient, type GhRunner, type FactoryStateResolution, type Logger, type LocalMountOptions, type MountClient, type ProbeCloser, type RelayfileCloudMountClientConfig, type ResolvedFactoryWorkspace } from '../index.js';
4
4
  import { type CheckFeatureMapOptions, type FeatureMapCheckReport } from '../featuremap/index.js';
5
5
  import type { FactoryIntegrationProvider } from '../ports/index.js';
6
+ import { type FactoryVersionInfo, type FactoryVersionInfoOptions } from '../version-info.js';
6
7
  import { type NotionIntakeClaimStore, type NotionContractPublisher } from '../intake/index.js';
7
8
  interface FleetCliDeps {
8
9
  fleet?: FleetClient;
@@ -40,6 +41,10 @@ interface FleetCliDeps {
40
41
  notionClaims?: NotionIntakeClaimStore;
41
42
  /** Hermetic verification-environment sweep for CLI tests and alternate runtimes. */
42
43
  reapEnvironments?: typeof reapFactoryEnvironmentsOnce;
44
+ /** Hermetic package/registry metadata for CLI tests and alternate runtimes. */
45
+ versionInfo?: (options?: FactoryVersionInfoOptions) => Promise<FactoryVersionInfo>;
46
+ /** Override the six-hour daemon drift check cadence in tests. */
47
+ versionCheckIntervalMs?: number;
43
48
  }
44
49
  interface GlobalOptions {
45
50
  backend: FleetBackend;
@@ -68,6 +73,7 @@ type ParsedCommand = {
68
73
  } | {
69
74
  kind: 'factory';
70
75
  action: 'run-once' | 'loop' | 'status' | 'loop-status' | 'kill-loop' | 'reap-orphans';
76
+ includeHeld?: boolean;
71
77
  } | {
72
78
  kind: 'factory';
73
79
  action: 'start';
@@ -109,6 +115,7 @@ export declare function parseGlobalOptions(argv: string[]): {
109
115
  globals: GlobalOptions;
110
116
  args: string[];
111
117
  };
118
+ export declare function reportFactoryVersionDrift(provider: FleetCliDeps['versionInfo'], logger: Logger): Promise<void>;
112
119
  export declare function formatLogArgs(args: unknown[]): string;
113
120
  export declare function resolveBrokerConnectionPath(startCwd?: string, env?: NodeJS.ProcessEnv): string | undefined;
114
121
  export type GithubIssueSelector = {
@@ -1 +1 @@
1
- {"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/cli/fleet.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/cli/fleet.ts"],"names":[],"mappings":"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"}
package/dist/cli/fleet.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { dirname, join, resolve } from 'node:path';
4
- import { randomUUID } from 'node:crypto';
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, parseOwnedBrokerAgentExitTimeoutMs, parseStandaloneBabysitTarget, readStandalonePullRequest, readLinearIssueWithCanonicalFallback, reapFactoryOrphansOnce, reapFactoryEnvironmentsOnce, readFactoryLoopHeartbeat, resolveFactoryStates, stateResolutionFromIds, standaloneBabysitterAgentName, renderAgentTask, resolveFactoryWorkspace, } from '../index.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';
11
11
  import { resolveTestGuidance } from '../dispatch/test-guidance.js';
12
12
  import { FakeFleetClient, FakeMountClient } from '../testing/index.js';
13
13
  import { GitAgentWorktreeManager } from '../git/agent-worktree.js';
@@ -17,6 +17,7 @@ import { ensureFactoryIntegrations, inspectFactoryIntegration, openIntegrationUr
17
17
  import { checkMountStaleness } from '../mount/relayfile-binary.js';
18
18
  import { MountAuthScopeError } from '../mount/mount-auth-error.js';
19
19
  import { resolveRelayWorkspaceKey } from '../fleet/relay-workspace-key.js';
20
+ import { isMeaningfullyBehind, readFactoryVersionInfo, } from '../version-info.js';
20
21
  import { GhCliIssuePublisher, RelayChannelNotionClaimStore, RelayChannelNotionContractPublisher, loadNotionIntakeManifest, runNotionIntake, } from '../intake/index.js';
21
22
  const autoDetectedIssueSources = new WeakSet();
22
23
  export async function runFleetCli(argv, deps = {}) {
@@ -200,8 +201,13 @@ export async function runFleetCli(argv, deps = {}) {
200
201
  registryPath: loaded.config.loop.registryPath,
201
202
  staleMs: loaded.config.loop.heartbeatStaleMs,
202
203
  fleet,
204
+ includeHeld: command.includeHeld,
205
+ logger: streamLogger(err),
206
+ });
207
+ writeJson(out, {
208
+ ...processes,
209
+ environments: await safeEnvironmentReap(Boolean(loaded.config.environments.kubernetes), deps.reapEnvironments),
203
210
  });
204
- writeJson(out, { ...processes, environments: await safeEnvironmentReap(deps.reapEnvironments) });
205
211
  return 0;
206
212
  }
207
213
  // Derive the workspace from the cloud session when it isn't pinned in
@@ -491,6 +497,18 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
491
497
  if (command.action === 'start') {
492
498
  const waiter = createStopSignalWaiter();
493
499
  let stoppedBySignal = false;
500
+ const logger = streamLogger(mountStderr);
501
+ const installedVersion = await safeFactoryVersionInfo(deps.versionInfo, false);
502
+ logger.info?.('[factory] daemon starting', installedVersion);
503
+ // The registry lookup is deliberately detached from the startup path.
504
+ // stop() only joins the bounded in-flight lookup during shutdown.
505
+ const versionMonitor = deps.versionInfo || !hasInjectedFactoryRuntime(deps)
506
+ ? startFactoryVersionMonitor({
507
+ versionInfo: deps.versionInfo,
508
+ intervalMs: deps.versionCheckIntervalMs,
509
+ logger,
510
+ })
511
+ : { stop: async () => { } };
494
512
  const flushAndResolve = async (code) => {
495
513
  try {
496
514
  await (deps.flushDaemonOutput ?? flushProcessOutput)();
@@ -558,6 +576,7 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
558
576
  return typeof code === 'number' ? code : 0;
559
577
  }
560
578
  finally {
579
+ await versionMonitor.stop();
561
580
  removeSignalHandlers();
562
581
  if (!stoppedBySignal) {
563
582
  await factory.stop();
@@ -570,7 +589,8 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
570
589
  return 0;
571
590
  }
572
591
  if (command.action === 'status') {
573
- writeJson(out, await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.heartbeatStaleMs));
592
+ 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));
574
594
  return 0;
575
595
  }
576
596
  if (command.action === 'loop-status') {
@@ -595,7 +615,7 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
595
615
  const reports = await factory.runLoop({ dryRun: globals.dryRun });
596
616
  writeJson(out, {
597
617
  reports,
598
- status: await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.heartbeatStaleMs),
618
+ status: await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.registryPath, config.loop.heartbeatStaleMs),
599
619
  });
600
620
  }
601
621
  finally {
@@ -809,10 +829,28 @@ async function ensureMountPath(mountFn, workspaceId, path, mountOpts, stderr, lo
809
829
  function resolveIntegrationsMountRoot(mount) {
810
830
  return mount.getLocalMountRoot?.() ?? resolve(process.cwd(), '.integrations');
811
831
  }
812
- async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, heartbeatStaleMs) {
832
+ async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, registryPath, heartbeatStaleMs, versionInfo) {
813
833
  const status = factory.status();
814
- const heartbeat = await readFactoryLoopHeartbeat(heartbeatPath);
834
+ const [heartbeat, registry] = await Promise.all([
835
+ readFactoryLoopHeartbeat(heartbeatPath),
836
+ readFactoryInFlightRegistry(registryPath),
837
+ ]);
838
+ // A registry stamped for a different heartbeat file belongs to another
839
+ // daemon instance; neither its dispatch nor held-agent view applies here.
840
+ const registryMatchesHeartbeat = !registry?.heartbeatPath || registry.heartbeatPath === heartbeatPath;
841
+ const registryDispatches = registryMatchesHeartbeat ? inFlightDispatchesFromRegistry(registry) : [];
842
+ const registryHeldAgents = registryMatchesHeartbeat ? heldAgentsFromRegistry(registry) : [];
843
+ const heldAgents = registryHeldAgents.length > 0 ? registryHeldAgents : (status.heldAgents ?? []);
844
+ const observableStatus = registryDispatches.length > 0
845
+ ? { ...status, inFlightDispatches: registryDispatches }
846
+ : status;
815
847
  const liveness = checkFactoryLoopLiveness(heartbeat, { staleMs: heartbeatStaleMs });
848
+ // A live heartbeat belongs to the daemon, while `status` is a fresh local
849
+ // Factory instance. Do not misreport that local instance's `not-running`
850
+ // state when an older daemon heartbeat predates this health field.
851
+ const readinessReconcile = liveness.ok
852
+ ? heartbeat?.readinessReconcile
853
+ : observableStatus.readinessReconcile;
816
854
  const eventListener = liveness.ok
817
855
  ? heartbeat?.eventListener ?? {
818
856
  state: 'unknown',
@@ -824,10 +862,13 @@ async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, heart
824
862
  };
825
863
  const health = mount.getLocalMountHealth?.();
826
864
  if (!health)
827
- return { ...status, eventListener };
865
+ return { ...observableStatus, ...versionInfo, heldAgents, eventListener, readinessReconcile };
828
866
  return {
829
- ...status,
867
+ ...observableStatus,
868
+ ...versionInfo,
869
+ heldAgents,
830
870
  eventListener,
871
+ readinessReconcile,
831
872
  localMountDegraded: health.degraded,
832
873
  ...(health.reason ? { localMountDegradedReason: health.reason } : {}),
833
874
  ...(health.localDir ? { localMountRoot: health.localDir } : {}),
@@ -839,6 +880,82 @@ async function factoryStatusWithMountHealth(factory, mount, heartbeatPath, heart
839
880
  },
840
881
  };
841
882
  }
883
+ function inFlightDispatchesFromRegistry(registry) {
884
+ if (!registry)
885
+ return [];
886
+ const grouped = new Map();
887
+ const claimPriority = { verified: 0, pending: 1, degraded: 2 };
888
+ for (const agent of registry.agents) {
889
+ if (!agent.issue)
890
+ continue;
891
+ const key = `${agent.issue.key}\u0000${agent.issue.uuid}\u0000${agent.issue.path}`;
892
+ const claim = agent.dispatchClaim ?? {
893
+ state: 'pending',
894
+ updatedAtMs: registry.updatedAtMs,
895
+ };
896
+ const existing = grouped.get(key);
897
+ const entry = existing ?? {
898
+ issue: { ...agent.issue },
899
+ agents: [],
900
+ claim: { ...claim },
901
+ };
902
+ if (claimPriority[claim.state] > claimPriority[entry.claim.state]) {
903
+ entry.claim = { ...claim };
904
+ }
905
+ if (!entry.agents.some((candidate) => candidate.name === agent.name)) {
906
+ entry.agents.push({
907
+ name: agent.name,
908
+ ...(agent.role ? { role: agent.role } : {}),
909
+ ...(agent.sessionRef ? { sessionRef: agent.sessionRef } : {}),
910
+ ...(agent.invocationId ? { invocationId: agent.invocationId } : {}),
911
+ ...(agent.node ? { node: agent.node } : {}),
912
+ });
913
+ }
914
+ grouped.set(key, entry);
915
+ }
916
+ return [...grouped.values()]
917
+ .map((entry) => ({
918
+ ...entry,
919
+ agents: entry.agents.sort((left, right) => left.name.localeCompare(right.name)),
920
+ }))
921
+ .sort((left, right) => left.issue.key.localeCompare(right.issue.key));
922
+ }
923
+ const DEFAULT_VERSION_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1_000;
924
+ async function safeFactoryVersionInfo(provider, includeRegistry) {
925
+ try {
926
+ return await (provider ?? readFactoryVersionInfo)({ includeRegistry });
927
+ }
928
+ catch {
929
+ return { version: 'unknown' };
930
+ }
931
+ }
932
+ export async function reportFactoryVersionDrift(provider, logger) {
933
+ const info = await safeFactoryVersionInfo(provider, true);
934
+ if (!isMeaningfullyBehind(info) || !info.latestVersion)
935
+ return;
936
+ logger.warn?.(`[factory] version drift detected: running ${info.version}; published latest ${info.latestVersion}`, {
937
+ ...(info.versionsBehind !== undefined ? { versionsBehind: info.versionsBehind } : {}),
938
+ ...(info.installedAt ? { installedAt: info.installedAt } : {}),
939
+ });
940
+ }
941
+ function startFactoryVersionMonitor(input) {
942
+ let pending;
943
+ const check = () => {
944
+ if (pending)
945
+ return;
946
+ pending = reportFactoryVersionDrift(input.versionInfo, input.logger)
947
+ .finally(() => { pending = undefined; });
948
+ };
949
+ check();
950
+ const interval = setInterval(check, input.intervalMs ?? DEFAULT_VERSION_CHECK_INTERVAL_MS);
951
+ interval.unref?.();
952
+ return {
953
+ stop: async () => {
954
+ clearInterval(interval);
955
+ await pending;
956
+ },
957
+ };
958
+ }
842
959
  function writeMountRefreshSummary(refreshedStaleMounts, stderr, debug) {
843
960
  if (debug) {
844
961
  for (const mount of [...refreshedStaleMounts].sort((left, right) => left.path.localeCompare(right.path))) {
@@ -889,7 +1006,18 @@ function parseFactoryCommand(args) {
889
1006
  if (action === 'start') {
890
1007
  return { kind: 'factory', action, ...parseFactoryStartFlags([issueOrPr, ...flags]) };
891
1008
  }
892
- if (action === 'run-once' || action === 'loop' || action === 'status' || action === 'loop-status' || action === 'kill-loop' || action === 'reap-orphans') {
1009
+ if (action === 'reap-orphans') {
1010
+ const values = [issueOrPr, ...flags].filter((value) => Boolean(value));
1011
+ const unexpected = values.find((value) => value !== '--include-held');
1012
+ if (unexpected)
1013
+ throw new Error(`Unknown factory reap-orphans option: ${unexpected}`);
1014
+ return {
1015
+ kind: 'factory',
1016
+ action,
1017
+ ...(values.includes('--include-held') ? { includeHeld: true } : {}),
1018
+ };
1019
+ }
1020
+ if (action === 'run-once' || action === 'loop' || action === 'status' || action === 'loop-status' || action === 'kill-loop') {
893
1021
  return { kind: 'factory', action };
894
1022
  }
895
1023
  if (action === 'canary') {
@@ -1059,7 +1187,10 @@ function hasInjectedFactoryRuntime(deps) {
1059
1187
  async function buildFleet(globals, loaded, deps) {
1060
1188
  if (deps.fleet)
1061
1189
  return deps.fleet;
1062
- if (globals.backend === 'internal' && hasExplicitFixtureFiles(loaded))
1190
+ // fixtureFiles are an explicit hermetic harness opt-in. Keep both backend
1191
+ // command paths off real fleet infrastructure so the durable relay CLI path
1192
+ // can be exercised end-to-end with deterministic fixtures too.
1193
+ if (hasExplicitFixtureFiles(loaded))
1063
1194
  return new FakeFleetClient();
1064
1195
  const cwd = process.cwd();
1065
1196
  const env = deps.env ?? process.env;
@@ -1291,7 +1422,7 @@ async function buildMount(loaded, deps, observability = {}) {
1291
1422
  if (deps.mount)
1292
1423
  return deps.mount;
1293
1424
  if (hasExplicitFixtureFiles(loaded))
1294
- return new FakeMountClient(loaded.fixtureFiles);
1425
+ return new FixtureMountClient(loaded.fixtureFiles);
1295
1426
  let mount;
1296
1427
  mount = await (deps.cloudMountFromConfig ?? RelayfileCloudMountClient.fromConfig)({
1297
1428
  workspaceId: loaded.config.workspaceId,
@@ -1303,6 +1434,21 @@ async function buildMount(loaded, deps, observability = {}) {
1303
1434
  return mount;
1304
1435
  }
1305
1436
  const hasExplicitFixtureFiles = (loaded) => loaded?.fixtureFiles !== undefined;
1437
+ /** Hermetic fixture runs have no server event feed, so expose the in-memory
1438
+ * tree shape as their change watermark. The discovery tree cache only
1439
+ * remembers which paths exist under a prefix, not their content, so the
1440
+ * watermark must track path membership only. Hashing full file entries
1441
+ * (as an earlier version of this did) also hashes each file's `revision`,
1442
+ * which FakeMountClient.writeFile bumps on every write — so any factory
1443
+ * writeback mid-sweep (a draft, a confirmed write) would change the digest
1444
+ * with no fixture change-event to explain the gap, and the checkpoint
1445
+ * would never persist across a sweep that actually dispatched anything. */
1446
+ class FixtureMountClient extends FakeMountClient {
1447
+ async getEventHighWatermark() {
1448
+ const paths = [...this.files.keys()].sort();
1449
+ return `fixture:${createHash('sha256').update(JSON.stringify(paths)).digest('hex')}`;
1450
+ }
1451
+ }
1306
1452
  async function isAllowedFactoryDraft(path, content, opts, mount, config) {
1307
1453
  if (!opts?.guarded)
1308
1454
  return false;
@@ -1481,7 +1627,7 @@ async function findGithubIssueThroughConnection(mount, selector, config, issueAr
1481
1627
  return found[0].issue;
1482
1628
  }
1483
1629
  async function issueProjectionStatus(factory, mount, config) {
1484
- const status = await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.heartbeatStaleMs);
1630
+ const status = await factoryStatusWithMountHealth(factory, mount, config.loop.heartbeatPath, config.loop.registryPath, config.loop.heartbeatStaleMs);
1485
1631
  const githubConnection = mount.integrationConnections
1486
1632
  ? await mount.integrationConnections.getStatus('github')
1487
1633
  : undefined;
@@ -1664,14 +1810,23 @@ function requireValue(args, index, flag) {
1664
1810
  function writeJson(out, value) {
1665
1811
  out.write(`${JSON.stringify(value, null, 2)}\n`);
1666
1812
  }
1667
- async function safeEnvironmentReap(reap = reapFactoryEnvironmentsOnce) {
1813
+ async function safeEnvironmentReap(configured, reap = reapFactoryEnvironmentsOnce) {
1814
+ if (!configured) {
1815
+ return {
1816
+ applicable: false,
1817
+ reason: 'kubernetes environment provider is not configured',
1818
+ reaped: [],
1819
+ retained: [],
1820
+ };
1821
+ }
1668
1822
  try {
1669
- return await reap();
1823
+ return { applicable: true, ...await reap() };
1670
1824
  }
1671
1825
  catch (error) {
1672
1826
  // Process cleanup remains useful on hosts without kubectl or an active
1673
1827
  // cluster. Surface the skipped backstop without failing the whole command.
1674
1828
  return {
1829
+ applicable: true,
1675
1830
  reaped: [],
1676
1831
  retained: [],
1677
1832
  error: error instanceof Error ? error.message : String(error),
@@ -1769,7 +1924,8 @@ Commands:
1769
1924
  loop Run the bounded loop configured in factory.config.json
1770
1925
  loop-status Print heartbeat/liveness status for the loop
1771
1926
  kill-loop Send SIGTERM to the heartbeat pid
1772
- reap-orphans Reap stale factory-owned agents
1927
+ reap-orphans [--include-held]
1928
+ Report stale processes and held agents; opt in to release held agents past deadline
1773
1929
  canary <KEY|path> Check that a known issue is dispatch-ready
1774
1930
  triage <KEY|path> Triage one issue and print the decision
1775
1931
  dispatch <KEY|path> Triage and dispatch one issue