@phnx-labs/agents-cli 1.20.49 → 1.20.51

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 (100) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +3 -0
  3. package/dist/commands/browser-picker.js +1 -18
  4. package/dist/commands/cloud.js +1 -25
  5. package/dist/commands/computer.d.ts +1 -0
  6. package/dist/commands/computer.js +129 -8
  7. package/dist/commands/doctor.js +133 -2
  8. package/dist/commands/exec.js +49 -6
  9. package/dist/commands/factory.js +1 -4
  10. package/dist/commands/inspect.js +1 -11
  11. package/dist/commands/mcp.js +2 -6
  12. package/dist/commands/message.js +1 -4
  13. package/dist/commands/profiles.js +1 -18
  14. package/dist/commands/repo.js +33 -14
  15. package/dist/commands/resource-view.d.ts +1 -0
  16. package/dist/commands/resource-view.js +5 -17
  17. package/dist/commands/secrets.d.ts +1 -0
  18. package/dist/commands/secrets.js +1 -28
  19. package/dist/commands/sessions-picker.js +1 -18
  20. package/dist/commands/sessions.js +6 -8
  21. package/dist/commands/teams-picker.js +1 -32
  22. package/dist/commands/teams.js +218 -97
  23. package/dist/commands/tmux.js +1 -3
  24. package/dist/commands/view.js +1 -9
  25. package/dist/commands/worktree.js +1 -4
  26. package/dist/lib/agents.d.ts +0 -4
  27. package/dist/lib/agents.js +20 -33
  28. package/dist/lib/auto-dispatch-linear.d.ts +18 -0
  29. package/dist/lib/auto-dispatch-linear.js +107 -0
  30. package/dist/lib/auto-dispatch-provider.d.ts +10 -0
  31. package/dist/lib/auto-dispatch-provider.js +25 -0
  32. package/dist/lib/auto-dispatch.d.ts +87 -0
  33. package/dist/lib/auto-dispatch.js +142 -0
  34. package/dist/lib/browser/cdp.js +11 -2
  35. package/dist/lib/browser/drivers/ssh.d.ts +28 -10
  36. package/dist/lib/browser/drivers/ssh.js +57 -18
  37. package/dist/lib/browser/refs.js +1 -5
  38. package/dist/lib/cli-resources.d.ts +0 -2
  39. package/dist/lib/cli-resources.js +30 -13
  40. package/dist/lib/cloud/rush.d.ts +0 -24
  41. package/dist/lib/cloud/rush.js +0 -31
  42. package/dist/lib/crabbox/cli.js +4 -1
  43. package/dist/lib/crabbox/lease.js +29 -1
  44. package/dist/lib/daemon.js +41 -0
  45. package/dist/lib/exec.js +43 -18
  46. package/dist/lib/format.d.ts +38 -0
  47. package/dist/lib/format.js +108 -0
  48. package/dist/lib/git.d.ts +21 -0
  49. package/dist/lib/git.js +92 -0
  50. package/dist/lib/hooks/cache.d.ts +9 -2
  51. package/dist/lib/hooks/cache.js +220 -8
  52. package/dist/lib/hooks.js +17 -8
  53. package/dist/lib/hosts/passthrough.js +30 -1
  54. package/dist/lib/hosts/progress.d.ts +31 -0
  55. package/dist/lib/hosts/progress.js +35 -0
  56. package/dist/lib/hosts/remote-cmd.d.ts +1 -1
  57. package/dist/lib/hosts/remote-cmd.js +13 -3
  58. package/dist/lib/platform/exec.d.ts +4 -1
  59. package/dist/lib/platform/exec.js +8 -2
  60. package/dist/lib/resources.d.ts +0 -8
  61. package/dist/lib/resources.js +0 -10
  62. package/dist/lib/runner.js +10 -2
  63. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  64. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  65. package/dist/lib/session/active.d.ts +11 -1
  66. package/dist/lib/session/active.js +3 -0
  67. package/dist/lib/session/db.d.ts +1 -4
  68. package/dist/lib/session/db.js +20 -25
  69. package/dist/lib/session/discover.d.ts +2 -2
  70. package/dist/lib/session/discover.js +61 -48
  71. package/dist/lib/session/parse.js +35 -34
  72. package/dist/lib/session/render.d.ts +7 -3
  73. package/dist/lib/session/render.js +15 -9
  74. package/dist/lib/session/state.d.ts +55 -0
  75. package/dist/lib/session/state.js +87 -10
  76. package/dist/lib/session/types.d.ts +9 -0
  77. package/dist/lib/shims.d.ts +25 -2
  78. package/dist/lib/shims.js +73 -8
  79. package/dist/lib/ssh-tunnel.d.ts +33 -2
  80. package/dist/lib/ssh-tunnel.js +94 -7
  81. package/dist/lib/staleness/types.d.ts +0 -1
  82. package/dist/lib/teams/agents.d.ts +108 -1
  83. package/dist/lib/teams/agents.js +511 -11
  84. package/dist/lib/teams/api.d.ts +7 -1
  85. package/dist/lib/teams/api.js +5 -2
  86. package/dist/lib/teams/registry.d.ts +17 -0
  87. package/dist/lib/teams/registry.js +2 -0
  88. package/dist/lib/teams/remoteWorktree.d.ts +57 -0
  89. package/dist/lib/teams/remoteWorktree.js +213 -0
  90. package/dist/lib/teams/scheduler.d.ts +29 -0
  91. package/dist/lib/teams/scheduler.js +78 -0
  92. package/dist/lib/teams/supervisor.js +7 -0
  93. package/dist/lib/types.d.ts +14 -1
  94. package/dist/lib/versions.d.ts +7 -26
  95. package/dist/lib/versions.js +44 -146
  96. package/dist/lib/warn-unpushed.d.ts +40 -0
  97. package/dist/lib/warn-unpushed.js +128 -0
  98. package/package.json +3 -1
  99. package/dist/lib/resources/index.d.ts +0 -53
  100. package/dist/lib/resources/index.js +0 -76
