@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.
- package/CHANGELOG.md +18 -2
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +10 -1
- package/dist/commands/routines.js +2 -0
- package/dist/lib/agents.js +129 -11
- package/dist/lib/commands.js +29 -0
- package/dist/lib/convert.d.ts +11 -0
- package/dist/lib/convert.js +22 -0
- package/dist/lib/doctor-diff.js +17 -1
- package/dist/lib/goose-commands.d.ts +41 -0
- package/dist/lib/goose-commands.js +176 -0
- package/dist/lib/hooks.js +134 -0
- package/dist/lib/permissions.d.ts +15 -0
- package/dist/lib/permissions.js +99 -0
- package/dist/lib/plugins.d.ts +20 -0
- package/dist/lib/plugins.js +118 -0
- package/dist/lib/resources/permissions.js +2 -0
- package/dist/lib/routines.d.ts +7 -0
- package/dist/lib/routines.js +18 -0
- package/dist/lib/runner.d.ts +7 -0
- package/dist/lib/runner.js +34 -6
- package/dist/lib/session/active.d.ts +8 -1
- package/dist/lib/session/active.js +1 -0
- package/dist/lib/session/parse.js +12 -0
- package/dist/lib/session/state.d.ts +33 -0
- package/dist/lib/session/state.js +40 -0
- package/dist/lib/staleness/detectors/permissions.js +23 -0
- package/dist/lib/staleness/detectors/subagents.js +36 -0
- package/dist/lib/staleness/detectors/workflows.js +29 -0
- package/dist/lib/staleness/writers/commands.js +7 -0
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/staleness/writers/subagents.js +22 -3
- package/dist/lib/subagents.d.ts +32 -0
- package/dist/lib/subagents.js +238 -0
- package/dist/lib/tmux/session.d.ts +13 -7
- package/dist/lib/tmux/session.js +23 -8
- package/dist/lib/versions.js +72 -0
- package/dist/lib/workflows.d.ts +9 -0
- package/dist/lib/workflows.js +84 -2
- package/package.json +1 -1
package/dist/lib/routines.js
CHANGED
|
@@ -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
|
}
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
494
|
+
const viaAgentsRun = dispatchesViaAgentsRun(config);
|
|
495
|
+
const cmd = viaAgentsRun
|
|
474
496
|
? baseCmd
|
|
475
497
|
: pinJobBinary(baseCmd, attemptAgent, attemptVersion);
|
|
476
|
-
const spawnEnv =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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/`.
|
|
7
|
-
*
|
|
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
|
}
|
package/dist/lib/subagents.d.ts
CHANGED
|
@@ -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
|