@agent-relay/factory 0.1.18 → 0.1.20
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 +52 -0
- package/dist/cli/fleet.d.ts +10 -0
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +333 -23
- package/dist/cli/fleet.js.map +1 -1
- package/dist/config/local-clone-paths.d.ts +18 -0
- package/dist/config/local-clone-paths.d.ts.map +1 -0
- package/dist/config/local-clone-paths.js +188 -0
- package/dist/config/local-clone-paths.js.map +1 -0
- package/dist/config/schema.d.ts +84 -2
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +71 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/dispatch/templates.d.ts +16 -0
- package/dist/dispatch/templates.d.ts.map +1 -1
- package/dist/dispatch/templates.js +81 -17
- package/dist/dispatch/templates.js.map +1 -1
- package/dist/fleet/internal-fleet-client.d.ts +4 -0
- package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
- package/dist/fleet/internal-fleet-client.js +6 -1
- package/dist/fleet/internal-fleet-client.js.map +1 -1
- package/dist/fleet/relay-fleet-client.d.ts +3 -0
- package/dist/fleet/relay-fleet-client.d.ts.map +1 -1
- package/dist/fleet/relay-fleet-client.js +17 -4
- package/dist/fleet/relay-fleet-client.js.map +1 -1
- package/dist/github/index.d.ts +2 -0
- package/dist/github/index.d.ts.map +1 -1
- package/dist/github/index.js +1 -0
- package/dist/github/index.js.map +1 -1
- package/dist/github/standalone-babysitter.d.ts +33 -0
- package/dist/github/standalone-babysitter.d.ts.map +1 -0
- package/dist/github/standalone-babysitter.js +214 -0
- package/dist/github/standalone-babysitter.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/logging.d.ts +17 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +347 -0
- package/dist/logging.js.map +1 -0
- package/dist/mount/relayfile-cloud-mount-client.js +10 -0
- package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
- package/dist/mount/relayfile-github-connection-write.d.ts.map +1 -1
- package/dist/mount/relayfile-github-connection-write.js +19 -7
- package/dist/mount/relayfile-github-connection-write.js.map +1 -1
- package/dist/orchestrator/batch-tracker.d.ts +3 -0
- package/dist/orchestrator/batch-tracker.d.ts.map +1 -1
- package/dist/orchestrator/batch-tracker.js +28 -0
- package/dist/orchestrator/batch-tracker.js.map +1 -1
- package/dist/orchestrator/factory.d.ts +2 -1
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +3022 -176
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/orchestrator/reaper.d.ts.map +1 -1
- package/dist/orchestrator/reaper.js +6 -4
- package/dist/orchestrator/reaper.js.map +1 -1
- package/dist/ports/fleet.d.ts +24 -0
- package/dist/ports/fleet.d.ts.map +1 -1
- package/dist/ports/mount.d.ts +7 -1
- package/dist/ports/mount.d.ts.map +1 -1
- package/dist/ports/state.d.ts +114 -0
- package/dist/ports/state.d.ts.map +1 -1
- package/dist/ports/writeback.d.ts +2 -0
- package/dist/ports/writeback.d.ts.map +1 -1
- package/dist/state/file-state-store.d.ts +33 -2
- package/dist/state/file-state-store.d.ts.map +1 -1
- package/dist/state/file-state-store.js +520 -10
- package/dist/state/file-state-store.js.map +1 -1
- package/dist/state/in-memory-state-store.d.ts +31 -1
- package/dist/state/in-memory-state-store.d.ts.map +1 -1
- package/dist/state/in-memory-state-store.js +245 -0
- package/dist/state/in-memory-state-store.js.map +1 -1
- package/dist/testing/fakes.d.ts +7 -0
- package/dist/testing/fakes.d.ts.map +1 -1
- package/dist/testing/fakes.js +5 -0
- package/dist/testing/fakes.js.map +1 -1
- package/dist/triage/agent-names.d.ts +13 -0
- package/dist/triage/agent-names.d.ts.map +1 -0
- package/dist/triage/agent-names.js +54 -0
- package/dist/triage/agent-names.js.map +1 -0
- package/dist/triage/heuristic.d.ts.map +1 -1
- package/dist/triage/heuristic.js +16 -23
- package/dist/triage/heuristic.js.map +1 -1
- package/dist/triage/llm.d.ts.map +1 -1
- package/dist/triage/llm.js +3 -2
- package/dist/triage/llm.js.map +1 -1
- package/dist/triage/schema.d.ts +26 -26
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/writeback/github.d.ts +1 -0
- package/dist/writeback/github.d.ts.map +1 -1
- package/dist/writeback/github.js +11 -0
- package/dist/writeback/github.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,6 +116,7 @@ From a source checkout instead of an npm install, run
|
|
|
116
116
|
| `factory status` | Print current factory status as JSON. |
|
|
117
117
|
| `factory triage <KEY\|path>` | Triage one issue and print the decision. |
|
|
118
118
|
| `factory dispatch <KEY\|path>` | Triage + dispatch one issue. Honors `--dry-run`. |
|
|
119
|
+
| `factory babysit <PR\|PR-URL>` | Spawn a one-shot babysitter for an existing open PR, even when it was not created by Factory. |
|
|
119
120
|
| `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. |
|
|
120
121
|
|
|
121
122
|
Global options work anywhere in the args: `--config <path>`, `--dry-run`,
|
|
@@ -127,6 +128,15 @@ to 30 minutes and can also be set with `FACTORY_AGENT_EXIT_TIMEOUT_MS`.
|
|
|
127
128
|
(There are a few more operational commands — `loop-status`, `kill-loop`,
|
|
128
129
|
`reap-orphans`, `close-probe` — for running the daemon in production.)
|
|
129
130
|
|
|
131
|
+
`factory babysit 10` uses `repos.default`; a full URL such as
|
|
132
|
+
`factory babysit https://github.com/org/repo/pull/10` supplies the repository
|
|
133
|
+
directly. The explicit command is opt-in on its own and does not require
|
|
134
|
+
`babysitter.enabled`. It rejects closed, merged, and draft PRs, uses a linked
|
|
135
|
+
issue spec when one can be resolved (otherwise the PR title/body), fixes the
|
|
136
|
+
existing PR branch, and always leaves the final review and merge to a human.
|
|
137
|
+
The command prints a spawn receipt and returns; the PR-keyed task-exit worker
|
|
138
|
+
continues on the relay broker and reports completion or access blockers there.
|
|
139
|
+
|
|
130
140
|
### Scheduled sync-fidelity canary
|
|
131
141
|
|
|
132
142
|
`factory canary` is the regression detector for upstream sync drift: if a synced
|
|
@@ -202,6 +212,23 @@ required capability (a named `node` target passes through), the node runs the
|
|
|
202
212
|
agent in its mapped checkout, and the orchestrator detects exits by reconciling
|
|
203
213
|
its tracked agents against the engine roster.
|
|
204
214
|
|
|
215
|
+
Relay dispatch is lifecycle-owned, not fire-and-forget. A one-shot `factory
|
|
216
|
+
dispatch --backend relay` keeps a small publisher runtime alive until the
|
|
217
|
+
remote branch has produced a PR, terminal issue writeback is acknowledged, and
|
|
218
|
+
remote agents are released. The lifecycle (including a per-run branch,
|
|
219
|
+
placement results, PR receipt, and a fenced owner lease) is persisted beside
|
|
220
|
+
the configured loop registry so `factory start` or a replacement dispatch
|
|
221
|
+
process on the same control-plane host can take over after a crash. Execution
|
|
222
|
+
nodes never need access to that state file, and remote PIDs are never signalled
|
|
223
|
+
as local processes.
|
|
224
|
+
|
|
225
|
+
The supported topology is one Factory control-plane host per workspace, with
|
|
226
|
+
any number of relay execution nodes. Multiple Factory processes on that host
|
|
227
|
+
are fenced through the shared `FileStateStore` lock/lease. Active/active Factory
|
|
228
|
+
control planes on different hosts are intentionally unsupported: the current
|
|
229
|
+
Relayfile and Relay messaging APIs do not expose a shared compare-and-set lease,
|
|
230
|
+
so separate local state files cannot provide a truthful cross-host fence.
|
|
231
|
+
|
|
205
232
|
Tokens involved — set only the first one on the orchestrator host:
|
|
206
233
|
|
|
207
234
|
| Token | Prefix | Who holds it |
|
|
@@ -225,6 +252,31 @@ an invalid config fails fast with a field-level error. See
|
|
|
225
252
|
and [`test/fixtures/factory.config.json`](test/fixtures/factory.config.json) for a
|
|
226
253
|
worked example (including offline fixture mode).
|
|
227
254
|
|
|
255
|
+
`cloneRoot` and every `clonePaths` value accept `~` or a leading `~/`, expanded
|
|
256
|
+
against the current user's home directory. Named-user forms such as `~alice`
|
|
257
|
+
are rejected because Node cannot expand them portably.
|
|
258
|
+
|
|
259
|
+
For a local, single-repository run, the checkout mapping can be omitted:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"repos": {
|
|
264
|
+
"org": "your-org",
|
|
265
|
+
"names": ["your-repo"],
|
|
266
|
+
"default": "your-org/your-repo"
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Run Factory from that repository (or one of its subdirectories). When exactly
|
|
272
|
+
one `repos.names` entry is configured and no `cloneRoot` or `clonePaths` field is
|
|
273
|
+
supplied, Factory resolves the checkout's git top-level and uses it only if a
|
|
274
|
+
GitHub remote matches the resolved `org/name`. The inference is logged. Missing,
|
|
275
|
+
unparseable, or mismatched remotes fail with a config-oriented error instead of
|
|
276
|
+
silently dispatching in the wrong directory. Explicit local clone paths are also
|
|
277
|
+
preflighted before commands that can dispatch through the internal backend;
|
|
278
|
+
relay-backend paths are left for their worker nodes to validate.
|
|
279
|
+
|
|
228
280
|
## Notes
|
|
229
281
|
|
|
230
282
|
- The daemon is headless by design; tools like Pear can consume this package and
|
package/dist/cli/fleet.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LocalClonePathOptions } from '../config/local-clone-paths.js';
|
|
1
2
|
import { createFactory, createFleet, ensureRelayBroker, type Capability, type Factory, type FactoryConfig, type FleetBackend, type FleetClient, type GhRunner, type FactoryStateResolution, type MountClient, type ProbeCloser, type RelayfileCloudMountClientConfig, type ResolvedFactoryWorkspace } from '../index.js';
|
|
2
3
|
interface FleetCliDeps {
|
|
3
4
|
fleet?: FleetClient;
|
|
@@ -18,10 +19,13 @@ interface FleetCliDeps {
|
|
|
18
19
|
stderr?: Pick<NodeJS.WriteStream, 'write'>;
|
|
19
20
|
probeCloser?: ProbeCloser;
|
|
20
21
|
probePrGhRunner?: GhRunner;
|
|
22
|
+
babysitPrGhRunner?: GhRunner;
|
|
21
23
|
now?: () => number;
|
|
22
24
|
stopSignalProcessLike?: Pick<NodeJS.Process, 'once' | 'off'>;
|
|
23
25
|
daemonExit?: (code: number) => void;
|
|
24
26
|
flushDaemonOutput?: () => Promise<void>;
|
|
27
|
+
/** Hermetic local-checkout probes for CLI integration tests. */
|
|
28
|
+
localClonePathOptions?: LocalClonePathOptions;
|
|
25
29
|
}
|
|
26
30
|
interface GlobalOptions {
|
|
27
31
|
backend: FleetBackend;
|
|
@@ -62,6 +66,11 @@ type ParsedCommand = {
|
|
|
62
66
|
} | {
|
|
63
67
|
kind: 'factory-dispatch';
|
|
64
68
|
issue: string;
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'factory-babysit';
|
|
71
|
+
prNumber: number;
|
|
72
|
+
repo?: string;
|
|
73
|
+
url?: string;
|
|
65
74
|
} | {
|
|
66
75
|
kind: 'factory-close-probe';
|
|
67
76
|
prNumber: number;
|
|
@@ -74,6 +83,7 @@ export declare function parseGlobalOptions(argv: string[]): {
|
|
|
74
83
|
globals: GlobalOptions;
|
|
75
84
|
args: string[];
|
|
76
85
|
};
|
|
86
|
+
export declare function formatLogArgs(args: unknown[]): string;
|
|
77
87
|
export declare function resolveBrokerConnectionPath(startCwd?: string): string | undefined;
|
|
78
88
|
export declare function installFactoryStopSignalHandlers(factory: Factory, opts?: {
|
|
79
89
|
exit?: (code: number) => void;
|
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":"
|
|
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,9 +1,12 @@
|
|
|
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 { stringifyLogValue } from '../logging.js';
|
|
4
5
|
import { ensureLocalMount } from '../mount/local-mount-preflight.js';
|
|
5
|
-
import {
|
|
6
|
+
import { resolveLocalFactoryConfig } from '../config/local-clone-paths.js';
|
|
7
|
+
import { FileStateStore, RelayfileCloudMountClient, checkFactoryLoopLiveness, closeProbePr, createFactory, createFleet, ensureRelayBroker, defaultGhRunner, explicitLinkedIssueKey, githubIssuePathParts, githubWatchStatePath, isInFactoryScope, parseGithubFactoryIssue, parseOwnedBrokerAgentExitTimeoutMs, parseStandaloneBabysitTarget, readStandalonePullRequest, readLinearIssueWithCanonicalFallback, reapFactoryOrphansOnce, readFactoryLoopHeartbeat, resolveFactoryStates, stateResolutionFromIds, standaloneBabysitterAgentName, renderAgentTask, resolveFactoryWorkspace, } from '../index.js';
|
|
6
8
|
import { FakeFleetClient, FakeMountClient } from '../testing/index.js';
|
|
9
|
+
const autoDetectedIssueSources = new WeakSet();
|
|
7
10
|
export async function runFleetCli(argv, deps = {}) {
|
|
8
11
|
const out = deps.stdout ?? process.stdout;
|
|
9
12
|
const err = deps.stderr ?? process.stderr;
|
|
@@ -33,7 +36,15 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
33
36
|
writeJson(out, result);
|
|
34
37
|
return 0;
|
|
35
38
|
}
|
|
36
|
-
const
|
|
39
|
+
const resolvesLocalClonePaths = globals.backend === 'internal' && commandUsesLocalCheckout(command);
|
|
40
|
+
const loaded = command.kind.startsWith('factory')
|
|
41
|
+
? await loadConfig(globals.config, {
|
|
42
|
+
...deps.localClonePathOptions,
|
|
43
|
+
inferFromCwd: resolvesLocalClonePaths,
|
|
44
|
+
logger: streamLogger(err),
|
|
45
|
+
validateConfiguredCheckouts: resolvesLocalClonePaths && (deps.localClonePathOptions?.validateConfiguredCheckouts ?? !hasInjectedFactoryRuntime(deps)),
|
|
46
|
+
})
|
|
47
|
+
: undefined;
|
|
37
48
|
fleet = await buildFleet(globals, loaded, deps);
|
|
38
49
|
switch (command.kind) {
|
|
39
50
|
case 'spawn': {
|
|
@@ -67,7 +78,8 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
67
78
|
case 'factory':
|
|
68
79
|
case 'factory-canary':
|
|
69
80
|
case 'factory-triage':
|
|
70
|
-
case 'factory-dispatch':
|
|
81
|
+
case 'factory-dispatch':
|
|
82
|
+
case 'factory-babysit': {
|
|
71
83
|
if (!loaded)
|
|
72
84
|
throw new Error('factory command requires config');
|
|
73
85
|
if (command.kind === 'factory' && command.action === 'reap-orphans') {
|
|
@@ -94,6 +106,9 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
94
106
|
if (!workspaceId)
|
|
95
107
|
throw new Error('factory command could not resolve a workspaceId');
|
|
96
108
|
const mount = await buildMount(loaded, deps);
|
|
109
|
+
if (command.kind === 'factory-babysit') {
|
|
110
|
+
return await runStandaloneBabysitCommand(command, mount, fleet, loaded.config, globals, out, deps, workspaceId, acceptableMountIds);
|
|
111
|
+
}
|
|
97
112
|
// Resolve Linear workflow states only when Linear is the issue source.
|
|
98
113
|
// A GitHub-only workspace has no /linear/states catalog and uses labels
|
|
99
114
|
// for lifecycle state, so it must not depend on that provider at startup.
|
|
@@ -304,9 +319,160 @@ async function runFactoryCommand(command, factory, mount, fleet, config, globals
|
|
|
304
319
|
writeJson(out, decision);
|
|
305
320
|
return 0;
|
|
306
321
|
}
|
|
322
|
+
if (command.kind === 'factory-dispatch' && globals.backend === 'relay' && !globals.dryRun) {
|
|
323
|
+
// A relay dispatch is not placement-only. This process becomes the durable
|
|
324
|
+
// lifecycle publisher (or attaches to the current owner's durable row),
|
|
325
|
+
// and stays through takeover/publication/writeback/release to terminal.
|
|
326
|
+
await factory.start({ mode: 'dispatch-owner' });
|
|
327
|
+
try {
|
|
328
|
+
const result = await factory.dispatch(decision, { dryRun: false });
|
|
329
|
+
writeJson(out, result);
|
|
330
|
+
await factory.waitForDispatchTerminal(result.issue);
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
await factory.stop();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
307
337
|
writeJson(out, await factory.dispatch(decision, { dryRun: globals.dryRun }));
|
|
308
338
|
return 0;
|
|
309
339
|
}
|
|
340
|
+
async function runStandaloneBabysitCommand(command, mount, fleet, config, globals, out, deps, workspaceId, acceptableMountIds) {
|
|
341
|
+
const repo = resolveStandaloneBabysitRepo(command.repo, config);
|
|
342
|
+
const clonePath = standaloneBabysitClonePath(repo, config);
|
|
343
|
+
const mountFn = deps.ensureLocalMount ?? ensureLocalMount;
|
|
344
|
+
const mountOpts = { acceptableWorkspaceIds: acceptableMountIds };
|
|
345
|
+
await ensureStandaloneBabysitMount(mountFn, workspaceId, process.cwd(), mountOpts, deps.stderr);
|
|
346
|
+
if (clonePath && resolve(clonePath) !== resolve(process.cwd())) {
|
|
347
|
+
await ensureStandaloneBabysitMount(mountFn, workspaceId, clonePath, mountOpts, deps.stderr);
|
|
348
|
+
}
|
|
349
|
+
const pr = await readStandalonePullRequest(mount, { repo, prNumber: command.prNumber, url: command.url }, deps.babysitPrGhRunner ?? defaultGhRunner);
|
|
350
|
+
const state = pr.state?.trim().toUpperCase();
|
|
351
|
+
if (pr.merged || state === 'MERGED') {
|
|
352
|
+
throw new Error(`factory babysit requires an open PR; ${repo} PR #${pr.number} is merged`);
|
|
353
|
+
}
|
|
354
|
+
if (state !== 'OPEN') {
|
|
355
|
+
throw new Error(`factory babysit requires an open PR; ${repo} PR #${pr.number} state is ${state ?? 'UNKNOWN'}`);
|
|
356
|
+
}
|
|
357
|
+
if (pr.draft) {
|
|
358
|
+
throw new Error(`factory babysit skips draft PRs; ${repo} PR #${pr.number} is a draft`);
|
|
359
|
+
}
|
|
360
|
+
if (pr.crossRepository) {
|
|
361
|
+
throw new Error(`factory babysit refuses cross-repository PRs by default; ${repo} PR #${pr.number} head is ${pr.headRepo ?? 'unknown'}`);
|
|
362
|
+
}
|
|
363
|
+
const linkedIssueKey = explicitLinkedIssueKey(pr.body);
|
|
364
|
+
let issue = {
|
|
365
|
+
key: `${repo}#${pr.number}`,
|
|
366
|
+
title: pr.title,
|
|
367
|
+
description: pr.body || '(No PR description was provided.)',
|
|
368
|
+
};
|
|
369
|
+
let specSource = 'pull-request';
|
|
370
|
+
if (linkedIssueKey) {
|
|
371
|
+
try {
|
|
372
|
+
const linked = await readIssueArg(mount, linkedIssueKey, config);
|
|
373
|
+
issue = {
|
|
374
|
+
key: linked.key,
|
|
375
|
+
title: linked.title,
|
|
376
|
+
description: linked.description,
|
|
377
|
+
};
|
|
378
|
+
specSource = 'linked-issue';
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// The PR remains independently babysittable when its referenced issue is
|
|
382
|
+
// not connected to this workspace. Its own title/body become the spec.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const agentName = standaloneBabysitterAgentName(repo, pr.number);
|
|
386
|
+
const task = renderAgentTask({
|
|
387
|
+
issue,
|
|
388
|
+
route: { repo, clonePath },
|
|
389
|
+
role: 'babysitter',
|
|
390
|
+
config: { mergePolicy: config.mergePolicy, terminalState: config.terminalState },
|
|
391
|
+
reviewerName: '',
|
|
392
|
+
pr: {
|
|
393
|
+
number: pr.number,
|
|
394
|
+
url: pr.url,
|
|
395
|
+
headRef: pr.headRef,
|
|
396
|
+
headSha: pr.headSha,
|
|
397
|
+
baseRef: pr.baseRef,
|
|
398
|
+
headRepo: pr.headRepo,
|
|
399
|
+
crossRepository: pr.crossRepository,
|
|
400
|
+
maintainerCanModify: pr.maintainerCanModify,
|
|
401
|
+
},
|
|
402
|
+
standaloneBabysitter: { specSource },
|
|
403
|
+
integrationsMountRoot: resolve(process.cwd(), '.integrations'),
|
|
404
|
+
});
|
|
405
|
+
const receiptBase = {
|
|
406
|
+
agent: agentName,
|
|
407
|
+
repo,
|
|
408
|
+
prNumber: pr.number,
|
|
409
|
+
url: pr.url,
|
|
410
|
+
source: pr.source,
|
|
411
|
+
specSource,
|
|
412
|
+
...(specSource === 'linked-issue' ? { linkedIssue: issue.key } : {}),
|
|
413
|
+
};
|
|
414
|
+
if (globals.dryRun) {
|
|
415
|
+
writeJson(out, { status: 'dry-run', ...receiptBase });
|
|
416
|
+
return 0;
|
|
417
|
+
}
|
|
418
|
+
const roster = await fleet.roster();
|
|
419
|
+
if (roster.agents.some((agent) => agent.name === agentName)) {
|
|
420
|
+
writeJson(out, { status: 'already-running', ...receiptBase });
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
const spawned = await fleet.spawn({
|
|
424
|
+
name: agentName,
|
|
425
|
+
capability: config.agentCapabilities.babysitter,
|
|
426
|
+
node: 'self',
|
|
427
|
+
repo,
|
|
428
|
+
clonePath,
|
|
429
|
+
task,
|
|
430
|
+
model: config.models.babysitter,
|
|
431
|
+
cwd: clonePath,
|
|
432
|
+
invocationId: `factory-babysit:${repo}#${pr.number}`,
|
|
433
|
+
});
|
|
434
|
+
fleet.preserveInfrastructureOnDispose?.();
|
|
435
|
+
writeJson(out, { status: 'spawned', ...receiptBase, agent: spawned.name });
|
|
436
|
+
return 0;
|
|
437
|
+
}
|
|
438
|
+
async function ensureStandaloneBabysitMount(mountFn, workspaceId, startDir, options, stderr = process.stderr) {
|
|
439
|
+
try {
|
|
440
|
+
await mountFn(workspaceId, startDir, options);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
444
|
+
stderr.write(`[factory] warning: could not start relayfile mount for standalone babysitter at ${resolve(startDir)}; ` +
|
|
445
|
+
`the agent will use the GitHub CLI fallback: ${message}\n`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function resolveStandaloneBabysitRepo(repo, config) {
|
|
449
|
+
const configured = repo ?? config.repos.default;
|
|
450
|
+
if (!configured) {
|
|
451
|
+
throw new Error('factory babysit <PR-number> requires repos.default; pass a full GitHub PR URL instead');
|
|
452
|
+
}
|
|
453
|
+
if (configured.includes('/'))
|
|
454
|
+
return configured;
|
|
455
|
+
const exactRoute = config.repos.byLabel[configured];
|
|
456
|
+
if (exactRoute)
|
|
457
|
+
return exactRoute;
|
|
458
|
+
const routed = [...new Set(Object.values(config.repos.byLabel).filter((candidate) => candidate === configured || candidate.endsWith(`/${configured}`)))];
|
|
459
|
+
if (routed.length === 1)
|
|
460
|
+
return routed[0];
|
|
461
|
+
if (routed.length > 1) {
|
|
462
|
+
throw new Error(`Unable to resolve repository ${configured}: matches multiple configured repos (${routed.sort().join(', ')}); ` +
|
|
463
|
+
'set repos.default to owner/repo or pass a full GitHub PR URL instead');
|
|
464
|
+
}
|
|
465
|
+
if (config.repos.org)
|
|
466
|
+
return `${config.repos.org}/${configured}`;
|
|
467
|
+
throw new Error(`Unable to resolve repository ${configured} to owner/repo; pass a full GitHub PR URL instead`);
|
|
468
|
+
}
|
|
469
|
+
function standaloneBabysitClonePath(repo, config) {
|
|
470
|
+
const configured = Object.entries(config.clonePaths ?? {}).find(([candidate]) => candidate.toLowerCase() === repo.toLowerCase())?.[1]
|
|
471
|
+
?? Object.entries(config.repos.clonePaths ?? {}).find(([candidate]) => candidate.toLowerCase() === repo.toLowerCase())?.[1];
|
|
472
|
+
if (configured && existsSync(configured))
|
|
473
|
+
return configured;
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
310
476
|
/**
|
|
311
477
|
* Ensures the relayfile mount is running at each configured clone path so
|
|
312
478
|
* spawned agents can resolve `.integrations` relative to their working
|
|
@@ -354,6 +520,11 @@ function parseFactoryCommand(args) {
|
|
|
354
520
|
throw new Error('factory dispatch requires an issue key or path');
|
|
355
521
|
return { kind: 'factory-dispatch', issue: issueOrPr };
|
|
356
522
|
}
|
|
523
|
+
if (action === 'babysit') {
|
|
524
|
+
if (flags.length > 0)
|
|
525
|
+
throw new Error(`Unexpected argument ${flags[0]}`);
|
|
526
|
+
return { kind: 'factory-babysit', ...parseStandaloneBabysitTarget(issueOrPr) };
|
|
527
|
+
}
|
|
357
528
|
if (action === 'close-probe') {
|
|
358
529
|
const prNumber = Number(issueOrPr);
|
|
359
530
|
if (!Number.isInteger(prNumber) || prNumber <= 0)
|
|
@@ -416,15 +587,36 @@ function parseFactoryStartFlags(args) {
|
|
|
416
587
|
}
|
|
417
588
|
return { mode };
|
|
418
589
|
}
|
|
419
|
-
async function loadConfig(path) {
|
|
590
|
+
async function loadConfig(path, options = {}) {
|
|
420
591
|
const configPath = path ?? resolve(process.cwd(), 'factory.config.json');
|
|
421
592
|
const raw = JSON.parse(await readFile(configPath, 'utf8'));
|
|
422
593
|
const record = asRecord(raw);
|
|
423
594
|
return {
|
|
424
|
-
config:
|
|
595
|
+
config: await resolveLocalFactoryConfig(raw, {
|
|
596
|
+
...options,
|
|
597
|
+
// fixtureFiles describe a hermetic in-memory run; their checkout paths
|
|
598
|
+
// are intentionally synthetic and must not be preflighted on the host.
|
|
599
|
+
validateConfiguredCheckouts: options.validateConfiguredCheckouts && !record.fixtureFiles,
|
|
600
|
+
}),
|
|
425
601
|
fixtureFiles: record.fixtureFiles ? asRecord(record.fixtureFiles) : undefined,
|
|
426
602
|
};
|
|
427
603
|
}
|
|
604
|
+
function commandUsesLocalCheckout(command) {
|
|
605
|
+
switch (command.kind) {
|
|
606
|
+
case 'factory':
|
|
607
|
+
return command.action === 'run-once' || command.action === 'loop' || command.action === 'start';
|
|
608
|
+
case 'factory-canary':
|
|
609
|
+
case 'factory-triage':
|
|
610
|
+
case 'factory-dispatch':
|
|
611
|
+
case 'factory-babysit':
|
|
612
|
+
return true;
|
|
613
|
+
default:
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function hasInjectedFactoryRuntime(deps) {
|
|
618
|
+
return Boolean(deps.fleet || deps.mount || deps.createFactory || deps.createFleet || deps.cloudMountFromConfig);
|
|
619
|
+
}
|
|
428
620
|
async function buildFleet(globals, loaded, deps) {
|
|
429
621
|
if (deps.fleet)
|
|
430
622
|
return deps.fleet;
|
|
@@ -466,15 +658,10 @@ function streamLogger(stream) {
|
|
|
466
658
|
error: (message, ...args) => write(message, args),
|
|
467
659
|
};
|
|
468
660
|
}
|
|
469
|
-
function formatLogArgs(args) {
|
|
661
|
+
export function formatLogArgs(args) {
|
|
470
662
|
if (args.length === 0)
|
|
471
663
|
return '';
|
|
472
|
-
|
|
473
|
-
return ` ${args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ')}`;
|
|
474
|
-
}
|
|
475
|
-
catch {
|
|
476
|
-
return '';
|
|
477
|
-
}
|
|
664
|
+
return ` ${args.map((arg) => (typeof arg === 'string' ? arg : stringifyLogValue(arg))).join(' ')}`;
|
|
478
665
|
}
|
|
479
666
|
export function resolveBrokerConnectionPath(startCwd = process.cwd()) {
|
|
480
667
|
let current = resolve(startCwd);
|
|
@@ -503,15 +690,37 @@ async function resolveStatesForIssueSource(mount, config, resolveStates) {
|
|
|
503
690
|
return stateResolutionFromIds(config.stateIds, config.linear.states);
|
|
504
691
|
}
|
|
505
692
|
if (!config.issueSource) {
|
|
506
|
-
|
|
507
|
-
if (
|
|
693
|
+
autoDetectedIssueSources.add(config);
|
|
694
|
+
if (shouldAutoDetectGithubSource(config)) {
|
|
508
695
|
config.issueSource = 'github';
|
|
509
696
|
return stateResolutionFromIds(config.stateIds, config.linear.states);
|
|
510
697
|
}
|
|
511
|
-
|
|
698
|
+
const linearReady = await mount.ensureSubRoot('/linear/issues', { timeoutMs: 90_000 });
|
|
699
|
+
config.issueSource = linearReady === 'ready' ? 'linear' : 'github';
|
|
700
|
+
if (config.issueSource === 'github') {
|
|
701
|
+
return stateResolutionFromIds(config.stateIds, config.linear.states);
|
|
702
|
+
}
|
|
512
703
|
}
|
|
513
704
|
return (resolveStates ?? defaultResolveStates)(mount, config);
|
|
514
705
|
}
|
|
706
|
+
function shouldAutoDetectGithubSource(config) {
|
|
707
|
+
if (!config.repos.org || configuredGithubIssueRepos(config).length === 0)
|
|
708
|
+
return false;
|
|
709
|
+
return Object.keys(config.stateIds).length === 0 &&
|
|
710
|
+
hasDefaultLinearStateNames(config.linear.states) &&
|
|
711
|
+
Object.keys(config.linear.statesByTeam).length === 0 &&
|
|
712
|
+
Object.keys(config.linear.teamIds).length === 0 &&
|
|
713
|
+
config.subscription.teams.length === 0 &&
|
|
714
|
+
config.subscription.projects.length === 0 &&
|
|
715
|
+
config.subscription.assignees.length === 0;
|
|
716
|
+
}
|
|
717
|
+
function hasDefaultLinearStateNames(states) {
|
|
718
|
+
return states.readyForAgent === 'Ready for Agent' &&
|
|
719
|
+
states.agentImplementing === 'Agent Implementing' &&
|
|
720
|
+
states.inPlanning === 'In Planning' &&
|
|
721
|
+
states.done === 'Done' &&
|
|
722
|
+
states.humanReview === 'In Human Review';
|
|
723
|
+
}
|
|
515
724
|
async function buildMount(loaded, deps) {
|
|
516
725
|
if (deps.mount)
|
|
517
726
|
return deps.mount;
|
|
@@ -580,20 +789,33 @@ async function findIssuePath(mount, key, config) {
|
|
|
580
789
|
if (config.issueSource === 'github') {
|
|
581
790
|
const number = Number(key.replace(/^#/, ''));
|
|
582
791
|
if (!Number.isInteger(number) || number <= 0) {
|
|
583
|
-
throw new Error(
|
|
792
|
+
throw new Error(`${githubIssueResolutionError(config, key)}: expected a positive issue number`);
|
|
793
|
+
}
|
|
794
|
+
const configuredRepos = configuredGithubIssueRepos(config);
|
|
795
|
+
if (configuredRepos.length === 0 && hasConfiguredGithubIssueRoutes(config)) {
|
|
796
|
+
throw new Error(`${githubIssueResolutionError(config, key)}: configured repository routes do not resolve to owner/repo; ` +
|
|
797
|
+
'set repos.default to owner/repo or map its label in repos.byLabel');
|
|
798
|
+
}
|
|
799
|
+
if (configuredRepos.length === 1) {
|
|
800
|
+
const directPath = await findDirectGithubIssuePath(mount, configuredRepos[0], number);
|
|
801
|
+
if (directPath)
|
|
802
|
+
return directPath;
|
|
584
803
|
}
|
|
585
|
-
const
|
|
586
|
-
|
|
804
|
+
const roots = configuredRepos.length > 0
|
|
805
|
+
? configuredRepos.flatMap(githubIssueRoots)
|
|
806
|
+
: ['/github/repos'];
|
|
807
|
+
const matches = (await Promise.all([...new Set(roots)].map((root) => mount.listTree(root))))
|
|
808
|
+
.flat()
|
|
587
809
|
.filter((path) => path.endsWith('.json'))
|
|
588
810
|
.filter((path) => {
|
|
589
811
|
const parts = githubIssuePathParts(path);
|
|
590
812
|
if (!parts || parts.number !== number)
|
|
591
813
|
return false;
|
|
592
|
-
return
|
|
814
|
+
return configuredRepos.length === 0 || configuredRepos.some((repo) => repo.toLowerCase() === `${parts.owner}/${parts.repo}`.toLowerCase());
|
|
593
815
|
})
|
|
594
816
|
.sort((left, right) => githubIssuePathPreference(left) - githubIssuePathPreference(right) || left.localeCompare(right));
|
|
595
817
|
if (matches.length === 0) {
|
|
596
|
-
throw new Error(
|
|
818
|
+
throw new Error(`${githubIssueResolutionError(config, key)}: found 0 matches`);
|
|
597
819
|
}
|
|
598
820
|
const matchesByRepo = new Map();
|
|
599
821
|
for (const path of matches) {
|
|
@@ -603,9 +825,9 @@ async function findIssuePath(mount, key, config) {
|
|
|
603
825
|
matchesByRepo.set(repo.toLowerCase(), { repo, path });
|
|
604
826
|
}
|
|
605
827
|
}
|
|
606
|
-
if (!
|
|
828
|
+
if (!config.repos.default && matchesByRepo.size > 1) {
|
|
607
829
|
const repos = [...matchesByRepo.values()].map((match) => match.repo).sort((left, right) => left.localeCompare(right));
|
|
608
|
-
throw new Error(
|
|
830
|
+
throw new Error(`${githubIssueResolutionError(config, key)}: matches multiple repositories (${repos.join(', ')}); ` +
|
|
609
831
|
'set repos.default or pass a repo-qualified argument');
|
|
610
832
|
}
|
|
611
833
|
return matchesByRepo.values().next().value.path;
|
|
@@ -613,10 +835,96 @@ async function findIssuePath(mount, key, config) {
|
|
|
613
835
|
const matches = (await mount.listTree('/linear/issues/'))
|
|
614
836
|
.filter((path) => path.startsWith(`/linear/issues/${key}__`) || path === `/linear/issues/${key}.json`);
|
|
615
837
|
if (matches.length !== 1) {
|
|
616
|
-
|
|
838
|
+
const detected = autoDetectedIssueSources.has(config) ? ', auto-detected' : '';
|
|
839
|
+
throw new Error(`Unable to resolve issue ${key} in Linear (issueSource=linear${detected}): found ${matches.length} matches; ` +
|
|
840
|
+
`set issueSource: 'github' if these are GitHub issues`);
|
|
617
841
|
}
|
|
618
842
|
return matches[0];
|
|
619
843
|
}
|
|
844
|
+
function configuredGithubIssueRepos(config) {
|
|
845
|
+
const candidates = config.repos.default
|
|
846
|
+
? [config.repos.default]
|
|
847
|
+
: [
|
|
848
|
+
...Object.values(config.repos.byLabel),
|
|
849
|
+
...Object.values(config.repos.byProject),
|
|
850
|
+
...config.repos.keywordRules.map((rule) => rule.repo),
|
|
851
|
+
];
|
|
852
|
+
const repos = new Map();
|
|
853
|
+
const routedRepos = [
|
|
854
|
+
...Object.values(config.repos.byLabel),
|
|
855
|
+
...Object.values(config.repos.byProject),
|
|
856
|
+
...config.repos.keywordRules.map((rule) => rule.repo),
|
|
857
|
+
];
|
|
858
|
+
for (const candidate of candidates) {
|
|
859
|
+
const mapped = Object.entries(config.repos.byLabel).find(([label]) => label.toLowerCase() === candidate.toLowerCase())?.[1];
|
|
860
|
+
const canonicalRoute = routedRepos.find((route) => route.includes('/') && route.toLowerCase() === candidate.toLowerCase());
|
|
861
|
+
const repo = candidate.includes('/')
|
|
862
|
+
? canonicalRoute ?? candidate
|
|
863
|
+
: mapped?.includes('/')
|
|
864
|
+
? mapped
|
|
865
|
+
: config.repos.org ? `${config.repos.org}/${mapped ?? candidate}` : undefined;
|
|
866
|
+
if (!repo || !/^[^/]+\/[^/]+$/u.test(repo))
|
|
867
|
+
continue;
|
|
868
|
+
repos.set(repo.toLowerCase(), repo);
|
|
869
|
+
}
|
|
870
|
+
return [...repos.values()];
|
|
871
|
+
}
|
|
872
|
+
function hasConfiguredGithubIssueRoutes(config) {
|
|
873
|
+
return Boolean(config.repos.default ||
|
|
874
|
+
Object.keys(config.repos.byLabel).length > 0 ||
|
|
875
|
+
Object.keys(config.repos.byProject).length > 0 ||
|
|
876
|
+
config.repos.keywordRules.length > 0);
|
|
877
|
+
}
|
|
878
|
+
async function findDirectGithubIssuePath(mount, repo, number) {
|
|
879
|
+
for (const path of githubIssueDirectPaths(repo, number)) {
|
|
880
|
+
try {
|
|
881
|
+
await mount.readFile(path);
|
|
882
|
+
return path;
|
|
883
|
+
}
|
|
884
|
+
catch (error) {
|
|
885
|
+
if (!isMountFileNotFound(error)) {
|
|
886
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
887
|
+
throw new Error(`Unable to read GitHub issue candidate ${path}: ${message}`, { cause: error });
|
|
888
|
+
}
|
|
889
|
+
// Direct reads are an optimization. A scoped tree scan below handles
|
|
890
|
+
// slugged directories and older mount shapes when these paths are absent.
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return undefined;
|
|
894
|
+
}
|
|
895
|
+
function isMountFileNotFound(error) {
|
|
896
|
+
const record = asRecord(error);
|
|
897
|
+
const response = asRecord(record.response);
|
|
898
|
+
const status = record.status ?? record.statusCode ?? response.status ?? response.statusCode;
|
|
899
|
+
const code = typeof record.code === 'string' ? record.code.toLowerCase() : undefined;
|
|
900
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
901
|
+
return status === 404 || status === '404' ||
|
|
902
|
+
code === 'not_found' || code === 'file_not_found' ||
|
|
903
|
+
/(?:file\s+not\s+found|\b404\b)/iu.test(message);
|
|
904
|
+
}
|
|
905
|
+
function githubIssueDirectPaths(repo, number) {
|
|
906
|
+
const [owner, name] = repo.split('/');
|
|
907
|
+
const roots = [
|
|
908
|
+
`/github/repos/${owner}__${name}/issues`,
|
|
909
|
+
`/github/repos/${owner}/${name}/issues`,
|
|
910
|
+
];
|
|
911
|
+
return [
|
|
912
|
+
...roots.map((root) => `${root}/${number}/meta.json`),
|
|
913
|
+
...roots.map((root) => `${root}/by-id/${number}.json`),
|
|
914
|
+
...roots.map((root) => `${root}/${number}.json`),
|
|
915
|
+
];
|
|
916
|
+
}
|
|
917
|
+
function githubIssueRoots(repo) {
|
|
918
|
+
const [owner, name] = repo.split('/');
|
|
919
|
+
return [
|
|
920
|
+
`/github/repos/${owner}__${name}/issues`,
|
|
921
|
+
`/github/repos/${owner}/${name}/issues`,
|
|
922
|
+
];
|
|
923
|
+
}
|
|
924
|
+
function githubIssueResolutionError(config, key) {
|
|
925
|
+
const detected = autoDetectedIssueSources.has(config) ? ', auto-detected' : '';
|
|
926
|
+
return `Unable to resolve GitHub issue ${key} (issueSource=github${detected})`;
|
|
927
|
+
}
|
|
620
928
|
const githubIssuePathPreference = (path) => path.endsWith('/meta.json') ? 0 : path.includes('/by-id/') ? 1 : 2;
|
|
621
929
|
function parseFlags(args) {
|
|
622
930
|
const flags = {};
|
|
@@ -666,6 +974,7 @@ function isFactoryAction(value) {
|
|
|
666
974
|
value === 'canary' ||
|
|
667
975
|
value === 'triage' ||
|
|
668
976
|
value === 'dispatch' ||
|
|
977
|
+
value === 'babysit' ||
|
|
669
978
|
value === 'close-probe';
|
|
670
979
|
}
|
|
671
980
|
function defaultAgentName(capability, now) {
|
|
@@ -730,6 +1039,7 @@ Commands:
|
|
|
730
1039
|
canary <KEY|path> Check that a known issue is dispatch-ready
|
|
731
1040
|
triage <KEY|path> Triage one issue and print the decision
|
|
732
1041
|
dispatch <KEY|path> Triage and dispatch one issue
|
|
1042
|
+
babysit <PR|URL> Shepherd an existing open PR to green
|
|
733
1043
|
close-probe <PR> Probe/close a PR for an issue
|
|
734
1044
|
fleet <command> Low-level fleet commands: spawn, roster, release
|
|
735
1045
|
|