@phnx-labs/agents-cli 1.20.60 → 1.20.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +18 -2
  2. package/README.md +1 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +10 -1
  5. package/dist/commands/routines.js +2 -0
  6. package/dist/lib/agents.js +129 -11
  7. package/dist/lib/commands.js +29 -0
  8. package/dist/lib/convert.d.ts +11 -0
  9. package/dist/lib/convert.js +22 -0
  10. package/dist/lib/doctor-diff.js +17 -1
  11. package/dist/lib/goose-commands.d.ts +41 -0
  12. package/dist/lib/goose-commands.js +176 -0
  13. package/dist/lib/hooks.js +134 -0
  14. package/dist/lib/permissions.d.ts +15 -0
  15. package/dist/lib/permissions.js +99 -0
  16. package/dist/lib/plugins.d.ts +20 -0
  17. package/dist/lib/plugins.js +118 -0
  18. package/dist/lib/resources/permissions.js +2 -0
  19. package/dist/lib/routines.d.ts +7 -0
  20. package/dist/lib/routines.js +18 -0
  21. package/dist/lib/runner.d.ts +7 -0
  22. package/dist/lib/runner.js +34 -6
  23. package/dist/lib/session/active.d.ts +8 -1
  24. package/dist/lib/session/active.js +1 -0
  25. package/dist/lib/session/parse.js +12 -0
  26. package/dist/lib/session/state.d.ts +33 -0
  27. package/dist/lib/session/state.js +40 -0
  28. package/dist/lib/staleness/detectors/permissions.js +23 -0
  29. package/dist/lib/staleness/detectors/subagents.js +36 -0
  30. package/dist/lib/staleness/detectors/workflows.js +29 -0
  31. package/dist/lib/staleness/writers/commands.js +7 -0
  32. package/dist/lib/staleness/writers/hooks.js +1 -1
  33. package/dist/lib/staleness/writers/subagents.js +22 -3
  34. package/dist/lib/subagents.d.ts +32 -0
  35. package/dist/lib/subagents.js +238 -0
  36. package/dist/lib/tmux/session.d.ts +13 -7
  37. package/dist/lib/tmux/session.js +23 -8
  38. package/dist/lib/versions.js +72 -0
  39. package/dist/lib/workflows.d.ts +9 -0
  40. package/dist/lib/workflows.js +84 -2
  41. package/package.json +1 -1
@@ -298,6 +298,24 @@ export function validateJob(config) {
298
298
  errors.push('workflow must be a lowercase alphanumeric name (hyphens and underscores allowed, e.g. autodev)');
299
299
  }
300
300
  }
301
+ if (config.resume !== undefined) {
302
+ // Only claude/codex support native `--resume`; other agents would emit a resume
303
+ // flag they don't understand. Resume reopens an agent session, so it is
304
+ // incompatible with workflow and loop jobs (which have no single session to reopen).
305
+ const RESUMABLE_AGENTS = ['claude', 'codex'];
306
+ if (typeof config.resume !== 'string' || config.resume.trim() === '') {
307
+ errors.push('resume must be a non-empty session id string');
308
+ }
309
+ if (hasWorkflow) {
310
+ errors.push('resume cannot be combined with workflow (resume reopens an existing agent session)');
311
+ }
312
+ if (config.loop) {
313
+ errors.push('resume cannot be combined with loop');
314
+ }
315
+ if (hasAgent && config.agent && !RESUMABLE_AGENTS.includes(config.agent)) {
316
+ errors.push(`resume is only supported for agents with native --resume (${RESUMABLE_AGENTS.join(', ')}); got '${config.agent}'`);
317
+ }
318
+ }
301
319
  if (config.mode && !['plan', 'edit', 'auto', 'skip', 'full'].includes(config.mode)) {
302
320
  errors.push("mode must be plan, edit, auto, or skip ('full' accepted as alias for skip)");
303
321
  }
@@ -50,6 +50,13 @@ export declare function resolveRoutineLaunch(config: JobConfig, cwd?: string): P
50
50
  * surface as "agents: no version of X configured".
51
51
  */
