@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.
- package/README.md +36 -1
- package/dist/cli/fleet.d.ts +8 -1
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +172 -16
- package/dist/cli/fleet.js.map +1 -1
- package/dist/config/schema.d.ts +796 -273
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +44 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/delivery/ticket-dispatch.d.ts +15 -0
- package/dist/delivery/ticket-dispatch.d.ts.map +1 -0
- package/dist/delivery/ticket-dispatch.js +20 -0
- package/dist/delivery/ticket-dispatch.js.map +1 -0
- package/dist/git/agent-worktree.d.ts.map +1 -1
- package/dist/git/agent-worktree.js +4 -0
- package/dist/git/agent-worktree.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/issue-key-match.d.ts +6 -0
- package/dist/issue-key-match.d.ts.map +1 -1
- package/dist/issue-key-match.js +14 -0
- package/dist/issue-key-match.js.map +1 -1
- package/dist/mount/relayfile-github-connection-write.d.ts.map +1 -1
- package/dist/mount/relayfile-github-connection-write.js +6 -3
- package/dist/mount/relayfile-github-connection-write.js.map +1 -1
- package/dist/orchestrator/batch-tracker.d.ts +7 -1
- package/dist/orchestrator/batch-tracker.d.ts.map +1 -1
- package/dist/orchestrator/batch-tracker.js +2 -0
- package/dist/orchestrator/batch-tracker.js.map +1 -1
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +1940 -153
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/orchestrator/index.d.ts +1 -1
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/reaper.d.ts +16 -2
- package/dist/orchestrator/reaper.d.ts.map +1 -1
- package/dist/orchestrator/reaper.js +79 -5
- package/dist/orchestrator/reaper.js.map +1 -1
- package/dist/ports/fleet.d.ts +4 -0
- package/dist/ports/fleet.d.ts.map +1 -1
- package/dist/ports/mount.d.ts +2 -0
- package/dist/ports/mount.d.ts.map +1 -1
- package/dist/ports/state.d.ts +60 -1
- package/dist/ports/state.d.ts.map +1 -1
- package/dist/state/file-state-store.d.ts +9 -1
- package/dist/state/file-state-store.d.ts.map +1 -1
- package/dist/state/file-state-store.js +160 -2
- package/dist/state/file-state-store.js.map +1 -1
- package/dist/state/in-memory-state-store.d.ts +7 -1
- package/dist/state/in-memory-state-store.d.ts.map +1 -1
- package/dist/state/in-memory-state-store.js +71 -0
- package/dist/state/in-memory-state-store.js.map +1 -1
- package/dist/triage/schema.d.ts +36 -0
- package/dist/triage/schema.d.ts.map +1 -1
- package/dist/triage/schema.js +2 -0
- package/dist/triage/schema.js.map +1 -1
- package/dist/types.d.ts +64 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version-info.d.ts +21 -0
- package/dist/version-info.d.ts.map +1 -0
- package/dist/version-info.js +146 -0
- package/dist/version-info.js.map +1 -0
- package/dist/writeback/github.d.ts.map +1 -1
- package/dist/writeback/github.js +12 -0
- package/dist/writeback/github.js.map +1 -1
- 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,
|
package/dist/cli/fleet.d.ts
CHANGED
|
@@ -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 = {
|
package/dist/cli/fleet.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/cli/fleet.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAG1E,OAAO,EAA6B,KAAK,qBAAqB,EAAE,MAAM,6BAA6B,CAAA;AAEnG,OAAO,EAKL,aAAa,EACb,WAAW,EAEX,iBAAiB,
|
|
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
|
-
|
|
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
|
|
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 { ...
|
|
865
|
+
return { ...observableStatus, ...versionInfo, heldAgents, eventListener, readinessReconcile };
|
|
828
866
|
return {
|
|
829
|
-
...
|
|
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 === '
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|