@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.
Files changed (77) hide show
  1. package/README.md +9 -0
  2. package/dist/cli/diagnose.d.ts +92 -0
  3. package/dist/cli/diagnose.d.ts.map +1 -0
  4. package/dist/cli/diagnose.js +346 -0
  5. package/dist/cli/diagnose.js.map +1 -0
  6. package/dist/cli/fleet.d.ts +13 -1
  7. package/dist/cli/fleet.d.ts.map +1 -1
  8. package/dist/cli/fleet.js +118 -11
  9. package/dist/cli/fleet.js.map +1 -1
  10. package/dist/config/schema.d.ts +69 -2
  11. package/dist/config/schema.d.ts.map +1 -1
  12. package/dist/config/schema.js +32 -0
  13. package/dist/config/schema.js.map +1 -1
  14. package/dist/fleet/control-plane-circuit.d.ts.map +1 -1
  15. package/dist/fleet/control-plane-circuit.js +13 -1
  16. package/dist/fleet/control-plane-circuit.js.map +1 -1
  17. package/dist/fleet/internal-fleet-client.d.ts +16 -0
  18. package/dist/fleet/internal-fleet-client.d.ts.map +1 -1
  19. package/dist/fleet/internal-fleet-client.js +228 -13
  20. package/dist/fleet/internal-fleet-client.js.map +1 -1
  21. package/dist/hosted/orchestrator.d.ts.map +1 -1
  22. package/dist/hosted/orchestrator.js +1 -4
  23. package/dist/hosted/orchestrator.js.map +1 -1
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/mount/relayfile-cloud-mount-client.d.ts +23 -1
  29. package/dist/mount/relayfile-cloud-mount-client.d.ts.map +1 -1
  30. package/dist/mount/relayfile-cloud-mount-client.js +38 -9
  31. package/dist/mount/relayfile-cloud-mount-client.js.map +1 -1
  32. package/dist/observability/cloud-reporter.d.ts +1 -0
  33. package/dist/observability/cloud-reporter.d.ts.map +1 -1
  34. package/dist/observability/cloud-reporter.js +182 -50
  35. package/dist/observability/cloud-reporter.js.map +1 -1
  36. package/dist/observability/error-class.d.ts +29 -0
  37. package/dist/observability/error-class.d.ts.map +1 -0
  38. package/dist/observability/error-class.js +36 -0
  39. package/dist/observability/error-class.js.map +1 -0
  40. package/dist/observability/index.d.ts +1 -0
  41. package/dist/observability/index.d.ts.map +1 -1
  42. package/dist/observability/index.js +1 -0
  43. package/dist/observability/index.js.map +1 -1
  44. package/dist/orchestrator/factory.d.ts.map +1 -1
  45. package/dist/orchestrator/factory.js +1461 -84
  46. package/dist/orchestrator/factory.js.map +1 -1
  47. package/dist/orchestrator/index.d.ts +1 -0
  48. package/dist/orchestrator/index.d.ts.map +1 -1
  49. package/dist/orchestrator/index.js +1 -0
  50. package/dist/orchestrator/index.js.map +1 -1
  51. package/dist/orchestrator/public-health.d.ts +82 -0
  52. package/dist/orchestrator/public-health.d.ts.map +1 -0
  53. package/dist/orchestrator/public-health.js +372 -0
  54. package/dist/orchestrator/public-health.js.map +1 -0
  55. package/dist/ports/state.d.ts +76 -3
  56. package/dist/ports/state.d.ts.map +1 -1
  57. package/dist/state/document-store.d.ts +2 -1
  58. package/dist/state/document-store.d.ts.map +1 -1
  59. package/dist/state/document-store.js.map +1 -1
  60. package/dist/state/file-state-store.d.ts +24 -5
  61. package/dist/state/file-state-store.d.ts.map +1 -1
  62. package/dist/state/file-state-store.js +136 -10
  63. package/dist/state/file-state-store.js.map +1 -1
  64. package/dist/state/in-memory-state-store.d.ts +21 -2
  65. package/dist/state/in-memory-state-store.d.ts.map +1 -1
  66. package/dist/state/in-memory-state-store.js +99 -6
  67. package/dist/state/in-memory-state-store.js.map +1 -1
  68. package/dist/state/watch-state-document.d.ts.map +1 -1
  69. package/dist/state/watch-state-document.js +45 -3
  70. package/dist/state/watch-state-document.js.map +1 -1
  71. package/dist/trajectory.d.ts +25 -0
  72. package/dist/trajectory.d.ts.map +1 -0
  73. package/dist/trajectory.js +51 -0
  74. package/dist/trajectory.js.map +1 -0
  75. package/dist/types.d.ts +106 -1
  76. package/dist/types.d.ts.map +1 -1
  77. 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
- if (hasInjectedFactoryRuntime(input.deps) && !input.deps.cloudSessionProvider)
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 ?? resolveFactoryWorkspace)();
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
- const cloudFetch = async (_request, init) => session.client.fetch('/api/v1/factory/events', init);
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: session.auth.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: async () => session.client.snapshot().accessToken,
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