52
52
  export declare function pinJobBinary(cmd: string[], agent: AgentId, version: string | undefined): string[];
53
+ /**
54
+ * Whether a job's command is dispatched through `agents run` (so `cmd[0] === 'agents'`)
55
+ * rather than the agent binary directly. True for workflow jobs and for resume jobs.
56
+ * Such commands must NOT be binary-pinned (pinning rewrites cmd[0] to the agent binary,
57
+ * producing a broken `<binary> run …`) and must not receive a version-pinned spawn env.
58
+ */
59
+ export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow' | 'resume'>): boolean;
53
60
  /**
54
61
  * Merge sandbox/base env with the canonical per-version exec env
55
62
  * (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
@@ -44,6 +44,14 @@ export function buildJobCommand(config, resolvedPrompt) {
44
44
  const cmd = ['agents', 'run', config.workflow, resolvedPrompt, '--mode', config.mode];
45
45
  return cmd;
46
46
  }
47
+ // Resume branch: reopen an EXISTING session via `agents run <agent> --resume <id>`
48
+ // instead of starting fresh. The real session resumes with its full prior context
49
+ // (index-based lookup, cwd-independent) and `resolvedPrompt` becomes its next turn —
50
+ // so a self-scheduled wake (e.g. /hibernate) is handled by the session that scheduled
51
+ // it, not a fresh, context-less agent that would refuse an "opaque" instruction.
52
+ if (config.resume) {
53
+ return ['agents', 'run', config.agent, '--resume', config.resume, resolvedPrompt, '--mode', config.mode];
54
+ }
47
55
  const template = AGENT_COMMANDS[config.agent];
48
56
  if (!template) {
49
57
  throw new Error(`Unsupported agent for daemon jobs: ${config.agent}`);
@@ -263,6 +271,15 @@ export function pinJobBinary(cmd, agent, version) {
263
271
  next[0] = binary;
264
272
  return next;
265
273
  }
274
+ /**
275
+ * Whether a job's command is dispatched through `agents run` (so `cmd[0] === 'agents'`)
276
+ * rather than the agent binary directly. True for workflow jobs and for resume jobs.
277
+ * Such commands must NOT be binary-pinned (pinning rewrites cmd[0] to the agent binary,
278
+ * producing a broken `<binary> run …`) and must not receive a version-pinned spawn env.
279
+ */
280
+ export function dispatchesViaAgentsRun(config) {
281
+ return Boolean(config.workflow || config.resume);
282
+ }
266
283
  /**
267
284
  * Merge sandbox/base env with the canonical per-version exec env
268
285
  * (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
@@ -394,7 +411,11 @@ export async function executeJob(config, deps) {
394
411
  schedule: config.schedule,
395
412
  });
396
413
  const resolvedPrompt = resolveJobPrompt(config);
397
- const useSandbox = config.sandbox !== false;
414
+ // Resume must run against the REAL home: `--resume <id>` resolves the session from
415
+ // the agent's config dir, and the sandbox overlay home has only a freshly-generated
416
+ // config with no session store. So a resume job is never sandboxed, regardless of
417
+ // `config.sandbox` (see the resume branch in buildJobCommand).
418
+ const useSandbox = config.sandbox !== false && !config.resume;
398
419
  const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
399
420
  const runId = generateRunId();
400
421
  const runDir = getRunDir(config.name, runId);
@@ -470,10 +491,11 @@ export async function executeJob(config, deps) {
470
491
  if (i === 0) {
471
492
  process.stderr.write(`[agents] routine ${config.name}: running ${label}\n`);
472
493
  }
473
- const cmd = config.workflow
494
+ const viaAgentsRun = dispatchesViaAgentsRun(config);
495
+ const cmd = viaAgentsRun
474
496
  ? baseCmd
475
497
  : pinJobBinary(baseCmd, attemptAgent, attemptVersion);
476
- const spawnEnv = config.workflow
498
+ const spawnEnv = viaAgentsRun
477
499
  ? (() => {
478
500
  const e = { ...baseEnv };
479
501
  if (config.timezone)
@@ -553,10 +575,16 @@ export async function executeJobDetached(config) {
553
575
  const version = launch.chain[0]?.version ?? config.version;
554
576
  const resolvedPrompt = resolveJobPrompt(config);
555
577
  let cmd = buildJobCommand(config, resolvedPrompt);
556
- if (!config.workflow && version) {
578
+ // workflow AND resume dispatch through `agents run` — never binary-pin them (pinning
579
+ // rewrites cmd[0] to the agent binary → broken `<binary> run …`).
580
+ if (!dispatchesViaAgentsRun(config) && version) {
557
581
  cmd = pinJobBinary(cmd, config.agent, version);
558
582
  }
559
- const useSandbox = config.sandbox !== false;
583
+ // Resume must run against the REAL home: `--resume <id>` resolves the session from
584
+ // the agent's config dir, and the sandbox overlay home has only a freshly-generated
585
+ // config with no session store. So a resume job is never sandboxed, regardless of
586
+ // `config.sandbox` (see the resume branch in buildJobCommand).
587
+ const useSandbox = config.sandbox !== false && !config.resume;
560
588
  const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
561
589
  const runId = generateRunId();
562
590
  const runDir = getRunDir(config.name, runId);
@@ -566,7 +594,7 @@ export async function executeJobDetached(config) {
566
594
  const baseEnv = useSandbox
567
595
  ? buildSpawnEnv(overlayHome)
568
596
  : { ...process.env };
569
- const spawnEnv = config.workflow
597
+ const spawnEnv = dispatchesViaAgentsRun(config)
570
598
  ? (() => {
571
599
  const e = { ...baseEnv };
572
600
  if (config.timezone)
@@ -1,6 +1,6 @@
1
1
  import type { CloudTaskStatus } from '../cloud/types.js';
2
2
  import { type PidSessionEntry } from './pid-registry.js';
3
- import { type SessionActivity, type AwaitingReason, type StructuredQuestion, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
3
+ import { type SessionActivity, type AwaitingReason, type StructuredQuestion, type TodoProgress, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
4
4
  import type { SessionAttachment } from './types.js';
5
5
  import { type SessionProvenance } from './provenance.js';
6
6
  /**
@@ -46,6 +46,13 @@ export interface ActiveSession {
46
46
  * says whether it is still pending.
47
47
  */
