@phnx-labs/agents-cli 1.20.59 → 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/dist/lib/shims.js CHANGED
@@ -2024,18 +2024,28 @@ export function releaseAdoptedLauncher(agent, overrides) {
2024
2024
  // records written before this format existed.
2025
2025
  const launcher = lines[1] || getPathShadowingExecutable(agent) || original;
2026
2026
  const shimReal = canonical(path.join(shimsDir, AGENTS[agent].cliCommand));
2027
+ const shimPath = path.resolve(path.join(shimsDir, AGENTS[agent].cliCommand));
2027
2028
  try {
2028
2029
  // Only rewrite the launcher if it currently points at our shim (i.e. we own
2029
2030
  // it). If the user has since replaced it themselves, leave it alone.
2030
2031
  let pointsAtShim = false;
2031
2032
  try {
2032
- pointsAtShim = fs.lstatSync(launcher).isSymbolicLink()
2033
- && canonical(launcher) === shimReal;
2033
+ const stat = fs.lstatSync(launcher);
2034
+ if (stat.isSymbolicLink()) {
2035
+ const target = fs.readlinkSync(launcher);
2036
+ const absoluteTarget = path.resolve(path.dirname(launcher), target);
2037
+ pointsAtShim = canonicalOrNull(launcher) === shimReal || absoluteTarget === shimPath;
2038
+ }
2034
2039
  }
2035
2040
  catch { /* launcher gone — recreate below */ }
2036
2041
  if (pointsAtShim || !fs.existsSync(launcher)) {
2037
2042
  try {
2038
- fs.rmSync(launcher);
2043
+ if (fs.lstatSync(launcher).isSymbolicLink()) {
2044
+ fs.unlinkSync(launcher);
2045
+ }
2046
+ else {
2047
+ fs.rmSync(launcher, { force: true });
2048
+ }
2039
2049
  }
2040
2050
  catch { /* may not exist */ }
2041
2051
  fs.symlinkSync(original, launcher);
@@ -10,7 +10,7 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import * as os from 'os';
12
12
  import * as yaml from 'yaml';
13
- import { ensureSkillsDir, agentConfigDirName } from './agents.js';
13
+ import { AGENTS, ensureSkillsDir, agentConfigDirName } from './agents.js';
14
14
  import { capableAgents, isCapable } from './capabilities.js';
15
15
  import { getUserSkillsDir, getSkillsDir as getSystemSkillsDir, getProjectAgentsDir, getEnabledExtraRepos, getTrashSkillsDir } from './state.js';
16
16
  import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
@@ -432,6 +432,19 @@ function versionSkillMatches(agent, version, skillName) {
432
432
  */
433
433
  export function diffVersionSkills(agent, version) {
434
434
  const available = new Set(listAllSkills());
435
+ // Goose and other native ~/.agents/skills consumers read central storage
436
+ // directly. They intentionally have no per-version copy to diff, so every
437
+ // available central skill is already current for every supported version.
438
+ if (AGENTS[agent].nativeAgentsSkillsDir) {
439
+ return {
440
+ agent,
441
+ version,
442
+ toAdd: [],
443
+ toUpdate: [],
444
+ matched: Array.from(available).sort(),
445
+ orphans: [],
446
+ };
447
+ }
435
448
  const installed = new Set(listSkillsInVersionHome(agent, version));
436
449
  const toAdd = [];
437
450
  const toUpdate = [];
@@ -110,8 +110,11 @@ function buildGeminiDetector() {
110
110
  return [];
111
111
  try {
112
112
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
113
- const allowed = settings?.tools?.allowed;
114
- if (Array.isArray(allowed) && allowed.length > 0) {
113
+ const core = settings?.tools?.core;
114
+ const exclude = settings?.tools?.exclude;
115
+ const hasCore = Array.isArray(core) && core.length > 0;
116
+ const hasExclude = Array.isArray(exclude) && exclude.length > 0;
117
+ if (hasCore || hasExclude) {
115
118
  return discoverPermissionGroups().map(g => g.name);
116
119
  }
117
120
  }
@@ -162,6 +165,28 @@ function buildGrokDetector() {
162
165
  },
163
166
  };
164
167
  }
168
+ function buildGooseDetector() {
169
+ return {
170
+ kind: 'permissions',
171
+ agent: 'goose',
172
+ list({ versionHome }) {
173
+ const permissionsPath = path.join(versionHome, '.config', 'goose', 'permission.yaml');
174
+ if (!fs.existsSync(permissionsPath))
175
+ return [];
176
+ try {
177
+ const config = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
178
+ const user = config?.user;
179
+ const count = (Array.isArray(user?.always_allow) ? user.always_allow.length : 0) +
180
+ (Array.isArray(user?.ask_before) ? user.ask_before.length : 0) +
181
+ (Array.isArray(user?.never_allow) ? user.never_allow.length : 0);
182
+ if (count > 0)
183
+ return discoverPermissionGroups().map(g => g.name);
184
+ }
185
+ catch { /* parse fail */ }
186
+ return [];
187
+ },
188
+ };
189
+ }
165
190
  function buildKimiDetector() {
166
191
  return {
167
192
  kind: 'permissions',
@@ -248,6 +273,7 @@ const handlers = {
248
273
  gemini: buildGeminiDetector,
249
274
  antigravity: buildAntigravityDetector,
250
275
  grok: buildGrokDetector,
276
+ goose: buildGooseDetector,
251
277
  kimi: buildKimiDetector,
252
278
  cursor: buildCursorDetector,
253
279
  droid: buildDroidDetector,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Subagents detector. Claude: flat .md files under `<agentDir>/agents/`.
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
5
  * OpenClaw: subdirectories containing AGENTS.md under `<versionHome>/.openclaw/`.
@@ -29,6 +29,9 @@ function buildClaudeDetector() {
29
29
  function buildGrokDetector() {
30
30
  return buildFlatMdAgentsDetector('grok', '.grok');
31
31
  }
32
+ function buildGeminiDetector() {
33
+ return buildFlatMdAgentsDetector('gemini', '.gemini');
34
+ }
32
35
  function buildCodexDetector() {
33
36
  return {
34
37
  kind: 'subagents',
@@ -128,13 +131,29 @@ function buildOpenCodeDetector() {
128
131
  },
129
132
  };
130
133
  }
134
+ function buildAntigravityDetector() {
135
+ return {
136
+ kind: 'subagents',
137
+ agent: 'antigravity',
138
+ list({ versionHome }) {
139
+ const agentsDir = path.join(versionHome, '.gemini', 'config', 'agents');
140
+ if (!fs.existsSync(agentsDir))
141
+ return [];
142
+ return fs.readdirSync(agentsDir, { withFileTypes: true })
143
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(agentsDir, d.name, 'agent.md')))
144
+ .map(d => d.name);
145
+ },
146
+ };
147
+ }
131
148
  const handlers = {
132
149
  claude: buildClaudeDetector,
133
150
  copilot: buildCopilotDetector,
151
+ gemini: buildGeminiDetector,
134
152
  grok: buildGrokDetector,
135
153
  codex: buildCodexDetector,
136
154
  kimi: buildKimiDetector,
137
155
  opencode: buildOpenCodeDetector,
156
+ antigravity: buildAntigravityDetector,
138
157
  droid: buildDroidDetector,
139
158
  openclaw: buildOpenclawDetector,
140
159
  kiro: buildKiroDetector,
@@ -3,7 +3,9 @@
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';
8
+ import * as yaml from 'yaml';
7
9
  import { capableAgents } from '../../capabilities.js';
8
10
  import { lazyAgentMap } from '../writers/lazy-map.js';
9
11
  function buildWorkflowsDetector(agent) {
@@ -11,6 +13,66 @@ function buildWorkflowsDetector(agent) {
11
13
  kind: 'workflows',
12
14
  agent,
13
15
  list({ versionHome }) {
16
+ if (agent === 'kimi') {
17
+ const skillsDir = path.join(versionHome, '.kimi-code', 'skills');
18
+ if (!fs.existsSync(skillsDir))
19
+ return [];
20
+ return fs.readdirSync(skillsDir, { withFileTypes: true })
21
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')))
22
+ .filter(d => {
23
+ try {
24
+ const skill = fs.readFileSync(path.join(skillsDir, d.name, 'SKILL.md'), 'utf-8');
25
+ const lines = skill.split('\n');
26
+ if (lines[0] !== '---')
27
+ return false;
28
+ const endIndex = lines.slice(1).findIndex(l => l === '---');
29
+ if (endIndex < 0)
30
+ return false;
31
+ const parsed = yaml.parse(lines.slice(1, endIndex + 1).join('\n'));
32
+ return parsed?.type === 'flow' && parsed.agents_workflow === d.name;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ })
38
+ .map(d => d.name);
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
+ }
68
+ if (agent === 'goose') {
69
+ const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
70
+ if (!fs.existsSync(recipesDir))
71
+ return [];
72
+ return fs.readdirSync(recipesDir, { withFileTypes: true })
73
+ .filter(d => d.isFile() && d.name.endsWith('.yaml') && !d.name.startsWith('.'))
74
+ .map(d => d.name.slice(0, -'.yaml'.length));
75
+ }
14
76
  const workflowsDir = path.join(versionHome, 'workflows');
15
77
  if (!fs.existsSync(workflowsDir))
16
78
  return [];
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Subagents writer. Claude flattens each subagent into a single .md file
3
- * under `<agentDir>/agents/`. Codex writes TOML under `.codex/agents/`.
2
+ * Subagents writer. Claude/Gemini/Grok flatten each subagent into a single
3
+ * .md file under their native agents directory. Codex writes TOML under
4
+ * `.codex/agents/`.
4
5
  * Droid (Factory AI) flattens each into a custom droid .md under
5
6
  * `<versionHome>/.factory/droids/`. OpenClaw copies the full subagent
6
7
  * directory (with AGENT.md renamed to AGENTS.md) into
@@ -13,7 +14,7 @@
13
14
  import * as fs from 'fs';
14
15
  import * as path from 'path';
15
16
  import { capableAgents } from '../../capabilities.js';
16
- import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, transformSubagentForOpenCode, transformSubagentForDroid, transformSubagentForKiro, syncSubagentToOpenclaw, parseSubagentFrontmatter, } from '../../subagents.js';
17
+ import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, transformSubagentForOpenCode, transformSubagentForAntigravity, transformSubagentForDroid, transformSubagentForKiro, syncSubagentToOpenclaw, parseSubagentFrontmatter, } from '../../subagents.js';
17
18
  import { safeJoin } from '../../paths.js';
18
19
  import { lazyAgentMap } from './lazy-map.js';
19
20
  function buildSubagentsWriter(agent) {
@@ -29,8 +30,9 @@ function buildSubagentsWriter(agent) {
29
30
  if (!sub)
30
31
  continue;
31
32
  try {
32
- if (agent === 'claude' || agent === 'grok') {
33
- const agentsDir = path.join(versionHome, agent === 'grok' ? '.grok' : '.claude', 'agents');
33
+ if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
34
+ const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
35
+ const agentsDir = path.join(versionHome, agentsRoot, 'agents');
34
36
  fs.mkdirSync(agentsDir, { recursive: true });
35
37
  fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForClaude(sub.path));
36
38
  synced.push(sub.name);
@@ -51,6 +53,12 @@ function buildSubagentsWriter(agent) {
51
53
  fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForOpenCode(sub.path));
52
54
  synced.push(sub.name);
53
55
  }
56
+ else if (agent === 'antigravity') {
57
+ const agentDir = safeJoin(path.join(versionHome, '.gemini', 'config', 'agents'), sub.name);
58
+ fs.mkdirSync(agentDir, { recursive: true });
59
+ fs.writeFileSync(safeJoin(agentDir, 'agent.md'), transformSubagentForAntigravity(sub.path));
60
+ synced.push(sub.name);
61
+ }
54
62
  else if (agent === 'droid') {
55
63
  const droidsDir = path.join(versionHome, '.factory', 'droids');
56
64
  fs.mkdirSync(droidsDir, { recursive: true });
@@ -68,6 +68,15 @@ 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 Antigravity's custom-agent markdown shape.
73
+ *
74
+ * Antigravity exposes custom agents as Markdown files with YAML frontmatter,
75
+ * close to Gemini CLI subagents. Keep portable frontmatter fields and flatten
76
+ * sibling markdown files into the prompt body like the other markdown-backed
77
+ * agents.
78
+ */
79
+ export declare function transformSubagentForAntigravity(subagentDir: string): string;
71
80
  /**
72
81
  * Transform a subagent into an OpenCode agent markdown file.
73
82
  *
@@ -157,7 +166,7 @@ export declare function removeSubagentFromAgent(subagentName: string, agent: Age
157
166
  export declare function subagentContentMatches(installedDir: string, sourceDir: string): boolean;
158
167
  /**
159
168
  * List subagents installed to a specific agent's home
160
- * Claude: scans ~/.claude/agents/{name}.md
169
+ * Claude/Gemini/Grok: scans ~/.{agent}/agents/{name}.md
161
170
  * Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
162
171
  * Kiro: scans ~/.kiro/agents/{name}.json
163
172
  * OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
@@ -274,6 +274,39 @@ export function transformSubagentForDroid(subagentDir) {
274
274
  * See GitHub docs for custom agents.
275
275
  */
276
276
  export const transformSubagentForCopilot = transformSubagentForDroid;
277
+ /**
278
+ * Transform a subagent into Antigravity's custom-agent markdown shape.
279
+ *
280
+ * Antigravity exposes custom agents as Markdown files with YAML frontmatter,
281
+ * close to Gemini CLI subagents. Keep portable frontmatter fields and flatten
282
+ * sibling markdown files into the prompt body like the other markdown-backed
283
+ * agents.
284
+ */
285
+ export function transformSubagentForAntigravity(subagentDir) {
286
+ const agentMd = path.join(subagentDir, 'AGENT.md');
287
+ const frontmatter = parseSubagentFrontmatter(agentMd);
288
+ const body = getSubagentBody(agentMd);
289
+ if (!frontmatter) {
290
+ throw new Error(`Invalid AGENT.md in ${subagentDir}`);
291
+ }
292
+ const frontmatterYaml = yaml.stringify({
293
+ name: frontmatter.name,
294
+ description: frontmatter.description,
295
+ kind: 'local',
296
+ ...(frontmatter.model && { model: frontmatter.model }),
297
+ }).trim();
298
+ let result = `---\n${frontmatterYaml}\n---\n\n${body}`;
299
+ const files = fs.readdirSync(subagentDir)
300
+ .filter(f => f.endsWith('.md') && f !== 'AGENT.md')
301
+ .sort();
302
+ for (const file of files) {
303
+ const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
304
+ const sectionName = file.replace('.md', '');
305
+ const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
306
+ result += `\n\n## ${title}\n\n${content}`;
307
+ }
308
+ return `${result.trim()}\n`;
309
+ }
277
310
  /**
278
311
  * Transform a subagent into an OpenCode agent markdown file.
279
312
  *
@@ -489,10 +522,10 @@ export function syncSubagentToOpenclaw(subagentDir, targetDir) {
489
522
  * Install a subagent to a specific agent's home
490
523
  */
491
524
  export function installSubagentToAgent(subagentDir, subagentName, agent, agentHome) {
492
- if (agent === 'claude' || agent === 'grok') {
493
- // Claude / Grok: flatten to single .md under ~/.claude/agents or ~/.grok/agents
494
- // Grok discovers user agent defs from ~/.grok/agents/*.md (same shape as Claude).
495
- const agentsDir = path.join(agentHome, agent === 'grok' ? '.grok' : '.claude', 'agents');
525
+ if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
526
+ // Claude / Gemini / Grok: flatten to single .md under the native agents dir.
527
+ const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
528
+ const agentsDir = path.join(agentHome, agentsRoot, 'agents');
496
529
  if (!fs.existsSync(agentsDir)) {
497
530
  fs.mkdirSync(agentsDir, { recursive: true });
498
531
  }
@@ -543,6 +576,19 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
543
576
  return { success: false, error: String(err) };
544
577
  }
545
578
  }
579
+ else if (agent === 'antigravity') {
580
+ // Antigravity: custom-agent markdown under ~/.gemini/config/agents/<name>/agent.md.
581
+ const agentDir = safeJoin(path.join(agentHome, '.gemini', 'config', 'agents'), subagentName);
582
+ if (!fs.existsSync(agentDir))
583
+ fs.mkdirSync(agentDir, { recursive: true });
584
+ try {
585
+ fs.writeFileSync(safeJoin(agentDir, 'agent.md'), transformSubagentForAntigravity(subagentDir));
586
+ return { success: true };
587
+ }
588
+ catch (err) {
589
+ return { success: false, error: String(err) };
590
+ }
591
+ }
546
592
  else if (agent === 'openclaw') {
547
593
  // OpenClaw: copy full directory
548
594
  const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
@@ -573,8 +619,8 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
573
619
  */
574
620
  export function removeSubagentFromAgent(subagentName, agent, agentHome) {
575
621
  try {
576
- if (agent === 'claude' || agent === 'grok') {
577
- const agentsRoot = agent === 'grok' ? '.grok' : '.claude';
622
+ if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
623
+ const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
578
624
  const targetPath = safeJoin(path.join(agentHome, agentsRoot, 'agents'), `${subagentName}.md`);
579
625
  if (fs.existsSync(targetPath)) {
580
626
  fs.unlinkSync(targetPath);
@@ -604,6 +650,12 @@ export function removeSubagentFromAgent(subagentName, agent, agentHome) {
604
650
  fs.unlinkSync(targetPath);
605
651
  return { success: true };
606
652
  }
653
+ else if (agent === 'antigravity') {
654
+ const targetDir = safeJoin(path.join(agentHome, '.gemini', 'config', 'agents'), subagentName);
655
+ if (fs.existsSync(targetDir))
656
+ fs.rmSync(targetDir, { recursive: true, force: true });
657
+ return { success: true };
658
+ }
607
659
  else if (agent === 'openclaw') {
608
660
  const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
609
661
  if (fs.existsSync(targetDir)) {
@@ -655,16 +707,17 @@ export function subagentContentMatches(installedDir, sourceDir) {
655
707
  // source of truth.
656
708
  /**
657
709
  * List subagents installed to a specific agent's home
658
- * Claude: scans ~/.claude/agents/{name}.md
710
+ * Claude/Gemini/Grok: scans ~/.{agent}/agents/{name}.md
659
711
  * Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
660
712
  * Kiro: scans ~/.kiro/agents/{name}.json
661
713
  * OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
662
714
  */
663
715
  export function listSubagentsForAgent(agentId, home) {
664
716
  const subagents = [];
665
- if (agentId === 'claude' || agentId === 'grok') {
666
- // Claude / Grok: flat .md files in agents/
667
- const agentsDir = path.join(home, agentId === 'grok' ? '.grok' : '.claude', 'agents');
717
+ if (agentId === 'claude' || agentId === 'gemini' || agentId === 'grok') {
718
+ // Claude / Gemini / Grok: flat .md files in agents/
719
+ const agentsRoot = agentId === 'grok' ? '.grok' : agentId === 'gemini' ? '.gemini' : '.claude';
720
+ const agentsDir = path.join(home, agentsRoot, 'agents');
668
721
  if (!fs.existsSync(agentsDir))
669
722
  return subagents;
670
723
  for (const file of fs.readdirSync(agentsDir)) {
@@ -730,6 +783,20 @@ export function listSubagentsForAgent(agentId, home) {
730
783
  subagents.push({ name, path: filePath, files: [file], frontmatter });
731
784
  }
732
785
  }
786
+ else if (agentId === 'antigravity') {
787
+ const agentsDir = path.join(home, '.gemini', 'config', 'agents');
788
+ if (!fs.existsSync(agentsDir))
789
+ return subagents;
790
+ for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
791
+ if (!entry.isDirectory())
792
+ continue;
793
+ const filePath = path.join(agentsDir, entry.name, 'agent.md');
794
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
795
+ continue;
796
+ const frontmatter = parseSubagentFrontmatter(filePath) ?? { name: entry.name, description: '' };
797
+ subagents.push({ name: entry.name, path: filePath, files: ['agent.md'], frontmatter });
798
+ }
799
+ }
733
800
  else if (agentId === 'copilot') {
734
801
  // Copilot: flat `<name>.agent.md` files under ~/.copilot/agents/
735
802
  const agentsDir = path.join(home, '.copilot', 'agents');
@@ -840,8 +907,9 @@ export function diffVersionSubagents(agent, version) {
840
907
  }
841
908
  }
842
909
  // Check what's installed
843
- if (agent === 'claude' || agent === 'grok') {
844
- const agentsDir = path.join(versionHome, agent === 'grok' ? '.grok' : '.claude', 'agents');
910
+ if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
911
+ const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
912
+ const agentsDir = path.join(versionHome, agentsRoot, 'agents');
845
913
  if (fs.existsSync(agentsDir)) {
846
914
  for (const file of fs.readdirSync(agentsDir)) {
847
915
  if (!file.endsWith('.md'))
@@ -878,6 +946,19 @@ export function diffVersionSubagents(agent, version) {
878
946
  }
879
947
  }
880
948
  }
949
+ else if (agent === 'antigravity') {
950
+ const agentsDir = path.join(versionHome, '.gemini', 'config', 'agents');
951
+ if (fs.existsSync(agentsDir)) {
952
+ for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
953
+ if (!entry.isDirectory())
954
+ continue;
955
+ if (!fs.existsSync(path.join(agentsDir, entry.name, 'agent.md')))
956
+ continue;
957
+ if (!discovered.has(entry.name))
958
+ orphans.push(entry.name);
959
+ }
960
+ }
961
+ }
881
962
  else if (agent === 'openclaw') {
882
963
  const openclawDir = path.join(versionHome, '.openclaw');
883
964
  if (fs.existsSync(openclawDir)) {
@@ -932,8 +1013,8 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
932
1013
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
933
1014
  const trashDir = path.join(getTrashSubagentsDir(), agent, version, subagentName);
934
1015
  try {
935
- if (agent === 'claude' || agent === 'grok') {
936
- const agentsRoot = agent === 'grok' ? '.grok' : '.claude';
1016
+ if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
1017
+ const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
937
1018
  const targetPath = path.join(versionHome, agentsRoot, 'agents', `${subagentName}.md`);
938
1019
  if (fs.existsSync(targetPath)) {
939
1020
  fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
@@ -961,6 +1042,13 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
961
1042
  fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
962
1043
  }
963
1044
  }
1045
+ else if (agent === 'antigravity') {
1046
+ const targetDir = path.join(versionHome, '.gemini', 'config', 'agents', subagentName);
1047
+ if (fs.existsSync(targetDir)) {
1048
+ fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
1049
+ fs.renameSync(targetDir, path.join(trashDir, stamp));
1050
+ }
1051
+ }
964
1052
  else if (agent === 'copilot') {
965
1053
  const targetPath = path.join(versionHome, '.copilot', 'agents', `${subagentName}.agent.md`);
966
1054
  if (fs.existsSync(targetPath)) {
@@ -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 = 4;
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 one killed without
158
- * launching a second tmux client against the same socket. A bare `kill-pane`
159
- * relied on the hook context supplying a "current pane", while an external
160
- * self-client could race the server under load. Single source of truth: both
161
- * the spawn-wrap (exec.ts) and the daemon reconcile build the hook here, so the
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;
@@ -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 = 4;
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 one killed without
308
- * launching a second tmux client against the same socket. A bare `kill-pane`
309
- * relied on the hook context supplying a "current pane", while an external
310
- * self-client could race the server under load. Single source of truth: both
311
- * the spawn-wrap (exec.ts) and the daemon reconcile build the hook here, so the
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) {