@phnx-labs/agents-cli 1.20.60 → 1.20.61
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 +6 -2
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/routines.js +2 -0
- package/dist/lib/agents.js +87 -1
- 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/staleness/detectors/workflows.js +29 -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/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
## 1.20.
|
|
5
|
+
## 1.20.61
|
|
6
|
+
|
|
7
|
+
- **Detect OpenCode sign-in state so `agents view` stops mislabeling a logged-in install as "not signed in."** `getAccountInfo` had no `opencode` case, so it fell through to `signedIn: false` and every row printed "(not signed in — run opencode to log in)" even with a live login. It now reads OpenCode's `auth.json` (`$XDG_DATA_HOME/opencode/auth.json`, defaulting to `~/.local/share/opencode/auth.json` on every platform — `xdg-basedir` does not special-case macOS), validates each provider entry against its `oauth`/`api`/`wellknown` credential shape, and reports the account as signed in with the non-secret provider ids surfaced as the account label (e.g. `id:muse-spark`). Credential secrets (`access`/`refresh`/`key`/`token`) are only inspected for presence — never read into any display or JSON output. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/agents.test.ts`.
|
|
8
|
+
- **Wire Antigravity workflows support (RUSH-1580).** `agents` now syncs workflows to Antigravity as markdown files with the required `description` frontmatter plus an `agents_workflow` ownership marker, invocable as `/<name>` slash commands. Antigravity workflows are the one non-version-isolated target: `agy` scans a single shared, HOME-global `~/.gemini/config/global_workflows/` at startup (a real home directory, never symlinked per version — verified via strace of `agy`), so the writer and detector both resolve that shared dir for every installed version instead of a per-version home. Gated at `>= 1.0.6`. The ownership marker prevents overwriting or removing user-authored workflows of the same name. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/workflows.ts`, `apps/cli/src/lib/staleness/detectors/workflows.ts`.
|
|
6
9
|
|
|
10
|
+
## 1.20.60
|
|
7
11
|
- **Fix Goose skill sync status for its native central-storage path.** `agents skills list goose@<version>` now reports skills under `~/.agents/skills/` as installed instead of falsely requiring a per-version `.config/goose/skills/` copy that Goose never reads. Source: `apps/cli/src/lib/skills.ts`.
|
|
8
12
|
- **Correct the documented `auto` and ACP `skip` semantics.** The README and bundled `run` skill now distinguish Kimi's interactive `--auto` from its already-auto-approved headless `-p` path, document Droid's native `--auto high`, and explain that ACP `skip` prefers `allow_always` but falls back to the first permission option offered by the server. Documentation only; runtime behavior is unchanged. Source: `README.md`, `skills/run/SKILL.md`.
|
|
9
|
-
- **Wire Antigravity subagents and Kimi workflow sync (RUSH-1548, RUSH-1581).** Antigravity now receives subagents as custom-agent Markdown under `~/.gemini/config/agents/<name>/agent.md` with the `>= 1.0.16` version gate enforced during sync. Kimi receives workflows as managed `type: flow` skills under `.kimi-code/skills/<name>/SKILL.md`, using the canonical slug as the flow name and an `agents_workflow` marker so native user-owned flows are not overwritten or removed. Antigravity workflows
|
|
13
|
+
- **Wire Antigravity subagents and Kimi workflow sync (RUSH-1548, RUSH-1581).** Antigravity now receives subagents as custom-agent Markdown under `~/.gemini/config/agents/<name>/agent.md` with the `>= 1.0.16` version gate enforced during sync. Kimi receives workflows as managed `type: flow` skills under `.kimi-code/skills/<name>/SKILL.md`, using the canonical slug as the flow name and an `agents_workflow` marker so native user-owned flows are not overwritten or removed. Antigravity workflows are wired separately in RUSH-1580 (they target a shared HOME-global dir, not a version home). Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/subagents.ts`, `apps/cli/src/lib/workflows.ts`.
|
|
10
14
|
- **Release retries rebuild the exact merged, CI-tested release tree even after `main` advances.** A registry/auth failure after the release PR merged previously stranded that version because the catch-up guard required the release merge to remain current `main`. The local release script now validates the original PR head and full green matrix, verifies the SHA-pinned keychain helper, rebuilds the unpinned menu-bar helper from historical source in a detached temporary worktree, rejects mismatched remote tags, and tags/publishes that exact merge without including later commits. Source: `apps/cli/scripts/release.sh`.
|
|
11
15
|
- **Wire Gemini plugins/subagents and Goose workflows/allowlists.** Gemini now syncs plugin bundles as Gemini extensions (`.gemini/extensions/<name>/gemini-extension.json`) from CLI 0.8.0+ and subagents as `.gemini/agents/*.md` from CLI 0.36.0+. Goose now syncs workflows as recipe/subrecipe YAML under `.config/goose/recipes/` and permission groups into `.config/goose/permission.yaml`. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/plugins.ts`, `apps/cli/src/lib/subagents.ts`, `apps/cli/src/lib/workflows.ts`, `apps/cli/src/lib/permissions.ts`. (RUSH-1568, RUSH-1569, RUSH-1574, RUSH-1582)
|
|
12
16
|
- **Wire Gemini permissions/allowlist support (RUSH-1567).** Gemini permission groups now sync Bash allow/deny rules into `.gemini/settings.json` as `tools.core` / `tools.exclude` entries with per-command `ShellTool(...)` patterns; non-Bash canonical permissions remain unsupported by Gemini's native tool grammar and are skipped. Flip `allowlist: true`, register the permission writer/detector through the capability table, and replace the dormant legacy `tools.allowed` serializer. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`, `apps/cli/src/lib/resources/permissions.ts`, `apps/cli/src/lib/staleness/detectors/permissions.ts`.
|
package/README.md
CHANGED
|
@@ -855,7 +855,7 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
|
|
|
855
855
|
| Grok Build | yes | yes | yes | yes | yes | skills ($name) | yes | no | `AGENTS.md` | no |
|
|
856
856
|
| OpenClaw | yes | yes | yes | no | yes | gateway | yes | yes | `workspace/AGENTS.md` | no |
|
|
857
857
|
| Cursor | yes | no | yes | no | yes | yes | no | no | `.cursorrules` | no |
|
|
858
|
-
| OpenCode | yes | no | yes |
|
|
858
|
+
| OpenCode | yes | no | yes | >= 1.1.1 | yes | yes | no | no | `AGENTS.md` | no |
|
|
859
859
|
| Copilot | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
860
860
|
| Amp | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
861
861
|
| Kiro | yes | no | yes | >= 2.8.0 | yes | yes | no | >= 1.23.0 | `AGENTS.md` | no |
|
package/dist/bin/agents
CHANGED
|
Binary file
|
|
@@ -347,6 +347,7 @@ export function registerRoutinesCommands(program) {
|
|
|
347
347
|
.option('--at <time>', 'One-shot mode: run once at this time (e.g., "14:30" or "2026-02-24 09:00"), then disable')
|
|
348
348
|
.option('--end-at <iso>', 'Stop firing on or after this ISO 8601 timestamp (e.g., "2026-12-31T23:59:00Z"); routine auto-disables.')
|
|
349
349
|
.option('--disabled', 'Create the routine but keep it paused (enable later with resume)')
|
|
350
|
+
.option('--resume <sessionId>', 'At fire time, resume this existing session id (via `agents run <agent> --resume`) instead of starting fresh — the actual session reopens with full context and the prompt becomes its next turn. Powers self-scheduled wake-ups (e.g. /hibernate). Requires --agent claude or codex; runs un-sandboxed (the session store lives in the real home, not the job overlay).')
|
|
350
351
|
.action(async (nameOrPath, options) => {
|
|
351
352
|
// Check if inline mode (has flags) or file mode
|
|
352
353
|
const hasInlineFlags = options.schedule || options.agent || options.workflow || options.prompt || options.at;
|
|
@@ -406,6 +407,7 @@ export function registerRoutinesCommands(program) {
|
|
|
406
407
|
...(devices ? { devices } : {}),
|
|
407
408
|
...(runOnce ? { runOnce: true } : {}),
|
|
408
409
|
...(options.endAt ? { endAt: options.endAt } : {}),
|
|
410
|
+
...(options.resume ? { resume: options.resume } : {}),
|
|
409
411
|
};
|
|
410
412
|
const errors = validateJob(config);
|
|
411
413
|
if (errors.length > 0) {
|
package/dist/lib/agents.js
CHANGED
|
@@ -450,7 +450,7 @@ export const AGENTS = {
|
|
|
450
450
|
variableSyntax: '{{args}}',
|
|
451
451
|
supportsHooks: true,
|
|
452
452
|
cloudProvider: 'antigravity',
|
|
453
|
-
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '1.0.16' }, rules: { file: 'AGENTS.md' }, workflows:
|
|
453
|
+
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '1.0.16' }, rules: { file: 'AGENTS.md' }, workflows: { since: '1.0.6' }, memory: false, modes: ['edit', 'skip'], rulesImports: false }, // workflows: markdown files in the shared, HOME-global ~/.gemini/config/global_workflows/ (agy scans it at startup; not version-isolated — see workflows.ts), invoked as /<name> slash commands
|
|
454
454
|
},
|
|
455
455
|
// xAI Grok Build CLI (`grok`) — early beta, SuperGrok Heavy. Auth via OAuth on
|
|
456
456
|
// first launch, or XAI_API_KEY env var for headless. MCP servers configured inline
|
|
@@ -1024,6 +1024,64 @@ async function antigravityKeychainSignedIn() {
|
|
|
1024
1024
|
}
|
|
1025
1025
|
return cachedAgyKeychainSignedIn;
|
|
1026
1026
|
}
|
|
1027
|
+
/**
|
|
1028
|
+
* OpenCode (sst/opencode) stores provider credentials in a single JSON file at
|
|
1029
|
+
* `$XDG_DATA_HOME/opencode/auth.json`, defaulting to
|
|
1030
|
+
* `~/.local/share/opencode/auth.json` on EVERY platform — its `xdg-basedir`
|
|
1031
|
+
* dependency does not special-case macOS, so there is no
|
|
1032
|
+
* `~/Library/Application Support` variant. The path is account-global (not
|
|
1033
|
+
* per-version), matching how `session/discover.ts` already resolves
|
|
1034
|
+
* `~/.local/share/opencode/opencode.db`.
|
|
1035
|
+
*
|
|
1036
|
+
* Resolution order, first existing wins:
|
|
1037
|
+
* 1. `<base>/.local/share/opencode/auth.json` — the passed per-version home.
|
|
1038
|
+
* This is primarily a test hook (suites write a hermetic auth file under a
|
|
1039
|
+
* temp home) but also covers any relocated install.
|
|
1040
|
+
* 2. `$XDG_DATA_HOME/opencode/auth.json` — an explicit XDG override, exactly
|
|
1041
|
+
* what OpenCode itself honours.
|
|
1042
|
+
* 3. `<realHome>/.local/share/opencode/auth.json` — the active default, under
|
|
1043
|
+
* `AGENTS_REAL_HOME` or `os.homedir()`, so every installed version reflects
|
|
1044
|
+
* the one account-global login (same fallback shape as
|
|
1045
|
+
* resolveAccountCredentialPath).
|
|
1046
|
+
* Returns the first existing path, or null. Never throws.
|
|
1047
|
+
*/
|
|
1048
|
+
function resolveOpenCodeAuthPath(base) {
|
|
1049
|
+
const candidates = [path.join(base, '.local', 'share', 'opencode', 'auth.json')];
|
|
1050
|
+
const xdgData = process.env.XDG_DATA_HOME;
|
|
1051
|
+
if (xdgData)
|
|
1052
|
+
candidates.push(path.join(xdgData, 'opencode', 'auth.json'));
|
|
1053
|
+
const realHome = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1054
|
+
candidates.push(path.join(realHome, '.local', 'share', 'opencode', 'auth.json'));
|
|
1055
|
+
for (const candidate of candidates) {
|
|
1056
|
+
try {
|
|
1057
|
+
if (fs.existsSync(candidate))
|
|
1058
|
+
return candidate;
|
|
1059
|
+
}
|
|
1060
|
+
catch { /* unreadable */ }
|
|
1061
|
+
}
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Validate one OpenCode auth.json entry against its discriminated union
|
|
1066
|
+
* (`type: 'oauth' | 'api' | 'wellknown'`) and confirm the credential actually
|
|
1067
|
+
* carries its required secret field(s) non-empty. This guards against a
|
|
1068
|
+
* corrupt/half-written entry reading as signed-in — the same "must have a real
|
|
1069
|
+
* credential" floor grok/antigravity apply. We only INSPECT the shape here; the
|
|
1070
|
+
* secret values (`access`/`refresh`/`key`/`token`) are never read out or
|
|
1071
|
+
* surfaced anywhere.
|
|
1072
|
+
*/
|
|
1073
|
+
function isValidOpenCodeCredential(value) {
|
|
1074
|
+
if (!value || typeof value !== 'object')
|
|
1075
|
+
return false;
|
|
1076
|
+
const cred = value;
|
|
1077
|
+
const nonEmpty = (v) => typeof v === 'string' && v.length > 0;
|
|
1078
|
+
switch (cred.type) {
|
|
1079
|
+
case 'oauth': return nonEmpty(cred.access) || nonEmpty(cred.refresh);
|
|
1080
|
+
case 'api': return nonEmpty(cred.key);
|
|
1081
|
+
case 'wellknown': return nonEmpty(cred.key) && nonEmpty(cred.token);
|
|
1082
|
+
default: return false;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1027
1085
|
export async function getAccountInfo(agentId, home) {
|
|
1028
1086
|
const base = home || os.homedir();
|
|
1029
1087
|
const empty = {
|
|
@@ -1253,6 +1311,34 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1253
1311
|
return { ...empty, lastActive };
|
|
1254
1312
|
return { ...empty, signedIn: true, lastActive };
|
|
1255
1313
|
}
|
|
1314
|
+
case 'opencode': {
|
|
1315
|
+
// OpenCode's auth.json is a record keyed by provider id ->
|
|
1316
|
+
// { type: 'oauth'|'api'|'wellknown', ...secret fields }. There is no
|
|
1317
|
+
// email/identity claim to surface, so — like antigravity/kimi — we
|
|
1318
|
+
// report signed-in state plus the NON-SECRET provider metadata (which
|
|
1319
|
+
// provider ids hold a valid credential) and never read the tokens/keys
|
|
1320
|
+
// themselves. The user's complaint was the row read "not signed in"
|
|
1321
|
+
// despite a live login; a valid provider entry now shows e.g.
|
|
1322
|
+
// "id:muse-spark" so they can see exactly which provider is configured.
|
|
1323
|
+
const authPath = resolveOpenCodeAuthPath(base);
|
|
1324
|
+
if (!authPath)
|
|
1325
|
+
return { ...empty, lastActive };
|
|
1326
|
+
const data = JSON.parse(await fs.promises.readFile(authPath, 'utf-8'));
|
|
1327
|
+
if (!data || typeof data !== 'object')
|
|
1328
|
+
return { ...empty, lastActive };
|
|
1329
|
+
const providers = Object.entries(data)
|
|
1330
|
+
.filter(([, cred]) => isValidOpenCodeCredential(cred))
|
|
1331
|
+
.map(([id]) => id)
|
|
1332
|
+
.sort();
|
|
1333
|
+
if (providers.length === 0)
|
|
1334
|
+
return { ...empty, lastActive };
|
|
1335
|
+
// Provider ids are config keys (e.g. "anthropic", "muse-spark"), not
|
|
1336
|
+
// secrets. Join them into a stable, human-readable account label +
|
|
1337
|
+
// identity key for usage dedup.
|
|
1338
|
+
const accountId = providers.join('+');
|
|
1339
|
+
const accountKey = buildIdentityKey(agentId, [['providers', accountId]]);
|
|
1340
|
+
return { ...empty, signedIn: true, accountId, accountKey, lastActive };
|
|
1341
|
+
}
|
|
1256
1342
|
default:
|
|
1257
1343
|
return { ...empty, lastActive };
|
|
1258
1344
|
}
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export interface JobConfig {
|
|
|
75
75
|
version?: string;
|
|
76
76
|
runOnce?: boolean;
|
|
77
77
|
endAt?: string;
|
|
78
|
+
/**
|
|
79
|
+
* When set, the job resumes this existing agent session id at fire time
|
|
80
|
+
* (`agents run <agent> --resume <id>`) instead of starting a fresh conversation,
|
|
81
|
+
* so the actual session reopens with full context and `prompt` becomes its next
|
|
82
|
+
* turn. Powers self-scheduled wake-ups (e.g. /hibernate). claude/codex only.
|
|
83
|
+
*/
|
|
84
|
+
resume?: string;
|
|
78
85
|
/** When set, executeJob runs this job through the loop driver instead of once. */
|
|
79
86
|
loop?: LoopConfig;
|
|
80
87
|
}
|
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)
|
|
@@ -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))
|
|
@@ -146,19 +146,25 @@ export declare function setSessionHook(name: string, hook: string, command: stri
|
|
|
146
146
|
* in the tmux server instead of launching a second tmux client against
|
|
147
147
|
* the same socket from inside the hook. That self-client could race the
|
|
148
148
|
* server under load and leave the dead split behind.
|
|
149
|
+
* v5 — `run-shell -b -C "kill-pane -t #{hook_pane}"` runs the targeted kill-pane
|
|
150
|
+
* in the background inside the server command queue. The synchronous
|
|
151
|
+
* variant could stall the hook on a loaded CI runner, letting the dead
|
|
152
|
+
* split survive until the test's wait timeout expired (flake in CI shard
|
|
153
|
+
* 3: pane-guarded pane-died hook / user split exit).
|
|
149
154
|
*/
|
|
150
|
-
export declare const AGENT_HOOK_SCHEMA =
|
|
155
|
+
export declare const AGENT_HOOK_SCHEMA = 5;
|
|
151
156
|
/**
|
|
152
157
|
* The guarded `pane-died` hook. Detach the client ONLY when the agent pane dies
|
|
153
158
|
* (so the blocking attach in runInTmux returns and the exit status can be read);
|
|
154
159
|
* a user split's death runs the else-branch, closing just that split. The
|
|
155
|
-
* else-branch goes through `run-shell -C` with an explicit `-t #{hook_pane}`
|
|
160
|
+
* else-branch goes through `run-shell -b -C` with an explicit `-t #{hook_pane}`
|
|
156
161
|
* target: tmux format-expands the command at fire time and executes it inside
|
|
157
|
-
* the server command queue, so the event pane is always the
|
|
158
|
-
* launching a second tmux client against the same socket
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
+
* the server command queue in the background, so the event pane is always the
|
|
163
|
+
* one killed without launching a second tmux client against the same socket and
|
|
164
|
+
* without stalling the hook on a loaded runner. A bare `kill-pane` relied on
|
|
165
|
+
* the hook context supplying a "current pane", while an external self-client
|
|
166
|
+
* could race the server under load. Single source of truth: both the
|
|
167
|
+
* spawn-wrap (exec.ts) and the daemon reconcile build the hook here, so the
|
|
162
168
|
* two can never drift.
|
|
163
169
|
*/
|
|
164
170
|
export declare function agentPaneDiedHook(sessionName: string, agentPane: string): string;
|
package/dist/lib/tmux/session.js
CHANGED
|
@@ -93,6 +93,15 @@ export async function createSession(opts) {
|
|
|
93
93
|
const res = await runTmux({ socket, args, env: opts.env });
|
|
94
94
|
// Only the new-session command in the `;`-chained invocation emits output.
|
|
95
95
|
const pane = /^%\d+$/.test(res.stdout.trim()) ? res.stdout.trim() : undefined;
|
|
96
|
+
// Keep the agent pane around after its process exits (so runInTmux can read
|
|
97
|
+
// the exit status and capture the final error), but do NOT keep that behavior
|
|
98
|
+
// for user-created splits. Apply remain-on-exit to the agent pane only, then
|
|
99
|
+
// revert the global default so future splits close automatically when their
|
|
100
|
+
// command finishes. The global-on above protects a fast-exiting agent during
|
|
101
|
+
// the brief window before the pane option is stamped.
|
|
102
|
+
if (pane) {
|
|
103
|
+
await runTmux({ socket, args: ['set-option', '-pt', pane, 'remain-on-exit', 'on', ';', 'set-option', '-g', 'remain-on-exit', 'off'], throwOnError: false }).catch(() => { });
|
|
104
|
+
}
|
|
96
105
|
const meta = {
|
|
97
106
|
name: opts.name,
|
|
98
107
|
socket,
|
|
@@ -294,25 +303,31 @@ export async function setSessionHook(name, hook, command, socket) {
|
|
|
294
303
|
* in the tmux server instead of launching a second tmux client against
|
|
295
304
|
* the same socket from inside the hook. That self-client could race the
|
|
296
305
|
* server under load and leave the dead split behind.
|
|
306
|
+
* v5 — `run-shell -b -C "kill-pane -t #{hook_pane}"` runs the targeted kill-pane
|
|
307
|
+
* in the background inside the server command queue. The synchronous
|
|
308
|
+
* variant could stall the hook on a loaded CI runner, letting the dead
|
|
309
|
+
* split survive until the test's wait timeout expired (flake in CI shard
|
|
310
|
+
* 3: pane-guarded pane-died hook / user split exit).
|
|
297
311
|
*/
|
|
298
|
-
export const AGENT_HOOK_SCHEMA =
|
|
312
|
+
export const AGENT_HOOK_SCHEMA = 5;
|
|
299
313
|
/** Per-session tmux user-option that records which AGENT_HOOK_SCHEMA a session's hook is at. */
|
|
300
314
|
const HOOK_SCHEMA_OPTION = '@ag_hook_schema';
|
|
301
315
|
/**
|
|
302
316
|
* The guarded `pane-died` hook. Detach the client ONLY when the agent pane dies
|
|
303
317
|
* (so the blocking attach in runInTmux returns and the exit status can be read);
|
|
304
318
|
* a user split's death runs the else-branch, closing just that split. The
|
|
305
|
-
* else-branch goes through `run-shell -C` with an explicit `-t #{hook_pane}`
|
|
319
|
+
* else-branch goes through `run-shell -b -C` with an explicit `-t #{hook_pane}`
|
|
306
320
|
* target: tmux format-expands the command at fire time and executes it inside
|
|
307
|
-
* the server command queue, so the event pane is always the
|
|
308
|
-
* launching a second tmux client against the same socket
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
321
|
+
* the server command queue in the background, so the event pane is always the
|
|
322
|
+
* one killed without launching a second tmux client against the same socket and
|
|
323
|
+
* without stalling the hook on a loaded runner. A bare `kill-pane` relied on
|
|
324
|
+
* the hook context supplying a "current pane", while an external self-client
|
|
325
|
+
* could race the server under load. Single source of truth: both the
|
|
326
|
+
* spawn-wrap (exec.ts) and the daemon reconcile build the hook here, so the
|
|
312
327
|
* two can never drift.
|
|
313
328
|
*/
|
|
314
329
|
export function agentPaneDiedHook(sessionName, agentPane) {
|
|
315
|
-
return `if -F '#{==:#{hook_pane},${agentPane}}' 'detach-client -s =${sessionName}' 'run-shell -C "kill-pane -t #{hook_pane}"'`;
|
|
330
|
+
return `if -F '#{==:#{hook_pane},${agentPane}}' 'detach-client -s =${sessionName}' 'run-shell -b -C "kill-pane -t #{hook_pane}"'`;
|
|
316
331
|
}
|
|
317
332
|
/** Stamp a session's hook-schema marker to the current version. */
|
|
318
333
|
export async function markSessionHookSchema(name, socket) {
|
package/dist/lib/versions.js
CHANGED
|
@@ -1148,6 +1148,72 @@ export function setGlobalDefault(agent, version) {
|
|
|
1148
1148
|
}
|
|
1149
1149
|
writeMeta(meta);
|
|
1150
1150
|
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Grok's official installer writes into ~/.grok/downloads, which (because we
|
|
1153
|
+
* symlink ~/.grok to the active version home) resolves to the PREVIOUS default
|
|
1154
|
+
* home during `agents add grok@<new>`. Move the freshly-downloaded binary and
|
|
1155
|
+
* its generic platform copy into the target version's isolated home so
|
|
1156
|
+
* `listInstalledVersions` and the shim resolve the right binary.
|
|
1157
|
+
*/
|
|
1158
|
+
function relocateGrokBinaryToVersionHome(installedVersion) {
|
|
1159
|
+
const hostGrokLink = path.join(getHomeDir(), agentConfigDirName('grok'));
|
|
1160
|
+
let sourceDownloads;
|
|
1161
|
+
try {
|
|
1162
|
+
sourceDownloads = path.join(fs.readlinkSync(hostGrokLink), 'downloads');
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
sourceDownloads = path.join(hostGrokLink, 'downloads');
|
|
1166
|
+
}
|
|
1167
|
+
const targetDownloads = path.join(getVersionHomePath('grok', installedVersion), agentConfigDirName('grok'), 'downloads');
|
|
1168
|
+
if (!fs.existsSync(sourceDownloads))
|
|
1169
|
+
return;
|
|
1170
|
+
if (path.resolve(sourceDownloads) === path.resolve(targetDownloads))
|
|
1171
|
+
return;
|
|
1172
|
+
const entries = fs.readdirSync(sourceDownloads).filter((e) => e.startsWith('grok-'));
|
|
1173
|
+
if (entries.length === 0)
|
|
1174
|
+
return;
|
|
1175
|
+
fs.mkdirSync(targetDownloads, { recursive: true });
|
|
1176
|
+
const escapedVersion = installedVersion.replace(/\./g, '\\.');
|
|
1177
|
+
const versionedPattern = new RegExp(`^grok-${escapedVersion}-`);
|
|
1178
|
+
const movedPaths = [];
|
|
1179
|
+
// Move the versioned binary first.
|
|
1180
|
+
for (const entry of entries) {
|
|
1181
|
+
if (!versionedPattern.test(entry))
|
|
1182
|
+
continue;
|
|
1183
|
+
const src = path.join(sourceDownloads, entry);
|
|
1184
|
+
const dst = path.join(targetDownloads, entry);
|
|
1185
|
+
if (fs.existsSync(dst))
|
|
1186
|
+
continue;
|
|
1187
|
+
try {
|
|
1188
|
+
fs.renameSync(src, dst);
|
|
1189
|
+
movedPaths.push(dst);
|
|
1190
|
+
}
|
|
1191
|
+
catch {
|
|
1192
|
+
/* ignore per-file failures */
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (movedPaths.length === 0)
|
|
1196
|
+
return;
|
|
1197
|
+
// The installer also creates a generic platform binary (e.g. grok-macos-aarch64)
|
|
1198
|
+
// that is a copy of the versioned binary. Move it too if its size matches.
|
|
1199
|
+
const movedSize = fs.statSync(movedPaths[0]).size;
|
|
1200
|
+
for (const entry of entries) {
|
|
1201
|
+
if (versionedPattern.test(entry))
|
|
1202
|
+
continue; // already handled
|
|
1203
|
+
const src = path.join(sourceDownloads, entry);
|
|
1204
|
+
const dst = path.join(targetDownloads, entry);
|
|
1205
|
+
if (fs.existsSync(dst))
|
|
1206
|
+
continue;
|
|
1207
|
+
try {
|
|
1208
|
+
if (fs.statSync(src).size === movedSize) {
|
|
1209
|
+
fs.renameSync(src, dst);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
catch {
|
|
1213
|
+
/* ignore per-file failures */
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1151
1217
|
/**
|
|
1152
1218
|
* Install a specific version of an agent.
|
|
1153
1219
|
*/
|
|
@@ -1204,6 +1270,12 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1204
1270
|
const versionDir = getVersionDir(agent, installedVersion);
|
|
1205
1271
|
fs.mkdirSync(versionDir, { recursive: true });
|
|
1206
1272
|
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
1273
|
+
// Grok's installer drops the binary into ~/.grok/downloads, which currently
|
|
1274
|
+
// resolves to the PREVIOUS default home. Move it into the target version home
|
|
1275
|
+
// so version isolation is correct.
|
|
1276
|
+
if (agent === 'grok') {
|
|
1277
|
+
relocateGrokBinaryToVersionHome(installedVersion);
|
|
1278
|
+
}
|
|
1207
1279
|
// Symlink the installed binary into the version's node_modules/.bin so
|
|
1208
1280
|
// listInstalledVersions (which checks getBinaryPath) sees this version as
|
|
1209
1281
|
// installed. Without this, `agents add antigravity@latest` succeeds
|
package/dist/lib/workflows.d.ts
CHANGED
|
@@ -265,6 +265,15 @@ export declare function pruneStaleWorkflowSubagents(sharedAgentsDir: string, wor
|
|
|
265
265
|
export declare function countWorkflowSubagents(workflowDir: string): number;
|
|
266
266
|
/** Convert a canonical agents-cli workflow bundle into a Kimi flow skill. */
|
|
267
267
|
export declare function transformWorkflowForKimi(workflowPath: string, name: string): string;
|
|
268
|
+
/**
|
|
269
|
+
* Convert a canonical agents-cli workflow bundle into an Antigravity workflow
|
|
270
|
+
* markdown file. Antigravity discovers workflows as flat `<name>.md` files under
|
|
271
|
+
* `~/.gemini/config/global_workflows/` (scanned by `agy` at startup) and exposes
|
|
272
|
+
* each as a `/<name>` slash command. Frontmatter carries the required `description`
|
|
273
|
+
* plus the shared `agents_workflow` ownership marker so agents-cli never clobbers a
|
|
274
|
+
* user-authored workflow of the same name.
|
|
275
|
+
*/
|
|
276
|
+
export declare function transformWorkflowForAntigravity(workflowPath: string, name: string): string;
|
|
268
277
|
/**
|
|
269
278
|
* Resolve an `agents run <workflow>` reference.
|
|
270
279
|
*
|
package/dist/lib/workflows.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* are composed at runtime by `agents run <workflow>`.
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
|
+
import * as os from 'os';
|
|
9
10
|
import * as path from 'path';
|
|
10
11
|
import * as yaml from 'yaml';
|
|
11
12
|
import { capableAgents } from './capabilities.js';
|
|
@@ -369,6 +370,26 @@ export function transformWorkflowForKimi(workflowPath, name) {
|
|
|
369
370
|
const instructions = (body || fm.description).trim();
|
|
370
371
|
return `---\n${frontmatter}\n---\n\n\`\`\`d2\nBEGIN -> step -> END\nstep: |md\n${indentD2BlockString(instructions)}\n|\n\`\`\`\n`;
|
|
371
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Convert a canonical agents-cli workflow bundle into an Antigravity workflow
|
|
375
|
+
* markdown file. Antigravity discovers workflows as flat `<name>.md` files under
|
|
376
|
+
* `~/.gemini/config/global_workflows/` (scanned by `agy` at startup) and exposes
|
|
377
|
+
* each as a `/<name>` slash command. Frontmatter carries the required `description`
|
|
378
|
+
* plus the shared `agents_workflow` ownership marker so agents-cli never clobbers a
|
|
379
|
+
* user-authored workflow of the same name.
|
|
380
|
+
*/
|
|
381
|
+
export function transformWorkflowForAntigravity(workflowPath, name) {
|
|
382
|
+
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
383
|
+
if (!fm)
|
|
384
|
+
throw new Error(`Invalid WORKFLOW.md in ${workflowPath}`);
|
|
385
|
+
const body = getWorkflowBody(workflowPath) || fm.description;
|
|
386
|
+
const frontmatter = yaml.stringify({
|
|
387
|
+
description: fm.description,
|
|
388
|
+
name: fm.name || name,
|
|
389
|
+
[KIMI_WORKFLOW_MARKER]: name,
|
|
390
|
+
}).trim();
|
|
391
|
+
return `---\n${frontmatter}\n---\n\n${body.trim()}\n`;
|
|
392
|
+
}
|
|
372
393
|
function expandWorkflowPath(ref) {
|
|
373
394
|
if (ref === '~')
|
|
374
395
|
return process.env.HOME ?? ref;
|
|
@@ -527,9 +548,24 @@ export function removeWorkflow(name) {
|
|
|
527
548
|
return { success: false, error: err.message };
|
|
528
549
|
}
|
|
529
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Antigravity user workflows are NOT version-isolated. `agy` scans a single,
|
|
553
|
+
* shared, HOME-global directory at startup — `~/.gemini/config/global_workflows/`
|
|
554
|
+
* — and that dir is a real directory in the user's home, never symlinked into a
|
|
555
|
+
* per-version home (only `~/.gemini/antigravity-cli` is version-scoped). Writing
|
|
556
|
+
* into a version home therefore lands somewhere agy never reads. So every
|
|
557
|
+
* antigravity version resolves to the same real shared dir; `versionHome` is
|
|
558
|
+
* intentionally ignored. (Verified via strace of `agy`: it opens
|
|
559
|
+
* `$HOME/.gemini/config/global_workflows/<name>.md` and never the version home.)
|
|
560
|
+
*/
|
|
561
|
+
function antigravityWorkflowsDir() {
|
|
562
|
+
return path.join(process.env.HOME ?? os.homedir(), '.gemini', 'config', 'global_workflows');
|
|
563
|
+
}
|
|
530
564
|
function workflowTargetRoot(agent, versionHome) {
|
|
531
565
|
if (agent === 'kimi')
|
|
532
566
|
return path.join(versionHome, '.kimi-code', 'skills');
|
|
567
|
+
if (agent === 'antigravity')
|
|
568
|
+
return antigravityWorkflowsDir();
|
|
533
569
|
return path.join(versionHome, 'workflows');
|
|
534
570
|
}
|
|
535
571
|
/** List workflow names synced into a specific agent version home. */
|
|
@@ -543,6 +579,20 @@ export function listWorkflowsForAgent(agent, versionHome) {
|
|
|
543
579
|
.filter(d => kimiWorkflowMarker(path.join(skillsDir, d.name, 'SKILL.md')) === d.name)
|
|
544
580
|
.map(d => d.name);
|
|
545
581
|
}
|
|
582
|
+
if (agent === 'antigravity') {
|
|
583
|
+
const dir = workflowTargetRoot(agent, versionHome);
|
|
584
|
+
if (!fs.existsSync(dir))
|
|
585
|
+
return [];
|
|
586
|
+
try {
|
|
587
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
588
|
+
.filter(d => d.isFile() && d.name.endsWith('.md') && !d.name.startsWith('.'))
|
|
589
|
+
.map(d => d.name.slice(0, -'.md'.length))
|
|
590
|
+
.filter(base => antigravityWorkflowMarker(path.join(dir, `${base}.md`)) === base);
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
546
596
|
if (agent === 'goose') {
|
|
547
597
|
const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
|
|
548
598
|
if (!fs.existsSync(recipesDir))
|
|
@@ -678,7 +728,17 @@ export function syncWorkflowToVersion(workflowPath, name, agent, versionHome) {
|
|
|
678
728
|
return { success: true };
|
|
679
729
|
}
|
|
680
730
|
if (agent === 'antigravity') {
|
|
681
|
-
|
|
731
|
+
const targetDir = workflowTargetRoot(agent, versionHome);
|
|
732
|
+
const targetFile = path.join(targetDir, `${name}.md`);
|
|
733
|
+
if (fs.existsSync(targetFile)) {
|
|
734
|
+
const marker = antigravityWorkflowMarker(targetFile);
|
|
735
|
+
if (marker !== name) {
|
|
736
|
+
return { success: false, error: `Antigravity workflow '${name}' already exists and is not managed by agents-cli` };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
740
|
+
fs.writeFileSync(targetFile, transformWorkflowForAntigravity(workflowPath, name), 'utf-8');
|
|
741
|
+
return { success: true };
|
|
682
742
|
}
|
|
683
743
|
const targetDir = path.join(workflowTargetRoot(agent, versionHome), name);
|
|
684
744
|
fs.mkdirSync(workflowTargetRoot(agent, versionHome), { recursive: true });
|
|
@@ -696,7 +756,20 @@ export function syncWorkflowToVersion(workflowPath, name, agent, versionHome) {
|
|
|
696
756
|
export function removeWorkflowFromVersion(agent, version, name) {
|
|
697
757
|
const versionHome = getVersionHomePath(agent, version);
|
|
698
758
|
if (agent === 'antigravity') {
|
|
699
|
-
|
|
759
|
+
const targetFile = path.join(workflowTargetRoot(agent, versionHome), `${name}.md`);
|
|
760
|
+
if (!fs.existsSync(targetFile)) {
|
|
761
|
+
return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
|
|
762
|
+
}
|
|
763
|
+
if (antigravityWorkflowMarker(targetFile) !== name) {
|
|
764
|
+
return { success: false, error: `Antigravity workflow '${name}' is not managed by agents-cli` };
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
fs.rmSync(targetFile, { force: true });
|
|
768
|
+
return { success: true };
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
return { success: false, error: err.message };
|
|
772
|
+
}
|
|
700
773
|
}
|
|
701
774
|
if (agent === 'goose') {
|
|
702
775
|
const recipePath = path.join(versionHome, '.config', 'goose', 'recipes', `${name}.yaml`);
|
|
@@ -754,6 +827,15 @@ function kimiWorkflowMarker(filePath) {
|
|
|
754
827
|
return null;
|
|
755
828
|
}
|
|
756
829
|
}
|
|
830
|
+
function antigravityWorkflowMarker(filePath) {
|
|
831
|
+
try {
|
|
832
|
+
const fm = parseSkillFrontmatter(filePath);
|
|
833
|
+
return typeof fm?.agents_workflow === 'string' ? fm.agents_workflow : null;
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
757
839
|
/** Iterate all installed (agent, version) pairs that support workflows. */
|
|
758
840
|
export function iterWorkflowsCapableVersions(filter) {
|
|
759
841
|
const result = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.61",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|