48
48
  plan?: string;
49
+ /**
50
+ * Live plan progress from the most recent `TodoWrite` (RUSH-1380): the checklist
51
+ * items + a done/total tally + the current step. The Factory Floor renders this
52
+ * as an N/M pill + checklist for every session — including remote and
53
+ * device-dispatched agents that have no local tool-call stream to parse.
54
+ */
55
+ todos?: TodoProgress;
49
56
  /** Last few assistant turns (most-recent last), for at-a-glance context in the UI. */
50
57
  tail?: string[];
51
58
  /** PR opened during the session. */
@@ -267,6 +267,7 @@ function applyState(base, state, fallbackFile) {
267
267
  awaitingReason: state.awaitingReason,
268
268
  question: state.question,
269
269
  plan: state.plan,
270
+ todos: state.todos,
270
271
  tail: state.tail,
271
272
  // Prefer the live preview (latest turn); keep the first-prompt topic as a fallback.
272
273
  preview: state.preview ?? base.preview,
@@ -225,6 +225,18 @@ export function summarizeToolUse(tool, args) {
225
225
  const steps = Array.isArray(args.plan) ? args.plan.length : 0;
226
226
  return `Plan: ${steps} step${steps === 1 ? '' : 's'}`;
227
227
  }
228
+ // Claude's live checklist: show progress + the current step, not a bare "TodoWrite".
229
+ case 'TodoWrite': {
230
+ const todos = Array.isArray(args.todos) ? args.todos : [];
231
+ if (todos.length === 0)
232
+ return 'Plan: 0 steps';
233
+ const done = todos.filter((t) => t?.status === 'completed').length;
234
+ const active = todos.find((t) => t?.status === 'in_progress');
235
+ const step = active?.activeForm || active?.content;
236
+ return step
237
+ ? `Plan ${done}/${todos.length}: ${truncate(String(step), 80)}`
238
+ : `Plan: ${done}/${todos.length} done`;
239
+ }
228
240
  // Codex tools
229
241
  case 'exec_command':
230
242
  return `Bash: ${truncate(String(args.command || args.cmd || '').replace(/\n/g, ' ').trim(), 120)}`;
@@ -41,6 +41,28 @@ export interface StructuredQuestion {
41
41
  reason: AwaitingReason;
42
42
  options?: QuestionOption[];
43
43
  }
44
+ /** One `TodoWrite` checklist item, as Claude's plan tool emits it. */
45
+ export interface TodoItem {
46
+ content: string;
47
+ status: 'pending' | 'in_progress' | 'completed';
48
+ /** Present-continuous label shown while this item is the active step. */
49
+ activeForm?: string;
50
+ }
51
+ /**
52
+ * Live plan progress derived from the most recent `TodoWrite` in the transcript
53
+ * (RUSH-1380). Lets the Factory Floor show "N/M done" + the current step for any
54
+ * session — including remote / device-dispatched agents that carry no local
55
+ * tool-call stream — instead of only a coarse working/idle verb.
56
+ */
57
+ export interface TodoProgress {
58
+ items: TodoItem[];
59
+ /** Count of completed items. */
60
+ done: number;
61
+ /** Total items. */
62
+ total: number;
63
+ /** The in-progress item's activeForm (falls back to its content). The live step. */
64
+ activeForm?: string;
65
+ }
44
66
  export interface DetectedPr {
45
67
  url: string;
46
68
  number?: number;
@@ -88,6 +110,12 @@ export interface SessionState {
88
110
  * render the actual plan without re-parsing the transcript.
89
111
  */
90
112
  plan?: string;
113
+ /**
114
+ * Live plan progress from the most recent `TodoWrite` (RUSH-1380). Present when
115
+ * the session has written a todo list; drives the Factory Floor N/M pill +
116
+ * checklist, notably for remote/device-dispatched agents with no local stream.
117
+ */
118
+ todos?: TodoProgress;
91
119
  /** Last few assistant turns (most-recent last), one line each — panel context. */
92
120
  tail?: string[];
93
121
  lastActivityMs?: number;
@@ -111,6 +139,11 @@ export interface StateContext {
111
139
  /** Override the running window (defaults to 2 min, matching active.ts). */
112
140
  activeWindowMs?: number;
113
141
  }
142
+ /**
143
+ * Derive live plan progress from a `TodoWrite` tool call's args. Returns undefined
144
+ * when there is no usable list, so a session with no plan carries no `todos` field.
145
+ */
146
+ export declare function extractTodoProgress(args?: Record<string, any>): TodoProgress | undefined;
114
147
  /** Detect a worktree from the session cwd, per the `.agents/worktrees/<slug>/` convention. */
115
148
  export declare function detectWorktree(cwd?: string, branch?: string): DetectedWorktree | undefined;
116
149
  /** Detect a tracker ticket from free text (prompt/topic) then a branch name. */
@@ -47,6 +47,40 @@ const PROSE_QUESTION_FRESH_MS = 30 * 60_000;
47
47
  /** Claude tool names that structurally mean "the agent handed control back to you". */
48
48
  const PLAN_TOOL = 'ExitPlanMode';
49
49
  const ASK_TOOL = 'AskUserQuestion';
50
+ /** Claude's live plan/checklist tool. */
51
+ const TODO_TOOL = 'TodoWrite';
52
+ /**
53
+ * Derive live plan progress from a `TodoWrite` tool call's args. Returns undefined
54
+ * when there is no usable list, so a session with no plan carries no `todos` field.
55
+ */
56
+ export function extractTodoProgress(args) {
57
+ const raw = args?.todos;
58
+ if (!Array.isArray(raw) || raw.length === 0)
59
+ return undefined;
60
+ const items = [];
61
+ for (const t of raw) {
62
+ const activeForm = typeof t?.activeForm === 'string' && t.activeForm ? t.activeForm : undefined;
63
+ const content = typeof t?.content === 'string' && t.content
64
+ ? t.content
65
+ : typeof t?.text === 'string' && t.text
66
+ ? t.text
67
+ : activeForm ?? '';
68
+ if (!content)
69
+ continue;
70
+ const status = t?.status === 'completed' || t?.status === 'in_progress' ? t.status : 'pending';
71
+ items.push(activeForm ? { content, status, activeForm } : { content, status });
72
+ }
73
+ if (items.length === 0)
74
+ return undefined;
75
+ const done = items.filter(i => i.status === 'completed').length;
76
+ const inProgress = items.find(i => i.status === 'in_progress');
77
+ return {
78
+ items,
79
+ done,
80
+ total: items.length,
81
+ activeForm: inProgress ? inProgress.activeForm ?? inProgress.content : undefined,
82
+ };
83
+ }
50
84
  /** Trailing '?' or a leading interrogative — a question aimed at the user. */
51
85
  const QUESTION_TRAILING = /\?["'”)\]]?\s*$/;
52
86
  const QUESTION_PHRASE = /\b(shall i|should i|do you want|would you like|which (?:one|option|approach|of)|can you (?:confirm|clarify)|please (?:confirm|clarify|advise)|let me know|are you (?:ok|okay|sure)|proceed\?)\b/i;
@@ -281,12 +315,18 @@ export function inferActivity(events, ctx = {}) {
281
315
  .slice(-3)
282
316
  .map(e => oneLine(e.content ?? ''))
283
317
  .filter(Boolean);
318
+ // Live plan progress: the most recent TodoWrite's checklist (RUSH-1380). Attached
319
+ // to `base` so every return path below carries it — a working, waiting, or idle
320
+ // session all keep showing how far the plan got.
321
+ const lastTodo = lastOf(meaningful, e => e.type === 'tool_use' && e.tool === TODO_TOOL);
322
+ const todos = lastTodo ? extractTodoProgress(lastTodo.args) : undefined;
284
323
  const base = {
285
324
  activity: 'idle',
286
325
  lastRole: lastMsg?.role,
287
326
  lastEventKind: last?.type,
288
327
  lastActivityMs: ctx.mtimeMs,
289
328
  preview: previewSource ? describeEvent(previewSource) : undefined,
329
+ todos,
290
330
  tail: tail.length ? tail : undefined,
291
331
  };
292
332
  if (!last)
@@ -266,6 +266,28 @@ function buildKiroDetector() {
266
266
  },
267
267
  };
268
268
  }
269
+ function buildOpenClawDetector() {
270
+ return {
271
+ kind: 'permissions',
272
+ agent: 'openclaw',
273
+ list({ versionHome }) {
274
+ const configPath = path.join(versionHome, '.openclaw', 'openclaw.json');
275
+ if (!fs.existsSync(configPath))
276
+ return [];
277
+ try {
278
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
279
+ const tools = config.tools;
280
+ const hasAllow = Array.isArray(tools?.alsoAllow) && tools.alsoAllow.length > 0;
281
+ const hasDeny = Array.isArray(tools?.deny) && tools.deny.length > 0;
282
+ if (hasAllow || hasDeny) {
283
+ return discoverPermissionGroups().map(g => g.name);
284
+ }
285
+ }
286
+ catch { /* parse fail */ }
287
+ return [];
288
+ },
289
+ };
290
+ }
269
291
  const handlers = {
270
292
  claude: buildClaudeDetector,
271
293
  codex: buildCodexDetector,
@@ -278,6 +300,7 @@ const handlers = {
278
300
  cursor: buildCursorDetector,
279
301
  droid: buildDroidDetector,
280
302
  kiro: buildKiroDetector,
303
+ openclaw: buildOpenClawDetector,
281
304
  };
282
305
  export const permissionsDetectors = lazyAgentMap(() => {
283
306
  const m = {};
@@ -2,6 +2,8 @@
2
2
  * Subagents detector. Claude/Gemini/Grok: flat .md files under `<agentDir>/agents/`.
3
3
  * Codex: flat .toml files under `<versionHome>/.codex/agents/`.
4
4
  * Droid: flat .md files under `<versionHome>/.factory/droids/`.
5
+ * Cursor: flat .md files under `<versionHome>/.cursor/agents/`.
6
+ * ForgeCode: flat .md files under `<versionHome>/.forge/agents/`.
5
7
  * OpenClaw: subdirectories containing AGENTS.md under `<versionHome>/.openclaw/`.
6
8
  * Mirrors versions.ts:521-539.
7
9
  */
@@ -74,6 +76,20 @@ function buildCopilotDetector() {
74
76
  },
75
77
  };
76
78
  }
79
+ function buildCursorDetector() {
80
+ return {
81
+ kind: 'subagents',
82
+ agent: 'cursor',
83
+ list({ versionHome }) {
84
+ const agentsDir = path.join(versionHome, '.cursor', 'agents');
85
+ if (!fs.existsSync(agentsDir))
86
+ return [];
87
+ return fs.readdirSync(agentsDir)
88
+ .filter(f => f.endsWith('.md'))
89
+ .map(f => f.replace(/\.md$/, ''));
90
+ },
91
+ };
92
+ }
77
93
  function buildOpenclawDetector() {
78
94
  return {
79
95
  kind: 'subagents',
@@ -117,6 +133,23 @@ function buildKiroDetector() {
117
133
  },
118
134
  };
119
135
  }
136
+ function buildForgeDetector() {
137
+ return buildFlatMdAgentsDetector('forge', '.forge');
138
+ }
139
+ function buildGooseDetector() {
140
+ return {
141
+ kind: 'subagents',
142
+ agent: 'goose',
143
+ list({ versionHome }) {
144
+ const agentsDir = path.join(versionHome, '.config', 'goose', 'agents');
145
+ if (!fs.existsSync(agentsDir))
146
+ return [];
147
+ return fs.readdirSync(agentsDir)
148
+ .filter(f => f.endsWith('.yaml'))
149
+ .map(f => f.replace(/\.yaml$/, ''));
150
+ },
151
+ };
152
+ }
120
153
  function buildOpenCodeDetector() {
121
154
  return {
122
155
  kind: 'subagents',
@@ -157,6 +190,9 @@ const handlers = {
157
190
  droid: buildDroidDetector,
158
191
  openclaw: buildOpenclawDetector,
159
192
  kiro: buildKiroDetector,
193
+ cursor: buildCursorDetector,
194
+ forge: buildForgeDetector,
195
+ goose: buildGooseDetector,
160
196
  };
161
197
  export const subagentsDetectors = lazyAgentMap(() => {
162
198
  const m = {};
@@ -3,6 +3,7 @@
3
3
  * containing WORKFLOW.md. Mirrors versions.ts:551-558.
4
4
  */
5
5
  import * as fs from 'fs';
6
+ import * as os from 'os';
6
7
  import * as path from 'path';
7
8
  import * as yaml from 'yaml';
8
9
  import { capableAgents } from '../../capabilities.js';
@@ -36,6 +37,34 @@ function buildWorkflowsDetector(agent) {
36
37
  })
37
38
  .map(d => d.name);
38
39
  }
40
+ if (agent === 'antigravity') {
41
+ // Antigravity user workflows are HOME-global and shared across versions,
42
+ // not version-isolated — see workflows.ts:antigravityWorkflowsDir(). agy
43
+ // scans the real ~/.gemini/config/global_workflows/, so the detector reads
44
+ // the same shared dir the writer targets (versionHome is intentionally unused).
45
+ const dir = path.join(process.env.HOME ?? os.homedir(), '.gemini', 'config', 'global_workflows');
46
+ if (!fs.existsSync(dir))
47
+ return [];
48
+ return fs.readdirSync(dir, { withFileTypes: true })
49
+ .filter(d => d.isFile() && d.name.endsWith('.md') && !d.name.startsWith('.'))
50
+ .filter(d => {
51
+ try {
52
+ const content = fs.readFileSync(path.join(dir, d.name), 'utf-8');
53
+ const lines = content.split('\n');
54
+ if (lines[0] !== '---')
55
+ return false;
56
+ const endIndex = lines.slice(1).findIndex(l => l === '---');
57
+ if (endIndex < 0)
58
+ return false;
59
+ const parsed = yaml.parse(lines.slice(1, endIndex + 1).join('\n'));
60
+ return parsed?.agents_workflow === d.name.slice(0, -'.md'.length);
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ })
66
+ .map(d => d.name.slice(0, -'.md'.length));
67
+ }
39
68
  if (agent === 'goose') {
40
69
  const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
41
70
  if (!fs.existsSync(recipesDir))
@@ -25,6 +25,7 @@ import { safeJoin } from '../../paths.js';
25
25
  import { markdownToToml } from '../../convert.js';
26
26
  import { commandAppliesTo, parseCommandMetadata } from '../../commands.js';
27
27
  import { installCommandSkillToVersion, shouldInstallCommandAsSkill } from '../../command-skills.js';
28
+ import { installGooseCommandToVersion } from '../../goose-commands.js';
28
29
  import { resolveCommandSource, trustedSkillRoots } from './sources.js';
29
30
  import { lazyAgentMap } from './lazy-map.js';
30
31
  function buildCommandsWriter(agent) {
@@ -59,6 +60,12 @@ function buildCommandsWriter(agent) {
59
60
  if (!installed.success)
60
61
  continue;
61
62
  }
63
+ else if (agent === 'goose') {
64
+ // Goose: recipe YAML + config.yaml slash_commands entry, not a file copy.
65
+ const installed = installGooseCommandToVersion(versionHome, cmd, srcFile);
66
+ if (!installed.success)
67
+ continue;
68
+ }
62
69
  else if (agentConfig.format === 'toml') {
63
70
  const content = fs.readFileSync(srcFile, 'utf-8');
64
71
  const tomlContent = markdownToToml(cmd, content);
@@ -36,7 +36,7 @@ function buildHooksWriter(agent) {
36
36
  // registerHooksForGrok — file copy alone only sees top-level available.hooks
37
37
  // names (RUSH-1353). Copilot/Kiro/Goose load managed *.json under their
38
38
  // hooks dirs the same way.
39
- if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity' || agent === 'kimi' || agent === 'droid' || agent === 'copilot' || agent === 'kiro' || agent === 'goose' || agent === 'cursor' || agent === 'grok') {
39
+ if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity' || agent === 'kimi' || agent === 'droid' || agent === 'copilot' || agent === 'kiro' || agent === 'goose' || agent === 'cursor' || agent === 'grok' || agent === 'hermes') {
40
40
  registerHooksToSettings(agent, versionHome);
41
41
  }
42
42
  return { synced };
@@ -3,8 +3,9 @@
3
3
  * .md file under their native agents directory. Codex writes TOML under
4
4
  * `.codex/agents/`.
5
5
  * Droid (Factory AI) flattens each into a custom droid .md under
6
- * `<versionHome>/.factory/droids/`. OpenClaw copies the full subagent
7
- * directory (with AGENT.md renamed to AGENTS.md) into
6
+ * `<versionHome>/.factory/droids/`. Cursor flattens each into a custom
7
+ * subagent .md under `<versionHome>/.cursor/agents/`. OpenClaw copies the
8
+ * full subagent directory (with AGENT.md renamed to AGENTS.md) into
8
9
  * `<versionHome>/.openclaw/<name>/`.
9
10
  *
10
11
  * Source-side discovery is `listInstalledSubagents` from lib/subagents.ts —
@@ -14,7 +15,7 @@
14
15
  import * as fs from 'fs';
15
16
  import * as path from 'path';
16
17
  import { capableAgents } from '../../capabilities.js';
17
- import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, transformSubagentForOpenCode, transformSubagentForAntigravity, transformSubagentForDroid, transformSubagentForKiro, syncSubagentToOpenclaw, parseSubagentFrontmatter, } from '../../subagents.js';
18
+ import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, transformSubagentForOpenCode, transformSubagentForAntigravity, transformSubagentForDroid, transformSubagentForForge, transformSubagentForKiro, transformSubagentForCursor, transformSubagentForGoose, syncSubagentToOpenclaw, parseSubagentFrontmatter, } from '../../subagents.js';
18
19
  import { safeJoin } from '../../paths.js';
19
20
  import { lazyAgentMap } from './lazy-map.js';
20
21
  function buildSubagentsWriter(agent) {
@@ -83,6 +84,24 @@ function buildSubagentsWriter(agent) {
83
84
  fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.json`), transformSubagentForKiro(sub.path));
84
85
  synced.push(sub.name);
85
86
  }
87
+ else if (agent === 'cursor') {
88
+ const agentsDir = path.join(versionHome, '.cursor', 'agents');
89
+ fs.mkdirSync(agentsDir, { recursive: true });
90
+ fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForCursor(sub.path));
91
+ synced.push(sub.name);
92
+ }
93
+ else if (agent === 'forge') {
94
+ const agentsDir = path.join(versionHome, '.forge', 'agents');
95
+ fs.mkdirSync(agentsDir, { recursive: true });
96
+ fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForForge(sub.path));
97
+ synced.push(sub.name);
98
+ }
99
+ else if (agent === 'goose') {
100
+ const agentsDir = path.join(versionHome, '.config', 'goose', 'agents');
101
+ fs.mkdirSync(agentsDir, { recursive: true });
102
+ fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.yaml`), transformSubagentForGoose(sub.path));
103
+ synced.push(sub.name);
104
+ }
86
105
  }
87
106
  catch { /* per-item sync failure: skip */ }
88
107
  }
@@ -68,6 +68,27 @@ export declare function transformSubagentForDroid(subagentDir: string): string;
68
68
  * See GitHub docs for custom agents.
69
69
  */
70
70
  export declare const transformSubagentForCopilot: typeof transformSubagentForDroid;
71
+ /**
72
+ * Transform a subagent into a Cursor CLI custom subagent `.md` file.
73
+ *
74
+ * Cursor loads subagents from `.cursor/agents/*.md` (project) or
75
+ * `~/.cursor/agents/*.md` (user) — Markdown with YAML frontmatter
76
+ * (name, description, model; also readonly/is_background, which our
77
+ * frontmatter schema doesn't carry). Cursor has no `color` field, so this is
78
+ * an alias of transformSubagentForDroid, same as Copilot.
79
+ * See https://cursor.com/docs/subagents.
80
+ */
81
+ export declare const transformSubagentForCursor: typeof transformSubagentForDroid;
82
+ /**
83
+ * Transform a subagent into a ForgeCode custom subagent `.md` file.
84
+ *
85
+ * ForgeCode loads named agents from `.forge/agents/*.md` (project) or
86
+ * `~/.forge/agents/*.md` (user) — Markdown with YAML frontmatter (id, title,
87
+ * description, tools, model, temperature) + a system-prompt body. ForgeCode has
88
+ * no `color` field, so this is an alias of transformSubagentForDroid, same as
89
+ * Copilot/Cursor. See https://forgecode.dev/docs/agent-definition-guide/.
90
+ */
91
+ export declare const transformSubagentForForge: typeof transformSubagentForDroid;
71
92
  /**
72
93
  * Transform a subagent into Antigravity's custom-agent markdown shape.
73
94
  *
@@ -138,6 +159,17 @@ export declare function transformSubagentForCodex(subagentDir: string): string;
138
159
  * set so the subagent can actually run.
139
160
  */
140
161
  export declare function transformSubagentForKiro(subagentDir: string): string;
162
+ /**
163
+ * Transform a subagent into a Goose recipe YAML file.
164
+ *
165
+ * Goose has no dedicated subagent file format — a named subagent IS a recipe.
166
+ * Goose resolves named agents from `~/.config/goose/agents/<name>.yaml` (global)
167
+ * and delegates to them by name in autonomous mode. The recipe schema mirrors the
168
+ * one agents-cli already emits for Goose workflow recipes (`writeGooseSubrecipe`):
169
+ * `version`, `title`, `description`, `instructions`, `prompt`, plus an optional
170
+ * `settings.goose_model`. See goose-docs.ai context-engineering/subagents.
171
+ */
172
+ export declare function transformSubagentForGoose(subagentDir: string): string;
141
173
  /**
142
174
  * Sync a subagent to an OpenClaw workspace
143
175
  * Copies full directory, renames AGENT.md to AGENTS.md