@@ -21,6 +21,7 @@ import { latestFileMtimeMs } from './fs-walk.js';
21
21
  import { damerauLevenshtein } from './fuzzy.js';
22
22
  import { getCacheDir, getVersionsDir, getShimsDir, getCliVersionCachePath } from './state.js';
23
23
  import { resolveVersion, getVersionHomePath, getBinaryPath } from './versions.js';
24
+ import { supports } from './capabilities.js';
24
25
  const execFileAsync = promisify(execFile);
25
26
  const HOME = os.homedir();
26
27
  /**
@@ -212,7 +213,7 @@ export const AGENTS = {
212
213
  // Claude Code has no headless Anthropic-hosted dispatch CLI (only
213
214
  // --remote-control, which bridges a *local* session). Its cloud is Rush.
214
215
  cloudProvider: 'rush',
215
- capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'CLAUDE.md' }, workflows: true, modes: ['plan', 'edit', 'auto', 'skip'], rulesImports: true },
216
+ capabilities: { hooks: true, mcp: true, mcpHttp: true, mcpHeaders: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'CLAUDE.md' }, workflows: true, modes: ['plan', 'edit', 'auto', 'skip'], rulesImports: true },
216
217
  },
217
218
  // codex hooks: gated to >= 0.116.0 (introduced [features] codex_hooks flag).
218
219
  codex: {
@@ -232,7 +233,7 @@ export const AGENTS = {
232
233
  variableSyntax: '$ARGUMENTS',
233
234
  supportsHooks: true,
234
235
  cloudProvider: 'codex',
235
- capabilities: { hooks: { since: '0.116.0' }, mcp: true, allowlist: false, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'skip'] },
236
+ capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: false, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'skip'] },
236
237
  },
237
238
  gemini: {
238
239
  id: 'gemini',
@@ -261,7 +262,7 @@ export const AGENTS = {
261
262
  url: 'https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/',
262
263
  },
263
264
  // gemini hooks: shipped in v0.26.0 (Jan 2026); older binaries silently ignore the `hooks` key.
264
- capabilities: { hooks: { since: '0.26.0' }, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'GEMINI.md' }, workflows: false, modes: ['plan', 'edit', 'skip'], rulesImports: true },
265
+ capabilities: { hooks: { since: '0.26.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'GEMINI.md' }, workflows: false, modes: ['plan', 'edit', 'skip'], rulesImports: true },
265
266
  },
266
267
  cursor: {
267
268
  id: 'cursor',
@@ -279,7 +280,7 @@ export const AGENTS = {
279
280
  format: 'markdown',
280
281
  variableSyntax: '$ARGUMENTS',
281
282
  supportsHooks: false,
282
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: '.cursorrules' }, workflows: false, modes: ['edit', 'skip'] },
283
+ capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: '.cursorrules' }, workflows: false, modes: ['edit', 'skip'] },
283
284
  },
284
285
  opencode: {
285
286
  id: 'opencode',
@@ -296,7 +297,7 @@ export const AGENTS = {
296
297
  format: 'markdown',
297
298
  variableSyntax: '$ARGUMENTS',
298
299
  supportsHooks: false,
299
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
300
+ capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
300
301
  },
301
302
  openclaw: {
302
303
  id: 'openclaw',
@@ -314,7 +315,7 @@ export const AGENTS = {
314
315
  format: 'markdown',
315
316
  variableSyntax: '{{ARGUMENTS}}',
316
317
  supportsHooks: true,
317
- capabilities: { hooks: true, mcp: true, allowlist: false, skills: true, commands: false, plugins: true, subagents: true, rules: { file: 'workspace/AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'skip'] },
318
+ capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: false, plugins: true, subagents: true, rules: { file: 'workspace/AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'skip'] },
318
319
  },
319
320
  copilot: {
320
321
  id: 'copilot',
@@ -336,7 +337,7 @@ export const AGENTS = {
336
337
  format: 'markdown',
337
338
  variableSyntax: '$ARGUMENTS',
338
339
  supportsHooks: false,
339
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'auto', 'skip'] },
340
+ capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'auto', 'skip'] },
340
341
  },
341
342
  amp: {
342
343
  id: 'amp',
@@ -353,7 +354,7 @@ export const AGENTS = {
353
354
  format: 'markdown',
354
355
  variableSyntax: '$ARGUMENTS',
355
356
  supportsHooks: false,
356
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
357
+ capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
357
358
  },
358
359
  kiro: {
359
360
  id: 'kiro',
@@ -371,7 +372,7 @@ export const AGENTS = {
371
372
  format: 'markdown',
372
373
  variableSyntax: '$ARGUMENTS',
373
374
  supportsHooks: false,
374
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
375
+ capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
375
376
  },
376
377
  goose: {
377
378
  id: 'goose',
@@ -389,7 +390,7 @@ export const AGENTS = {
389
390
  format: 'markdown',
390
391
  variableSyntax: '$ARGUMENTS',
391
392
  supportsHooks: false,
392
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: false, commands: false, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
393
+ capabilities: { hooks: false, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: false, skills: false, commands: false, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
393
394
  },
394
395
  // Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
395
396
  // configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
@@ -418,7 +419,7 @@ export const AGENTS = {
418
419
  variableSyntax: '{{args}}',
419
420
  supportsHooks: true,
420
421
  cloudProvider: 'antigravity',
421
- capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit', 'skip'], rulesImports: false },
422
+ capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit', 'skip'], rulesImports: false },
422
423
  },
423
424
  // xAI Grok Build CLI (`grok`) — early beta, SuperGrok Heavy. Auth via OAuth on
424
425
  // first launch, or XAI_API_KEY env var for headless. MCP servers configured inline
@@ -445,6 +446,8 @@ export const AGENTS = {
445
446
  capabilities: {
446
447
  hooks: true,
447
448
  mcp: true,
449
+ mcpHttp: false,
450
+ mcpHeaders: false,
448
451
  allowlist: true, // maps to Grok's granular Bash/Edit/Write/Read/Grep/WebFetch/MCPTool rules
449
452
  skills: true,
450
453
  commands: false, // covered by skills
@@ -481,6 +484,8 @@ export const AGENTS = {
481
484
  capabilities: {
482
485
  hooks: true,
483
486
  mcp: true,
487
+ mcpHttp: false,
488
+ mcpHeaders: false,
484
489
  allowlist: true,
485
490
  skills: true,
486
491
  commands: false,
@@ -540,6 +545,8 @@ export const AGENTS = {
540
545
  capabilities: {
541
546
  hooks: true,
542
547
  mcp: true,
548
+ mcpHttp: false,
549
+ mcpHeaders: false,
543
550
  allowlist: false,
544
551
  skills: false,
545
552
  commands: true,
@@ -708,11 +715,6 @@ export async function getAllCliStates() {
708
715
  }
709
716
  return states;
710
717
  }
711
- /** Check whether the agent's config directory exists on disk. */
712
- export function isConfigured(agentId) {
713
- const agent = AGENTS[agentId];
714
- return fs.existsSync(agent.configDir);
715
- }
716
718
  /**
717
719
  * Agents that `agents setup` probes for pre-existing native installations
718
720
  * (i.e., a config dir present before agents-cli took over). Add an agent here
@@ -1310,31 +1312,16 @@ function buildIdentityKey(agentId, parts) {
1310
1312
  return null;
1311
1313
  return `${agentId}:${encoded.join(':')}`;
1312
1314
  }
1313
- /** Check whether a named MCP server is registered with the agent's CLI. */
1314
- export async function isMcpRegistered(agentId, mcpName) {
1315
- const agent = AGENTS[agentId];
1316
- if (!agent.capabilities.mcp || !(await isCliInstalled(agentId))) {
1317
- return false;
1318
- }
1319
- try {
1320
- const { stdout } = await execFileAsync(agent.cliCommand, ['mcp', 'list']);
1321
- return stdout.toLowerCase().includes(mcpName.toLowerCase());
1322
- }
1323
- catch {
1324
- /* mcp list command failed */
1325
- return false;
1326
- }
1327
- }
1328
1315
  /** Register an MCP server with an agent's CLI via `mcp add`. */
