@agent-relay/factory 0.1.63 → 0.1.65
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 +9 -0
- package/dist/cli/diagnose.d.ts +92 -0
- package/dist/cli/diagnose.d.ts.map +1 -0
- package/dist/cli/diagnose.js +346 -0
- package/dist/cli/diagnose.js.map +1 -0
- package/dist/cli/fleet.d.ts +13 -1
- package/dist/cli/fleet.d.ts.map +1 -1
- package/dist/cli/fleet.js +118 -11
- package/dist/cli/fleet.js.map +1 -1
- package/dist/config/schema.d.ts +69 -2
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +32 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet/control-plane-circuit.d.ts.map +1 -1
- package/dist/fleet/control-plane-circuit.js +13 -1
- package/dist/fleet/control-plane-circuit.js.map +1 -1
- package/dist/fleet/internal-fleet-client.d.ts +16 -0
- package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
- package/dist/fleet/internal-fleet-client.js +228 -13
- package/dist/fleet/internal-fleet-client.js.map +1 -1
- package/dist/hosted/orchestrator.d.ts.map +1 -1
- package/dist/hosted/orchestrator.js +1 -4
- package/dist/hosted/orchestrator.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts +23 -1
- package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
- package/dist/mount/relayfile-cloud-mount-client.js +38 -9
- package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
- package/dist/observability/cloud-reporter.d.ts +1 -0
- package/dist/observability/cloud-reporter.d.ts.map +1 -1
- package/dist/observability/cloud-reporter.js +182 -50
- package/dist/observability/cloud-reporter.js.map +1 -1
- package/dist/observability/error-class.d.ts +29 -0
- package/dist/observability/error-class.d.ts.map +1 -0
- package/dist/observability/error-class.js +36 -0
- package/dist/observability/error-class.js.map +1 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +1 -0
- package/dist/observability/index.js.map +1 -1
- package/dist/orchestrator/factory.d.ts.map +1 -1
- package/dist/orchestrator/factory.js +1461 -84
- package/dist/orchestrator/factory.js.map +1 -1
- package/dist/orchestrator/index.d.ts +1 -0
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +1 -0
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/public-health.d.ts +82 -0
- package/dist/orchestrator/public-health.d.ts.map +1 -0
- package/dist/orchestrator/public-health.js +372 -0
- package/dist/orchestrator/public-health.js.map +1 -0
- package/dist/ports/state.d.ts +76 -3
- package/dist/ports/state.d.ts.map +1 -1
- package/dist/state/document-store.d.ts +2 -1
- package/dist/state/document-store.d.ts.map +1 -1
- package/dist/state/document-store.js.map +1 -1
- package/dist/state/file-state-store.d.ts +24 -5
- package/dist/state/file-state-store.d.ts.map +1 -1
- package/dist/state/file-state-store.js +136 -10
- package/dist/state/file-state-store.js.map +1 -1
- package/dist/state/in-memory-state-store.d.ts +21 -2
- package/dist/state/in-memory-state-store.d.ts.map +1 -1
- package/dist/state/in-memory-state-store.js +99 -6
- package/dist/state/in-memory-state-store.js.map +1 -1
- package/dist/state/watch-state-document.d.ts.map +1 -1
- package/dist/state/watch-state-document.js +45 -3
- package/dist/state/watch-state-document.js.map +1 -1
- package/dist/trajectory.d.ts +25 -0
- package/dist/trajectory.d.ts.map +1 -0
- package/dist/trajectory.js +51 -0
- package/dist/trajectory.js.map +1 -0
- package/dist/types.d.ts +106 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/fleet.js
CHANGED
|
@@ -7,6 +7,7 @@ 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 { diagnoseDeployedFactory, renderDeployedDiagnosis } from './diagnose.js';
|
|
10
11
|
import { FACTORY_EXIT, exitCodeForDispatchResult, exitCodeForError, exitCodeForIterationReport, exitCodeForLoopReports, exitCodeForRelayDispatch, } from './exit-codes.js';
|
|
11
12
|
import { FileStateStore, RelayfileCloudMountClient, checkFactoryLoopLiveness, closeProbePr, createFactory, createFleet, createFactoryCloudEventV1, ensureRelayBroker, FactoryCloudReporter, FileFactoryCloudEventOutbox, defaultGhRunner, explicitLinkedIssueKey, githubIssuePathParts, githubWatchStatePath, isAllowedFactoryGithubDraft, isAllowedFactoryGithubArtifactDraft, isInFactoryScope, parseGithubFactoryIssue, publishFactoryMountHealth, heldAgentsFromRegistry, parseOwnedBrokerAgentExitTimeoutMs, parseStandaloneBabysitTarget, readStandalonePullRequest, readLinearIssueWithCanonicalFallback, reapFactoryOrphansOnce, reapFactoryEnvironmentsOnce, readFactoryInFlightRegistry, readFactoryLoopHeartbeat, resolveFactoryStates, stateResolutionFromIds, standaloneBabysitterAgentName, renderAgentTask, resolveFactoryWorkspace, } from '../index.js';
|
|
12
13
|
import { resolveTestGuidance } from '../dispatch/test-guidance.js';
|
|
@@ -17,6 +18,7 @@ import { loadOrCreateFactoryInstanceId, resolveFactoryInstanceName } from '../ob
|
|
|
17
18
|
import { ensureFactoryIntegrations, inspectFactoryIntegration, openIntegrationUrl, } from '../mount/relayfile-integration-preflight.js';
|
|
18
19
|
import { checkMountStaleness } from '../mount/relayfile-binary.js';
|
|
19
20
|
import { MountAuthScopeError } from '../mount/mount-auth-error.js';
|
|
21
|
+
import { createHostedCloudAccessTokenProvider, hostedCloudAccessTokenUrl, resolveHostedCloudApiUrl, } from '../mount/relayfile-cloud-mount-client.js';
|
|
20
22
|
import { resolveRelayWorkspaceKey } from '../fleet/relay-workspace-key.js';
|
|
21
23
|
import { isMeaningfullyBehind, readFactoryVersionInfo, } from '../version-info.js';
|
|
22
24
|
import { GhCliIssuePublisher, NotionApiFactoryTasksClient, RelayChannelNotionClaimStore, RelayChannelNotionContractPublisher, generateFactoryTasksManifest, loadNotionIntakeManifest, runNotionIntake, } from '../intake/index.js';
|
|
@@ -39,7 +41,7 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
39
41
|
return 0;
|
|
40
42
|
}
|
|
41
43
|
const { globals, args } = parseGlobalOptions(argv);
|
|
42
|
-
const command = parseFleetCommand(args);
|
|
44
|
+
const command = parseFleetCommand(args, deps.env ?? process.env);
|
|
43
45
|
if (command.kind === 'featuremap-check') {
|
|
44
46
|
const report = await (deps.featureMapCheck ?? checkFeatureMap)({
|
|
45
47
|
...(command.manifestPath ? { manifestPath: command.manifestPath } : {}),
|
|
@@ -48,6 +50,26 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
48
50
|
writeJson(out, report);
|
|
49
51
|
return 0;
|
|
50
52
|
}
|
|
53
|
+
if (command.kind === 'factory-diagnose') {
|
|
54
|
+
// Deliberately ahead of every config/mount/fleet setup below: the whole
|
|
55
|
+
// point of #295 is that this runs from anywhere — a laptop, a lane, a
|
|
56
|
+
// runbook — against an instance this checkout knows nothing about.
|
|
57
|
+
const diagnosis = await diagnoseDeployedFactory({
|
|
58
|
+
url: command.url,
|
|
59
|
+
...(command.token ? { token: command.token } : {}),
|
|
60
|
+
...(command.timeoutMs !== undefined ? { timeoutMs: command.timeoutMs } : {}),
|
|
61
|
+
...(deps.diagnoseFetch ? { fetch: deps.diagnoseFetch } : {}),
|
|
62
|
+
});
|
|
63
|
+
if (command.json) {
|
|
64
|
+
writeJson(out, diagnosis);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
out.write(renderDeployedDiagnosis(diagnosis));
|
|
68
|
+
}
|
|
69
|
+
// Same contract as `factory canary`: a lane that only reads $? must be
|
|
70
|
+
// able to tell "production is dispatching" from "it is not".
|
|
71
|
+
return diagnosis.dispatching ? FACTORY_EXIT.OK : FACTORY_EXIT.FAILED;
|
|
72
|
+
}
|
|
51
73
|
if (command.kind === 'factory-init') {
|
|
52
74
|
await initializeFactory({ repo: command.repo, workspaceId: command.workspaceId, stdout: out, stderr: err });
|
|
53
75
|
return 0;
|
|
@@ -395,13 +417,13 @@ export async function runFleetCli(argv, deps = {}) {
|
|
|
395
417
|
}
|
|
396
418
|
}
|
|
397
419
|
}
|
|
398
|
-
export function parseFleetCommand(args) {
|
|
420
|
+
export function parseFleetCommand(args, env = process.env) {
|
|
399
421
|
const [verb, ...rest] = args;
|
|
400
422
|
if (!verb) {
|
|
401
423
|
throw new Error(usage());
|
|
402
424
|
}
|
|
403
425
|
if (isFactoryAction(verb)) {
|
|
404
|
-
return parseFactoryCommand(args);
|
|
426
|
+
return parseFactoryCommand(args, env);
|
|
405
427
|
}
|
|
406
428
|
if (verb === 'fleet') {
|
|
407
429
|
return parseFleetSubcommand(rest);
|
|
@@ -1056,7 +1078,7 @@ function resolveLocalMountFn(deps, mount) {
|
|
|
1056
1078
|
throw new Error('Mount client does not provide Relayfile SDK local-mount support');
|
|
1057
1079
|
};
|
|
1058
1080
|
}
|
|
1059
|
-
function parseFactoryCommand(args) {
|
|
1081
|
+
function parseFactoryCommand(args, env = process.env) {
|
|
1060
1082
|
const [action, issueOrPr, ...flags] = args;
|
|
1061
1083
|
if (action === 'init') {
|
|
1062
1084
|
const values = [issueOrPr, ...flags].filter((value) => Boolean(value));
|
|
@@ -1076,6 +1098,9 @@ function parseFactoryCommand(args) {
|
|
|
1076
1098
|
}
|
|
1077
1099
|
return { kind: 'factory-init', repo, workspaceId };
|
|
1078
1100
|
}
|
|
1101
|
+
if (action === 'diagnose') {
|
|
1102
|
+
return parseFactoryDiagnoseFlags([issueOrPr, ...flags].filter((value) => Boolean(value)), env);
|
|
1103
|
+
}
|
|
1079
1104
|
if (action === 'start') {
|
|
1080
1105
|
return { kind: 'factory', action, ...parseFactoryStartFlags([issueOrPr, ...flags]) };
|
|
1081
1106
|
}
|
|
@@ -1159,6 +1184,61 @@ function evaluateFactoryCanary(report, issueArg) {
|
|
|
1159
1184
|
reason: 'issue pulled but neither dispatched, triaged, nor skipped',
|
|
1160
1185
|
};
|
|
1161
1186
|
}
|
|
1187
|
+
function parseFactoryDiagnoseFlags(flags, env) {
|
|
1188
|
+
let url;
|
|
1189
|
+
// The injected environment, not the process one: an embedder or a hermetic
|
|
1190
|
+
// test that supplies `deps.env` must not silently skip the authenticated
|
|
1191
|
+
// read (#300 review, P2, cubic).
|
|
1192
|
+
let token = env.FACTORY_EVIDENCE_TOKEN?.trim() || undefined;
|
|
1193
|
+
let json = false;
|
|
1194
|
+
let timeoutMs;
|
|
1195
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
1196
|
+
const flag = flags[index];
|
|
1197
|
+
if (flag === '--deployed' || flag === '--url') {
|
|
1198
|
+
url = requireValue(flags, ++index, flag);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (flag === '--token') {
|
|
1202
|
+
token = requireValue(flags, ++index, '--token');
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (flag === '--timeout-ms') {
|
|
1206
|
+
const value = Number(requireValue(flags, ++index, '--timeout-ms'));
|
|
1207
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
1208
|
+
throw new Error('--timeout-ms requires a positive integer');
|
|
1209
|
+
timeoutMs = value;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (flag === '--json') {
|
|
1213
|
+
json = true;
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
// A bare URL is the same request spelled shorter.
|
|
1217
|
+
if (!flag.startsWith('-') && !url) {
|
|
1218
|
+
url = flag;
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
// Never echo the value: `factory diagnose <url> <token>` is a plausible
|
|
1222
|
+
// slip, and this message reaches stderr and from there CI logs (#300
|
|
1223
|
+
// review, CodeRabbit). The position is enough to find it.
|
|
1224
|
+
if (!flag.startsWith('-')) {
|
|
1225
|
+
throw new Error(`factory diagnose accepts one url; argument ${index + 1} is a second positional value. ` +
|
|
1226
|
+
'Pass a token with --token, not as a bare argument.');
|
|
1227
|
+
}
|
|
1228
|
+
throw new Error(`Unknown factory diagnose option at argument ${index + 1}`);
|
|
1229
|
+
}
|
|
1230
|
+
if (!url) {
|
|
1231
|
+
throw new Error('factory diagnose requires --deployed <url> (the deployed instance base URL)');
|
|
1232
|
+
}
|
|
1233
|
+
if (!/^https?:\/\//u.test(url)) {
|
|
1234
|
+
// Not echoed, for the same reason as the positional case above: the value
|
|
1235
|
+
// in the url slot is exactly where a mistyped `--token` argument lands
|
|
1236
|
+
// (#300 review, cubic).
|
|
1237
|
+
throw new Error('factory diagnose --deployed requires an http(s) url; the value given does not start with ' +
|
|
1238
|
+
'http:// or https://. If that value is a token, pass it with --token.');
|
|
1239
|
+
}
|
|
1240
|
+
return { kind: 'factory-diagnose', url, json, ...(token ? { token } : {}), ...(timeoutMs !== undefined ? { timeoutMs } : {}) };
|
|
1241
|
+
}
|
|
1162
1242
|
function parseFactoryStartFlags(args) {
|
|
1163
1243
|
let mode = 'live';
|
|
1164
1244
|
const flags = args.filter((arg) => Boolean(arg));
|
|
@@ -1208,10 +1288,18 @@ function factoryReportingMode(command) {
|
|
|
1208
1288
|
async function buildFactoryCloudReporter(input) {
|
|
1209
1289
|
if (!input.config.reporting.enabled)
|
|
1210
1290
|
return undefined;
|
|
1211
|
-
|
|
1291
|
+
const runtimeEnv = input.deps.env ?? process.env;
|
|
1292
|
+
// Hosted mode is a usable endpoint, not a defined-but-blank variable: an
|
|
1293
|
+
// empty value must fall through to the local-session path instead of
|
|
1294
|
+
// reaching createHostedCloudAccessTokenProvider, which throws on `new URL('')`.
|
|
1295
|
+
const hostedAccessTokenUrl = hostedCloudAccessTokenUrl(runtimeEnv);
|
|
1296
|
+
if (hasInjectedFactoryRuntime(input.deps)
|
|
1297
|
+
&& !input.deps.cloudSessionProvider
|
|
1298
|
+
&& !hostedAccessTokenUrl)
|
|
1212
1299
|
return undefined;
|
|
1213
1300
|
try {
|
|
1214
|
-
const activeWorkspace = await (input.deps.resolveWorkspace
|
|
1301
|
+
const activeWorkspace = await (input.deps.resolveWorkspace
|
|
1302
|
+
?? (() => resolveFactoryWorkspace(undefined, runtimeEnv)))();
|
|
1215
1303
|
const activeWorkspaceIds = new Set([
|
|
1216
1304
|
activeWorkspace.workspaceId,
|
|
1217
1305
|
activeWorkspace.cloudWorkspaceId,
|
|
@@ -1220,14 +1308,29 @@ async function buildFactoryCloudReporter(input) {
|
|
|
1220
1308
|
input.logger.warn?.('[factory] Cloud progress reporting skipped because the active account workspace differs from Factory config');
|
|
1221
1309
|
return undefined;
|
|
1222
1310
|
}
|
|
1223
|
-
const session = await (input.deps.cloudSessionProvider ?? ensureCloudSession)({ interactive: false });
|
|
1224
1311
|
const outboxPath = input.config.reporting.outboxPath
|
|
1225
1312
|
?? join(dirname(input.config.loop.registryPath), 'factory-cloud-events.json');
|
|
1226
1313
|
const instanceId = await loadOrCreateFactoryInstanceId(`${outboxPath}.instance-id`);
|
|
1227
1314
|
const instanceName = resolveFactoryInstanceName(input.config);
|
|
1228
|
-
|
|
1315
|
+
let apiUrl;
|
|
1316
|
+
let getAccessToken;
|
|
1317
|
+
let cloudFetch;
|
|
1318
|
+
if (hostedAccessTokenUrl) {
|
|
1319
|
+
apiUrl = resolveHostedCloudApiUrl(runtimeEnv);
|
|
1320
|
+
getAccessToken = createHostedCloudAccessTokenProvider({
|
|
1321
|
+
url: hostedAccessTokenUrl,
|
|
1322
|
+
fetchImpl: input.deps.cloudAccessTokenFetch ?? fetch,
|
|
1323
|
+
});
|
|
1324
|
+
cloudFetch = input.deps.cloudReporterFetch;
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
const session = await (input.deps.cloudSessionProvider ?? ensureCloudSession)({ interactive: false });
|
|
1328
|
+
apiUrl = session.auth.apiUrl;
|
|
1329
|
+
getAccessToken = async () => session.client.snapshot().accessToken;
|
|
1330
|
+
cloudFetch = async (_request, init) => session.client.fetch('/api/v1/factory/events', init);
|
|
1331
|
+
}
|
|
1229
1332
|
return new FactoryCloudReporter({
|
|
1230
|
-
apiUrl
|
|
1333
|
+
apiUrl,
|
|
1231
1334
|
instance: {
|
|
1232
1335
|
id: instanceId,
|
|
1233
1336
|
bootId: randomUUID(),
|
|
@@ -1240,8 +1343,8 @@ async function buildFactoryCloudReporter(input) {
|
|
|
1240
1343
|
},
|
|
1241
1344
|
},
|
|
1242
1345
|
outbox: new FileFactoryCloudEventOutbox({ path: outboxPath }),
|
|
1243
|
-
getAccessToken
|
|
1244
|
-
fetch: cloudFetch,
|
|
1346
|
+
getAccessToken,
|
|
1347
|
+
...(cloudFetch ? { fetch: cloudFetch } : {}),
|
|
1245
1348
|
logger: input.logger,
|
|
1246
1349
|
batchSize: input.config.reporting.batchSize,
|
|
1247
1350
|
requestTimeoutMs: input.config.reporting.requestTimeoutMs,
|
|
@@ -1969,6 +2072,7 @@ function isCapability(value) {
|
|
|
1969
2072
|
}
|
|
1970
2073
|
function isFactoryAction(value) {
|
|
1971
2074
|
return value === 'init' ||
|
|
2075
|
+
value === 'diagnose' ||
|
|
1972
2076
|
value === 'start' ||
|
|
1973
2077
|
value === 'run-once' ||
|
|
1974
2078
|
value === 'loop' ||
|
|
@@ -2040,6 +2144,9 @@ Commands:
|
|
|
2040
2144
|
status Print current factory status as JSON
|
|
2041
2145
|
loop Run the bounded loop configured in factory.config.json
|
|
2042
2146
|
loop-status Print heartbeat/liveness status for the loop
|
|
2147
|
+
diagnose --deployed <url>
|
|
2148
|
+
Ask a DEPLOYED instance why it is or is not dispatching
|
|
2149
|
+
(--token/FACTORY_EVIDENCE_TOKEN also reads /evidence)
|
|
2043
2150
|
kill-loop Send SIGTERM to the heartbeat pid
|
|
2044
2151
|
reap-orphans [--include-held]
|
|
2045
2152
|
Report stale processes and held agents; opt in to release held agents past deadline
|