1329
1316
  export async function registerMcp(agentId, name, command, scope = 'user', transport = 'stdio', options) {
1330
1317
  const agent = AGENTS[agentId];
1331
1318
  if (!agent.capabilities.mcp) {
1332
1319
  return { success: false, error: 'Agent does not support MCP' };
1333
1320
  }
1334
- if (transport === 'http' && agentId !== 'claude' && agentId !== 'codex' && agentId !== 'gemini') {
1321
+ if (transport === 'http' && !supports(agentId, 'mcpHttp').ok) {
1335
1322
  return { success: false, error: 'skipped: agent does not support HTTP MCP registration' };
1336
1323
  }
1337
- if (transport === 'http' && options?.headers && Object.keys(options.headers).length > 0 && agentId !== 'claude') {
1324
+ if (transport === 'http' && options?.headers && Object.keys(options.headers).length > 0 && !supports(agentId, 'mcpHeaders').ok) {
1338
1325
  return { success: false, error: 'skipped: HTTP MCP headers are only supported for Claude registration' };
1339
1326
  }
1340
1327
  if (!options?.binary && !(await isCliInstalled(agentId))) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Concrete Linear gateway for auto-dispatch.
3
+ *
4
+ * Talks to the Linear GraphQL API. The API key is read from the LINEAR_API_KEY
5
+ * env var, falling back to the macOS keychain generic-password `linear-api-key`
6
+ * (how the rest of the stack stores it). If no key is resolvable, the gateway is
7
+ * absent and the daemon skips auto-dispatch entirely.
8
+ *
9
+ * "Started" (Doing) and "unstarted" (Todo) are Linear state *types*, so we never
10
+ * hardcode a team's custom state names. `startIssue` resolves the team's first
11
+ * started-type state (cached per team) and moves the issue there — the existing
12
+ * factory webhook turns that transition into a real run.
13
+ */
14
+ import type { LinearGateway } from './auto-dispatch.js';
15
+ /** Resolve the Linear API key from env or (on macOS) the keychain. Null if absent. */
16
+ export declare function resolveLinearApiKey(): string | null;
17
+ /** Build a real Linear gateway, or null when no API key is configured. */
18
+ export declare function createLinearGateway(): LinearGateway | null;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Concrete Linear gateway for auto-dispatch.
3
+ *
4
+ * Talks to the Linear GraphQL API. The API key is read from the LINEAR_API_KEY
5
+ * env var, falling back to the macOS keychain generic-password `linear-api-key`
6
+ * (how the rest of the stack stores it). If no key is resolvable, the gateway is
7
+ * absent and the daemon skips auto-dispatch entirely.
8
+ *
9
+ * "Started" (Doing) and "unstarted" (Todo) are Linear state *types*, so we never
10
+ * hardcode a team's custom state names. `startIssue` resolves the team's first
11
+ * started-type state (cached per team) and moves the issue there — the existing
12
+ * factory webhook turns that transition into a real run.
13
+ */
14
+ import { execFileSync } from 'child_process';
15
+ const LINEAR_API = 'https://api.linear.app/graphql';
16
+ /** Resolve the Linear API key from env or (on macOS) the keychain. Null if absent. */
17
+ export function resolveLinearApiKey() {
18
+ const fromEnv = process.env.LINEAR_API_KEY?.trim();
19
+ if (fromEnv)
20
+ return fromEnv;
21
+ if (process.platform === 'darwin') {
22
+ try {
23
+ const out = execFileSync('security', ['find-generic-password', '-s', 'linear-api-key', '-w'], {
24
+ encoding: 'utf-8',
25
+ stdio: ['ignore', 'pipe', 'ignore'],
26
+ });
27
+ const key = out.trim();
28
+ if (key)
29
+ return key;
30
+ }
31
+ catch {
32
+ // not in keychain — fall through
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ async function gql(apiKey, query, variables) {
38
+ const res = await fetch(LINEAR_API, {
39
+ method: 'POST',
40
+ headers: { Authorization: apiKey, 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ query, variables }),
42
+ });
43
+ if (!res.ok)
44
+ throw new Error(`Linear API HTTP ${res.status}`);
45
+ const json = (await res.json());
46
+ if (json.errors?.length)
47
+ throw new Error(`Linear API: ${json.errors.map((e) => e.message).join('; ')}`);
48
+ if (!json.data)
49
+ throw new Error('Linear API: empty response');
50
+ return json.data;
51
+ }
52
+ /** Build a real Linear gateway, or null when no API key is configured. */
53
+ export function createLinearGateway() {
54
+ const apiKey = resolveLinearApiKey();
55
+ if (!apiKey)
56
+ return null;
57
+ // Cache the resolved started-state id per team so we don't re-query each dispatch.
58
+ const startedStateByTeam = new Map();
59
+ async function resolveStartedState(teamId) {
60
+ const cached = startedStateByTeam.get(teamId);
61
+ if (cached)
62
+ return cached;
63
+ const data = await gql(apiKey, `query($t:String!){ workflowStates(filter:{ team:{ id:{ eq:$t } }, type:{ eq:"started" } }, first:10){ nodes{ id type position } } }`, { t: teamId });
64
+ const nodes = data.workflowStates.nodes.slice().sort((a, b) => a.position - b.position);
65
+ const state = nodes[0];
66
+ if (!state)
67
+ throw new Error(`no started-type workflow state for team ${teamId}`);
68
+ startedStateByTeam.set(teamId, state.id);
69
+ return state.id;
70
+ }
71
+ // team id per issue, captured during fetchDelegatedTodo, so startIssue can resolve state.
72
+ const teamByIssue = new Map();
73
+ return {
74
+ async countInFlight(linearProjectId) {
75
+ const data = await gql(apiKey, `query($p:ID!){ issues(filter:{ project:{ id:{ eq:$p } }, state:{ type:{ eq:"started" } }, delegate:{ null:false } }, first:250){ nodes{ id } } }`, { p: linearProjectId });
76
+ return data.issues.nodes.length;
77
+ },
78
+ async fetchDelegatedTodo(linearProjectId) {
79
+ const data = await gql(apiKey, `query($p:ID!){ issues(filter:{ project:{ id:{ eq:$p } }, state:{ type:{ eq:"unstarted" } }, delegate:{ null:false } }, first:50){ nodes{ id identifier title priority delegate{ name } team{ id } } } }`, { p: linearProjectId });
80
+ const out = [];
81
+ for (const n of data.issues.nodes) {
82
+ if (!n.delegate?.name)
83
+ continue;
84
+ if (n.team?.id)
85
+ teamByIssue.set(n.id, n.team.id);
86
+ out.push({ id: n.id, identifier: n.identifier, title: n.title ?? '', priority: n.priority ?? 0, delegateName: n.delegate.name });
87
+ }
88
+ return out;
89
+ },
90
+ async markStarted(issueId, delegateName) {
91
+ const teamId = teamByIssue.get(issueId);
92
+ if (!teamId)
93
+ throw new Error(`unknown team for issue ${issueId} (fetch it first)`);
94
+ const stateId = await resolveStartedState(teamId);
95
+ const data = await gql(apiKey, `mutation($id:String!,$s:String!){ issueUpdate(id:$id, input:{ stateId:$s }){ success } }`, { id: issueId, s: stateId });
96
+ if (!data.issueUpdate.success)
97
+ throw new Error(`issueUpdate failed for ${issueId}`);
98
+ // Leave a trail so the auto-dispatch is visible in the ticket history.
99
+ try {
100
+ await gql(apiKey, `mutation($i:String!,$b:String!){ commentCreate(input:{ issueId:$i, body:$b }){ success } }`, { i: issueId, b: `Auto-dispatched to **${delegateName}** by the factory (moved Todo → Doing).` });
101
+ }
102
+ catch {
103
+ // comment is best-effort — the state change is what matters
104
+ }
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Concrete Dispatcher for auto-dispatch — runs a ticket through agents-cli's own
3
+ * cloud-provider abstraction (`resolveProvider().dispatch()`), the same layer as
4
+ * `agents cloud run`. Rush (Prix) is one provider among rush/codex/factory; a
5
+ * project pins its provider via `provider` in projects.json, else the delegate
6
+ * agent's native cloud is used. This is what removes the hidden Prix coupling —
7
+ * auto-dispatch no longer depends on the prix/api webhook.
8
+ */
9
+ import type { Dispatcher } from './auto-dispatch.js';
10
+ export declare function createProviderDispatcher(): Dispatcher;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Concrete Dispatcher for auto-dispatch — runs a ticket through agents-cli's own
3
+ * cloud-provider abstraction (`resolveProvider().dispatch()`), the same layer as
4
+ * `agents cloud run`. Rush (Prix) is one provider among rush/codex/factory; a
5
+ * project pins its provider via `provider` in projects.json, else the delegate
6
+ * agent's native cloud is used. This is what removes the hidden Prix coupling —
7
+ * auto-dispatch no longer depends on the prix/api webhook.
8
+ */
9
+ import { resolveProvider } from './cloud/registry.js';
10
+ export function createProviderDispatcher() {
11
+ return {
12
+ async dispatch({ agent, prompt, repo, provider }) {
13
+ const p = resolveProvider(provider, agent);
14
+ const caps = p.capabilities();
15
+ if (!caps.available) {
16
+ throw new Error(`cloud provider '${p.id}' is not available (auth/binary missing)`);
17
+ }
18
+ if (!caps.dispatch) {
19
+ throw new Error(`cloud provider '${p.id}' does not support dispatch`);
20
+ }
21
+ const task = await p.dispatch({ agent, prompt, repo });
22
+ return { id: task.id };
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Auto-dispatch — pull side of the factory dispatch loop.
3
+ *
4
+ * Polls Linear for issues that are delegated to an agent and still in Todo for a
5
+ * managed project, and — up to a per-project concurrency cap — DISPATCHES each
6
+ * through agents-cli's own cloud-provider abstraction (`resolveProvider().dispatch()`),
7
+ * then marks the ticket Doing so it isn't picked up twice.
8
+ *
9
+ * The dispatch goes through the same provider layer as `agents cloud run`, so Rush
10
+ * (Prix) is just one provider among rush/codex/factory — NOT a hidden requirement.
11
+ * A project may pin its provider via `provider` in ~/.agents/factory/projects.json.
12
+ *
13
+ * OPT-IN: a project auto-dispatches ONLY when `autoDispatch: true` AND `maxAgents > 0`.
14
+ * Nothing global is on by default.
15
+ */
16
+ /** The subset of a managed project this module needs (mirror of the shared JSON). */
17
+ export interface AutoDispatchProject {
18
+ id: string;
19
+ name: string;
20
+ linearProjectId?: string;
21
+ repoSlug?: string;
22
+ autoDispatch?: boolean;
23
+ maxAgents?: number;
24
+ provider?: string;
25
+ }
26
+ /** A Linear issue that is a candidate for dispatch. */
27
+ export interface DelegatedIssue {
28
+ id: string;
29
+ identifier: string;
30
+ title: string;
31
+ delegateName: string;
32
+ priority: number;
33
+ }
34
+ /** One planned dispatch: which issue, to which agent, in which project. */
35
+ export interface PlannedDispatch {
36
+ projectId: string;
37
+ repoSlug?: string;
38
+ provider?: string;
39
+ issueId: string;
40
+ identifier: string;
41
+ title: string;
42
+ delegateName: string;
43
+ }
44
+ /** Path to the shared factory project registry (written by the factory UI). */
45
+ export declare function factoryProjectsPath(): string;
46
+ /** Read the project registry rows this module cares about. */
47
+ export declare function readAutoDispatchProjects(): AutoDispatchProject[];
48
+ /** A project is eligible only when it has explicitly opted in with a positive cap. */
49
+ export declare function isEligible(p: AutoDispatchProject): boolean;
50
+ /**
51
+ * PURE planner. Given eligible projects, in-flight counts, and delegated-Todo
52
+ * issues per project, decide which issues to dispatch — never exceeding
53
+ * `maxAgents` in-flight. Highest Linear priority first.
54
+ */
55
+ export declare function planAutoDispatch(projects: AutoDispatchProject[], inFlightByProject: Record<string, number>, delegatedTodoByProject: Record<string, DelegatedIssue[]>): PlannedDispatch[];
56
+ /** Map Linear priority to a sortable rank (urgent first, none last). */
57
+ export declare function priorityRank(priority: number): number;
58
+ /** Build the prompt an auto-dispatched agent receives for a ticket. */
59
+ export declare function dispatchPrompt(identifier: string, title: string): string;
60
+ /** Linear I/O surface — discovery + bookkeeping (NOT the dispatch trigger). */
61
+ export interface LinearGateway {
62
+ /** Count issues in a started ("Doing") state whose delegate is set, per project. */
63
+ countInFlight(linearProjectId: string): Promise<number>;
64
+ /** Fetch delegated issues in an unstarted ("Todo") state for a project. */
65
+ fetchDelegatedTodo(linearProjectId: string): Promise<DelegatedIssue[]>;
66
+ /** After a successful dispatch: move the issue Todo -> Doing so it isn't re-picked. */
67
+ markStarted(issueId: string, delegateName: string): Promise<void>;
68
+ }
69
+ /** Dispatch surface — runs a ticket through agents-cli's cloud/local provider layer. */
70
+ export interface Dispatcher {
71
+ dispatch(opts: {
72
+ agent: string;
73
+ prompt: string;
74
+ repo?: string;
75
+ provider?: string;
76
+ }): Promise<{
77
+ id: string;
78
+ }>;
79
+ }
80
+ export interface AutoDispatchDeps {
81
+ projects: AutoDispatchProject[];
82
+ linear: LinearGateway;
83
+ dispatcher: Dispatcher;
84
+ log?: (level: 'INFO' | 'WARN' | 'ERROR', msg: string) => void;
85
+ }
86
+ /** One tick: read state per eligible project, plan, dispatch, then mark started. */
87
+ export declare function autoDispatchTick(deps: AutoDispatchDeps): Promise<PlannedDispatch[]>;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Auto-dispatch — pull side of the factory dispatch loop.
3
+ *
4
+ * Polls Linear for issues that are delegated to an agent and still in Todo for a
5
+ * managed project, and — up to a per-project concurrency cap — DISPATCHES each
6
+ * through agents-cli's own cloud-provider abstraction (`resolveProvider().dispatch()`),
7
+ * then marks the ticket Doing so it isn't picked up twice.
8
+ *
9
+ * The dispatch goes through the same provider layer as `agents cloud run`, so Rush
10
+ * (Prix) is just one provider among rush/codex/factory — NOT a hidden requirement.
11
+ * A project may pin its provider via `provider` in ~/.agents/factory/projects.json.
12
+ *
13
+ * OPT-IN: a project auto-dispatches ONLY when `autoDispatch: true` AND `maxAgents > 0`.
14
+ * Nothing global is on by default.
15
+ */
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import { homedir } from 'os';
19
+ /** Path to the shared factory project registry (written by the factory UI). */
20
+ export function factoryProjectsPath() {
21
+ return path.join(homedir(), '.agents', 'factory', 'projects.json');
22
+ }
23
+ /** Read the project registry rows this module cares about. */
24
+ export function readAutoDispatchProjects() {
25
+ let raw;
26
+ try {
27
+ raw = JSON.parse(fs.readFileSync(factoryProjectsPath(), 'utf-8'));
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ if (!Array.isArray(raw))
33
+ return [];
34
+ const out = [];
35
+ for (const r of raw) {
36
+ if (!r || typeof r !== 'object')
37
+ continue;
38
+ const o = r;
39
+ if (typeof o.id !== 'string' || typeof o.name !== 'string')
40
+ continue;
41
+ out.push({
42
+ id: o.id,
43
+ name: o.name,
44
+ linearProjectId: typeof o.linearProjectId === 'string' ? o.linearProjectId : undefined,
45
+ repoSlug: typeof o.repoSlug === 'string' ? o.repoSlug : undefined,
46
+ autoDispatch: o.autoDispatch === true,
47
+ maxAgents: typeof o.maxAgents === 'number' ? o.maxAgents : undefined,
48
+ provider: typeof o.provider === 'string' ? o.provider : undefined,
49
+ });
50
+ }
51
+ return out;
52
+ }
53
+ /** A project is eligible only when it has explicitly opted in with a positive cap. */
54
+ export function isEligible(p) {
55
+ return p.autoDispatch === true && typeof p.maxAgents === 'number' && p.maxAgents > 0 && !!p.linearProjectId;
56
+ }
57
+ /**
58
+ * PURE planner. Given eligible projects, in-flight counts, and delegated-Todo
59
+ * issues per project, decide which issues to dispatch — never exceeding
60
+ * `maxAgents` in-flight. Highest Linear priority first.
61
+ */
62
+ export function planAutoDispatch(projects, inFlightByProject, delegatedTodoByProject) {
63
+ const plan = [];
64
+ for (const p of projects) {
65
+ if (!isEligible(p))
66
+ continue;
67
+ const cap = p.maxAgents;
68
+ const slots = cap - (inFlightByProject[p.id] ?? 0);
69
+ if (slots <= 0)
70
+ continue;
71
+ const candidates = (delegatedTodoByProject[p.id] ?? [])
72
+ .slice()
73
+ .sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority));
74
+ for (const issue of candidates.slice(0, slots)) {
75
+ plan.push({
76
+ projectId: p.id,
77
+ repoSlug: p.repoSlug,
78
+ provider: p.provider,
79
+ issueId: issue.id,
80
+ identifier: issue.identifier,
81
+ title: issue.title,
82
+ delegateName: issue.delegateName,
83
+ });
84
+ }
85
+ }
86
+ return plan;
87
+ }
88
+ /** Map Linear priority to a sortable rank (urgent first, none last). */
89
+ export function priorityRank(priority) {
90
+ return priority === 0 ? 5 : priority;
91
+ }
92
+ /** Build the prompt an auto-dispatched agent receives for a ticket. */
93
+ export function dispatchPrompt(identifier, title) {
94
+ return `Work on Linear ticket ${identifier}: ${title}. Read the ticket for full context, implement it, and open a PR when done.`;
95
+ }
96
+ /** One tick: read state per eligible project, plan, dispatch, then mark started. */
97
+ export async function autoDispatchTick(deps) {
98
+ const log = deps.log ?? (() => { });
99
+ const eligible = deps.projects.filter(isEligible);
100
+ if (eligible.length === 0)
101
+ return [];
102
+ const inFlight = {};
103
+ const todo = {};
104
+ for (const p of eligible) {
105
+ const pid = p.linearProjectId;
106
+ try {
107
+ inFlight[p.id] = await deps.linear.countInFlight(pid);
108
+ todo[p.id] = await deps.linear.fetchDelegatedTodo(pid);
109
+ }
110
+ catch (err) {
111
+ log('WARN', `auto-dispatch: failed to read Linear state for '${p.name}': ${err.message}`);
112
+ inFlight[p.id] = Number.MAX_SAFE_INTEGER; // fail closed for this project
113
+ todo[p.id] = [];
114
+ }
115
+ }
116
+ const plan = planAutoDispatch(eligible, inFlight, todo);
117
+ const dispatched = [];
118
+ for (const d of plan) {
119
+ try {
120
+ const task = await deps.dispatcher.dispatch({
121
+ agent: d.delegateName.trim().toLowerCase(),
122
+ prompt: dispatchPrompt(d.identifier, d.title),
123
+ repo: d.repoSlug,
124
+ provider: d.provider,
125
+ });
126
+ // Bookkeeping only — dispatch already happened; moving to Doing keeps the
127
+ // ticket out of the next Todo poll. A failure here is non-fatal.
128
+ try {
129
+ await deps.linear.markStarted(d.issueId, d.delegateName);
130
+ }
131
+ catch (err) {
132
+ log('WARN', `auto-dispatch: dispatched ${d.identifier} but failed to mark Doing: ${err.message}`);
133
+ }
134
+ dispatched.push(d);
135
+ log('INFO', `auto-dispatch: dispatched ${d.identifier} to ${d.delegateName} (task ${task.id})`);
136
+ }
137
+ catch (err) {
138
+ log('WARN', `auto-dispatch: failed to dispatch ${d.identifier}: ${err.message}`);
139
+ }
140
+ }
141
+ return dispatched;
142
+ }
@@ -1,3 +1,12 @@
1
+ // The `ws` client, NOT the platform (undici) WebSocket: undici enforces a
2
+ // non-configurable max decompressed message size, and a CDP response carrying
3
+ // a base64 screenshot of a content-rich page blows past it — the socket dies
4
+ // with code 1006 ("Max decompressed message size exceeded") while the command
5
+ // is pending. `ws` offers no permessage-deflate by default and takes an
6
+ // explicit payload cap. (Reproduced live against a remote Edge over
7
+ // `browser --host`: fresh blank newtab passed, reused content-rich newtab
8
+ // failed on every Page.captureScreenshot.)
9
+ import WSWebSocket from 'ws';
1
10
  const pipeTransports = new Map();
2
11
  export function registerPipeTransport(transport) {
3
12
  const id = `pipe://${process.pid}/${pipeTransports.size + 1}`;
@@ -26,7 +35,7 @@ export class CDPClient {
26
35
  return;
27
36
  }
28
37
  return new Promise((resolve, reject) => {
29
- this.ws = new WebSocket(endpoint);
38
+ this.ws = new WSWebSocket(endpoint, { maxPayload: 256 * 1024 * 1024 });
30
39
  this.transport = 'websocket';
31
40
  this.ws.onopen = () => resolve();
32
41
  this.ws.onerror = () => reject(new Error('WebSocket error'));
@@ -102,7 +111,7 @@ export class CDPClient {
102
111
  this.transport = null;
103
112
  }
104
113
  get connected() {
105
- return ((this.ws !== null && this.ws.readyState === WebSocket.OPEN) ||
114
+ return ((this.ws !== null && this.ws.readyState === WSWebSocket.OPEN) ||
106
115
  (this.pipe !== null && !this.pipe.write.destroyed));
107
116
  }
108
117
  get isOpen() {