@phnx-labs/agents-cli 1.14.2 → 1.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +17 -7
  2. package/dist/commands/browser.d.ts +2 -0
  3. package/dist/commands/browser.js +388 -0
  4. package/dist/commands/daemon.js +1 -1
  5. package/dist/commands/doctor.d.ts +16 -9
  6. package/dist/commands/doctor.js +248 -12
  7. package/dist/commands/prune.js +9 -3
  8. package/dist/commands/refresh-rules.d.ts +15 -0
  9. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  10. package/dist/commands/routines.js +1 -1
  11. package/dist/commands/rules.js +100 -4
  12. package/dist/commands/secrets.js +198 -11
  13. package/dist/commands/sync.js +19 -0
  14. package/dist/commands/teams.js +162 -22
  15. package/dist/commands/trash.d.ts +10 -0
  16. package/dist/commands/trash.js +187 -0
  17. package/dist/commands/view.js +46 -13
  18. package/dist/index.js +62 -4
  19. package/dist/lib/agents.js +2 -2
  20. package/dist/lib/browser/cdp.d.ts +24 -0
  21. package/dist/lib/browser/cdp.js +94 -0
  22. package/dist/lib/browser/chrome.d.ts +16 -0
  23. package/dist/lib/browser/chrome.js +157 -0
  24. package/dist/lib/browser/drivers/local.d.ts +8 -0
  25. package/dist/lib/browser/drivers/local.js +22 -0
  26. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  27. package/dist/lib/browser/drivers/ssh.js +129 -0
  28. package/dist/lib/browser/index.d.ts +5 -0
  29. package/dist/lib/browser/index.js +5 -0
  30. package/dist/lib/browser/input.d.ts +6 -0
  31. package/dist/lib/browser/input.js +52 -0
  32. package/dist/lib/browser/ipc.d.ts +12 -0
  33. package/dist/lib/browser/ipc.js +223 -0
  34. package/dist/lib/browser/profiles.d.ts +11 -0
  35. package/dist/lib/browser/profiles.js +61 -0
  36. package/dist/lib/browser/refs.d.ts +21 -0
  37. package/dist/lib/browser/refs.js +88 -0
  38. package/dist/lib/browser/service.d.ts +45 -0
  39. package/dist/lib/browser/service.js +404 -0
  40. package/dist/lib/browser/types.d.ts +73 -0
  41. package/dist/lib/browser/types.js +7 -0
  42. package/dist/lib/cloud/codex.js +1 -1
  43. package/dist/lib/cloud/registry.js +2 -2
  44. package/dist/lib/cloud/rush.js +2 -2
  45. package/dist/lib/cloud/store.js +2 -2
  46. package/dist/lib/daemon.d.ts +1 -1
  47. package/dist/lib/daemon.js +47 -11
  48. package/dist/lib/diff-text.d.ts +25 -0
  49. package/dist/lib/diff-text.js +47 -0
  50. package/dist/lib/doctor-diff.d.ts +64 -0
  51. package/dist/lib/doctor-diff.js +497 -0
  52. package/dist/lib/git.js +3 -3
  53. package/dist/lib/hooks.d.ts +6 -0
  54. package/dist/lib/hooks.js +6 -1
  55. package/dist/lib/migrate.js +77 -0
  56. package/dist/lib/pty-client.js +3 -3
  57. package/dist/lib/pty-server.js +36 -7
  58. package/dist/lib/resources.js +1 -1
  59. package/dist/lib/rotate.d.ts +8 -1
  60. package/dist/lib/rotate.js +17 -4
  61. package/dist/lib/rules/compile.d.ts +104 -0
  62. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  63. package/dist/lib/rules/compose.d.ts +78 -0
  64. package/dist/lib/rules/compose.js +170 -0
  65. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  66. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  67. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  68. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  69. package/dist/lib/secrets/bundles.d.ts +61 -4
  70. package/dist/lib/secrets/bundles.js +222 -54
  71. package/dist/lib/secrets/index.d.ts +24 -5
  72. package/dist/lib/secrets/index.js +70 -41
  73. package/dist/lib/session/active.js +5 -5
  74. package/dist/lib/session/db.js +4 -4
  75. package/dist/lib/session/discover.js +2 -2
  76. package/dist/lib/session/render.js +21 -7
  77. package/dist/lib/shims.d.ts +28 -4
  78. package/dist/lib/shims.js +72 -14
  79. package/dist/lib/state.d.ts +22 -28
  80. package/dist/lib/state.js +83 -76
  81. package/dist/lib/sync-manifest.d.ts +2 -2
  82. package/dist/lib/sync-manifest.js +5 -5
  83. package/dist/lib/teams/agents.d.ts +4 -2
  84. package/dist/lib/teams/agents.js +11 -4
  85. package/dist/lib/teams/api.d.ts +1 -1
  86. package/dist/lib/teams/api.js +2 -2
  87. package/dist/lib/teams/index.d.ts +1 -0
  88. package/dist/lib/teams/index.js +1 -0
  89. package/dist/lib/teams/persistence.js +3 -3
  90. package/dist/lib/teams/registry.d.ts +8 -1
  91. package/dist/lib/teams/registry.js +8 -2
  92. package/dist/lib/teams/worktree.d.ts +30 -0
  93. package/dist/lib/teams/worktree.js +96 -0
  94. package/dist/lib/types.d.ts +12 -6
  95. package/dist/lib/types.js +3 -3
  96. package/dist/lib/versions.d.ts +30 -2
  97. package/dist/lib/versions.js +127 -105
  98. package/package.json +1 -1
  99. package/scripts/postinstall.js +29 -0
  100. package/dist/commands/refresh-memory.d.ts +0 -15
  101. package/dist/lib/memory-compile.d.ts +0 -66
package/dist/lib/state.js CHANGED
@@ -2,9 +2,11 @@
2
2
  * Filesystem layout and persistent state for agents-cli.
3
3
  *
4
4
  * Two roots:
5
- * - ~/.agents-system/ — system repo (npm-shipped resources, versions, shims, backups, runs)
5
+ * - ~/.agents-system/ — system repo (npm-shipped resources and canonical read-side defaults)
6
6
  * - ~/.agents/ — user repo (user-authored commands, skills, hooks, rules, mcp,
7
- * permissions, subagents, profiles, secrets, agents.yaml)
7
+ * permissions, subagents, profiles, secrets, agents.yaml,
8
+ * packages, routines, runs, versions, shims, backups, plugins,
9
+ * drive, trash)
8
10
  *
9
11
  * Resolution precedence for resources: project > user > system.
10
12
  * Every module that needs a path or reads/writes agents.yaml goes through here.
@@ -29,23 +31,22 @@ const SYSTEM_COMMANDS_DIR = path.join(SYSTEM_AGENTS_DIR, 'commands');
29
31
  const SYSTEM_HOOKS_DIR = path.join(SYSTEM_AGENTS_DIR, 'hooks');
30
32
  const SYSTEM_SKILLS_DIR = path.join(SYSTEM_AGENTS_DIR, 'skills');
31
33
  const SYSTEM_RULES_DIR = path.join(SYSTEM_AGENTS_DIR, 'rules');
32
- const SYSTEM_LEGACY_MEMORY_DIR = path.join(SYSTEM_AGENTS_DIR, 'memory');
33
34
  const SYSTEM_MCP_DIR = path.join(SYSTEM_AGENTS_DIR, 'mcp');
34
35
  const SYSTEM_PERMISSIONS_DIR = path.join(SYSTEM_AGENTS_DIR, 'permissions');
35
36
  const SYSTEM_SUBAGENTS_DIR = path.join(SYSTEM_AGENTS_DIR, 'subagents');
36
- const SYSTEM_SECRETS_DIR = path.join(SYSTEM_AGENTS_DIR, 'secrets');
37
37
  const SYSTEM_PROMPTCUTS_FILE = path.join(SYSTEM_AGENTS_DIR, 'hooks', 'promptcuts.yaml');
38
38
  const SYSTEM_MCP_CONFIG_FILE = path.join(SYSTEM_AGENTS_DIR, 'mcp.json');
39
39
  const SYSTEM_INSTRUCTIONS_FILE = path.join(SYSTEM_AGENTS_DIR, 'instructions.md');
40
- // System-only paths (never duplicated in user repo)
41
- const PACKAGES_DIR = path.join(SYSTEM_AGENTS_DIR, 'packages');
42
- const ROUTINES_DIR = path.join(SYSTEM_AGENTS_DIR, 'routines');
43
- const RUNS_DIR = path.join(SYSTEM_AGENTS_DIR, 'runs');
44
- const VERSIONS_DIR = path.join(SYSTEM_AGENTS_DIR, 'versions');
45
- const SHIMS_DIR = path.join(SYSTEM_AGENTS_DIR, 'shims');
46
- const BACKUPS_DIR = path.join(SYSTEM_AGENTS_DIR, 'backups');
47
- const PLUGINS_DIR = path.join(SYSTEM_AGENTS_DIR, 'plugins');
48
- const DRIVE_DIR = path.join(SYSTEM_AGENTS_DIR, 'drive');
40
+ // User-level operational state
41
+ const PACKAGES_DIR = path.join(USER_AGENTS_DIR, 'packages');
42
+ const ROUTINES_DIR = path.join(USER_AGENTS_DIR, 'routines');
43
+ const RUNS_DIR = path.join(USER_AGENTS_DIR, 'runs');
44
+ const VERSIONS_DIR = path.join(USER_AGENTS_DIR, 'versions');
45
+ const SHIMS_DIR = path.join(USER_AGENTS_DIR, 'shims');
46
+ const BACKUPS_DIR = path.join(USER_AGENTS_DIR, 'backups');
47
+ const PLUGINS_DIR = path.join(USER_AGENTS_DIR, 'plugins');
48
+ const DRIVE_DIR = path.join(USER_AGENTS_DIR, 'drive');
49
+ const TRASH_DIR = path.join(USER_AGENTS_DIR, 'trash');
49
50
  // ─── User resource dirs ───────────────────────────────────────────────────────
50
51
  const USER_COMMANDS_DIR = path.join(USER_AGENTS_DIR, 'commands');
51
52
  const USER_HOOKS_DIR = path.join(USER_AGENTS_DIR, 'hooks');
@@ -62,7 +63,7 @@ const META_HEADER = `# agents-cli metadata
62
63
 
63
64
  `;
64
65
  // ─── Root getters ─────────────────────────────────────────────────────────────
65
- /** Root of the system data directory (~/.agents-system/). Legacy alias — prefer getSystemAgentsDir(). */
66
+ /** Root of the system data directory (~/.agents-system/). */
66
67
  export function getAgentsDir() {
67
68
  return SYSTEM_AGENTS_DIR;
68
69
  }
@@ -138,44 +139,14 @@ export function getHooksDir() { return SYSTEM_HOOKS_DIR; }
138
139
  export function getSkillsDir() { return SYSTEM_SKILLS_DIR; }
139
140
  /** Path to the canonical rules directory — system repo. */
140
141
  export function getRulesDir() { return SYSTEM_RULES_DIR; }
141
- /** Back-compat export; resolves to system rules dir. */
142
- export function getMemoryDir() { return SYSTEM_RULES_DIR; }
143
- let legacyMemoryWarned = false;
144
- /**
145
- * Read-side resolution for the canonical rules dir.
146
- *
147
- * Returns SYSTEM_RULES_DIR normally. Falls back to the legacy
148
- * SYSTEM_LEGACY_MEMORY_DIR (~/.agents-system/memory/) only when the upstream
149
- * still uses the old layout and the user hasn't pulled the rename yet —
150
- * detected by absence of rules/AGENTS.md and presence of memory/AGENTS.md.
151
- *
152
- * Prints a single warning per process the first time the fallback fires.
153
- * Per the read-only system-repo invariant, this never moves files; the rename
154
- * is applied when the user pulls upstream.
155
- */
156
- export function getResolvedRulesDir() {
157
- const rulesAgents = path.join(SYSTEM_RULES_DIR, 'AGENTS.md');
158
- const legacyAgents = path.join(SYSTEM_LEGACY_MEMORY_DIR, 'AGENTS.md');
159
- if (fs.existsSync(rulesAgents))
160
- return SYSTEM_RULES_DIR;
161
- if (fs.existsSync(legacyAgents)) {
162
- if (!legacyMemoryWarned) {
163
- legacyMemoryWarned = true;
164
- process.stderr.write('agents-cli: Legacy memory/ directory detected — agents-cli still works, ' +
165
- "but run 'agents repo pull system' to migrate to rules/.\n");
166
- }
167
- return SYSTEM_LEGACY_MEMORY_DIR;
168
- }
169
- return SYSTEM_RULES_DIR;
170
- }
142
+ /** Read-side resolution for the canonical rules dir — system repo. */
143
+ export function getResolvedRulesDir() { return SYSTEM_RULES_DIR; }
171
144
  /** Path to MCP server YAML configs — system repo. */
172
145
  export function getMcpDir() { return SYSTEM_MCP_DIR; }
173
146
  /** Path to permission group YAML files — system repo. */
174
147
  export function getPermissionsDir() { return SYSTEM_PERMISSIONS_DIR; }
175
148
  /** Path to subagent definition directories — system repo. */
176
149
  export function getSubagentsDir() { return SYSTEM_SUBAGENTS_DIR; }
177
- /** Path to encrypted secret bundles — system repo. */
178
- export function getSecretsDir() { return SYSTEM_SECRETS_DIR; }
179
150
  /** Path to ~/.agents-system/promptcuts.yaml. */
180
151
  export function getPromptcutsPath() { return SYSTEM_PROMPTCUTS_FILE; }
181
152
  /** Path to the legacy MCP config JSON. */
@@ -190,7 +161,6 @@ export function getSystemRulesDir() { return SYSTEM_RULES_DIR; }
190
161
  export function getSystemMcpDir() { return SYSTEM_MCP_DIR; }
191
162
  export function getSystemPermissionsDir() { return SYSTEM_PERMISSIONS_DIR; }
192
163
  export function getSystemSubagentsDir() { return SYSTEM_SUBAGENTS_DIR; }
193
- export function getSystemSecretsDir() { return SYSTEM_SECRETS_DIR; }
194
164
  export function getSystemPromptcutsPath() { return SYSTEM_PROMPTCUTS_FILE; }
195
165
  // ─── User resource getters ────────────────────────────────────────────────────
196
166
  export function getUserCommandsDir() { return USER_COMMANDS_DIR; }
@@ -202,23 +172,27 @@ export function getUserPermissionsDir() { return USER_PERMISSIONS_DIR; }
202
172
  export function getUserSubagentsDir() { return USER_SUBAGENTS_DIR; }
203
173
  export function getUserSecretsDir() { return USER_SECRETS_DIR; }
204
174
  export function getUserPromptcutsPath() { return USER_PROMPTCUTS_FILE; }
205
- // ─── System-only path getters ─────────────────────────────────────────────────
206
- /** Path to cloned packages (~/.agents-system/packages/). */
175
+ // ─── User operational path getters ────────────────────────────────────────────
176
+ /** Path to cloned packages (~/.agents/packages/). */
207
177
  export function getPackagesDir() { return PACKAGES_DIR; }
208
- /** Path to routine YAML definitions (~/.agents-system/routines/). */
178
+ /** Path to routine YAML definitions (~/.agents/routines/). */
209
179
  export function getRoutinesDir() { return ROUTINES_DIR; }
210
- /** Path to routine execution logs (~/.agents-system/runs/). */
180
+ /** Path to routine execution logs (~/.agents/runs/). */
211
181
  export function getRunsDir() { return RUNS_DIR; }
212
- /** Path to installed agent CLI binaries (~/.agents-system/versions/). */
182
+ /** Path to installed agent CLI binaries (~/.agents/versions/). */
213
183
  export function getVersionsDir() { return VERSIONS_DIR; }
214
- /** Path to version-switching shim scripts (~/.agents-system/shims/). */
184
+ /** Path to version-switching shim scripts (~/.agents/shims/). */
215
185
  export function getShimsDir() { return SHIMS_DIR; }
216
- /** Path to config backups (~/.agents-system/backups/). */
186
+ /** Path to config backups (~/.agents/backups/). */
217
187
  export function getBackupsDir() { return BACKUPS_DIR; }
218
- /** Path to plugin bundles (~/.agents-system/plugins/). */
188
+ /** Path to plugin bundles (~/.agents/plugins/). */
219
189
  export function getPluginsDir() { return PLUGINS_DIR; }
220
- /** Path to synced remote session data (~/.agents-system/drive/). */
190
+ /** Path to synced remote session data (~/.agents/drive/). */
221
191
  export function getDriveDir() { return DRIVE_DIR; }
192
+ /** Path to soft-deleted resources (~/.agents/trash/). */
193
+ export function getTrashDir() { return TRASH_DIR; }
194
+ /** Path to soft-deleted version dirs (~/.agents/trash/versions/). */
195
+ export function getTrashVersionsDir() { return path.join(TRASH_DIR, 'versions'); }
222
196
  /**
223
197
  * Path to a single user-level extra DotAgent repo clone (~/.agents-<alias>/).
224
198
  *
@@ -264,7 +238,7 @@ export function ensureAgentsDir() {
264
238
  fs.chmodSync(USER_AGENTS_DIR, 0o700);
265
239
  }
266
240
  catch { }
267
- // System repo
241
+ // System repo plus user-level operational state
268
242
  if (!fs.existsSync(SYSTEM_AGENTS_DIR)) {
269
243
  fs.mkdirSync(SYSTEM_AGENTS_DIR, opts);
270
244
  }
@@ -351,8 +325,8 @@ export function readMeta() {
351
325
  ensureAgentsDir();
352
326
  // NOTE: agents.yaml migration from ~/.agents-system/ to ~/.agents/ is handled
353
327
  // exclusively by runMigration() in migrate.ts, called from postinstall and
354
- // explicit command-time fallbacks (agents view/use/pull). Calling it here
355
- // would mutate real-user filesystem state during test runs that import this
328
+ // from a one-shot bootstrap step in src/index.ts. Calling it here would
329
+ // mutate real-user filesystem state during test runs that import this
356
330
  // module, causing cross-test pollution.
357
331
  // Legacy migration: check for old meta.yaml in system dir
358
332
  const oldMetaFile = path.join(SYSTEM_AGENTS_DIR, 'meta.yaml');
@@ -384,29 +358,45 @@ export function readMeta() {
384
358
  /* meta.yaml migration failed */
385
359
  }
386
360
  }
387
- if (fs.existsSync(META_FILE)) {
388
- let mtime = 0;
361
+ // Merge agents.yaml from both system and user repos. User repo wins on conflicts.
362
+ let systemMeta = null;
363
+ let userMeta = null;
364
+ if (fs.existsSync(SYSTEM_META_FILE)) {
389
365
  try {
390
- mtime = fs.statSync(META_FILE).mtimeMs;
391
- }
392
- catch { /* file vanished */ }
393
- if (metaCache && metaCache.mtime === mtime) {
394
- return metaCache.meta;
366
+ const content = fs.readFileSync(SYSTEM_META_FILE, 'utf-8');
367
+ systemMeta = yaml.parse(content);
395
368
  }
369
+ catch { /* ignore */ }
370
+ }
371
+ if (fs.existsSync(META_FILE)) {
396
372
  try {
397
373
  const content = fs.readFileSync(META_FILE, 'utf-8');
398
- const parsed = yaml.parse(content);
399
- const meta = parsed || createDefaultMeta();
400
- if (applyRegistrySeeds(meta)) {
401
- writeMeta(meta);
402
- return meta;
403
- }
404
- metaCache = { mtime, meta };
405
- return meta;
374
+ userMeta = yaml.parse(content);
406
375
  }
407
- catch {
408
- return createDefaultMeta();
376
+ catch { /* ignore */ }
377
+ }
378
+ if (systemMeta || userMeta) {
379
+ // Merge: system as base, user overwrites
380
+ const base = createDefaultMeta();
381
+ const meta = {
382
+ ...base,
383
+ ...systemMeta,
384
+ ...userMeta,
385
+ agents: { ...systemMeta?.agents, ...userMeta?.agents },
386
+ };
387
+ // Merge registries carefully to preserve type
388
+ if (systemMeta?.registries || userMeta?.registries) {
389
+ meta.registries = {
390
+ ...base.registries,
391
+ ...systemMeta?.registries,
392
+ ...userMeta?.registries,
393
+ };
394
+ }
395
+ if (applyRegistrySeeds(meta)) {
396
+ writeMeta(meta);
397
+ return meta;
409
398
  }
399
+ return meta;
410
400
  }
411
401
  const meta = createDefaultMeta();
412
402
  if (applyRegistrySeeds(meta)) {
@@ -469,3 +459,20 @@ export function clearVersionResources(agent, version) {
469
459
  writeMeta(meta);
470
460
  }
471
461
  }
462
+ /** Active rules preset for an agent@version. Defaults to "default" when unset. */
463
+ export function getActiveRulesPreset(agent, version) {
464
+ const meta = readMeta();
465
+ return meta.versions?.[agent]?.[version]?.rulesPreset || 'default';
466
+ }
467
+ /** Persist the active rules preset for an agent@version. */
468
+ export function setActiveRulesPreset(agent, version, preset) {
469
+ const meta = readMeta();
470
+ if (!meta.versions)
471
+ meta.versions = {};
472
+ if (!meta.versions[agent])
473
+ meta.versions[agent] = {};
474
+ if (!meta.versions[agent][version])
475
+ meta.versions[agent][version] = {};
476
+ meta.versions[agent][version].rulesPreset = preset;
477
+ writeMeta(meta);
478
+ }
@@ -74,8 +74,8 @@ export declare function buildManifest(agent: AgentId, version: string, available
74
74
  * Returns true (stale) at the first detected mismatch — no need to scan everything.
75
75
  * Returns false (clean) only after all checks pass.
76
76
  *
77
- * For rules, also delegates to isMemoryStale() to catch @-import changes
78
- * for agents that pre-compile their memory file.
77
+ * For rules, also delegates to isRulesStale() to catch @-import changes
78
+ * for agents that pre-compile their rules file.
79
79
  */
80
80
  export declare function isSyncStale(manifest: SyncManifest, available: AvailableResources, agent: AgentId, version: string, cwd: string): boolean;
81
81
  export {};
@@ -27,7 +27,7 @@ import * as crypto from 'crypto';
27
27
  import { getVersionsDir, getProjectAgentsDir, getUserAgentsDir, getSkillsDir, getUserHooksDir, getHooksDir, getUserRulesDir, getResolvedRulesDir, getUserPermissionsDir, getPermissionsDir, getEnabledExtraRepos, } from './state.js';
28
28
  import { resolveResource } from './resources.js';
29
29
  import { listMcpServerConfigs } from './mcp.js';
30
- import { isMemoryStale } from './memory-compile.js';
30
+ import { isRulesStale } from './rules/compile.js';
31
31
  import { getActivePermissionSetName } from './permissions.js';
32
32
  import { listInstalledSubagents } from './subagents.js';
33
33
  import { safeJoin } from './paths.js';
@@ -351,8 +351,8 @@ export function buildManifest(agent, version, available, cwd) {
351
351
  * Returns true (stale) at the first detected mismatch — no need to scan everything.
352
352
  * Returns false (clean) only after all checks pass.
353
353
  *
354
- * For rules, also delegates to isMemoryStale() to catch @-import changes
355
- * for agents that pre-compile their memory file.
354
+ * For rules, also delegates to isRulesStale() to catch @-import changes
355
+ * for agents that pre-compile their rules file.
356
356
  */
357
357
  export function isSyncStale(manifest, available, agent, version, cwd) {
358
358
  // ── Commands ──────────────────────────────────────────────────────────────
@@ -395,7 +395,7 @@ export function isSyncStale(manifest, available, agent, version, cwd) {
395
395
  if (!entry || isFileStale(entry.source, hookPath))
396
396
  return true;
397
397
  }
398
- // ── Rules/memory ──────────────────────────────────────────────────────────
398
+ // ── Rules ─────────────────────────────────────────────────────────────────
399
399
  if (nameSetDiffers(Object.keys(manifest.rules.files), available.memory))
400
400
  return true;
401
401
  for (const name of available.memory) {
@@ -407,7 +407,7 @@ export function isSyncStale(manifest, available, agent, version, cwd) {
407
407
  return true;
408
408
  }
409
409
  // Also catch @-import changes for non-native-import agents
410
- if (isMemoryStale(agent, version))
410
+ if (isRulesStale(agent, version))
411
411
  return true;
412
412
  // ── MCP ───────────────────────────────────────────────────────────────────
413
413
  const mcpServers = listMcpServerConfigs(cwd);
@@ -97,10 +97,12 @@ export declare class AgentProcess {
97
97
  taskType: TaskType | null;
98
98
  cloudRepo: string | null;
99
99
  cloudBranch: string | null;
100
+ worktreeName: string | null;
101
+ worktreePath: string | null;
100
102
  private eventsCache;
101
103
  private lastReadPos;
102
104
  private baseDir;
103
- constructor(agentId: string, taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode, pid?: number | null, status?: AgentStatus, startedAt?: Date, completedAt?: Date | null, baseDir?: string | null, parentSessionId?: string | null, workspaceDir?: string | null, cloudSessionId?: string | null, cloudProvider?: string | null, prUrl?: string | null, version?: string | null, remoteSessionId?: string | null, name?: string | null, after?: string[], effort?: EffortLevel | null, model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudRepo?: string | null, cloudBranch?: string | null);
105
+ constructor(agentId: string, taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode, pid?: number | null, status?: AgentStatus, startedAt?: Date, completedAt?: Date | null, baseDir?: string | null, parentSessionId?: string | null, workspaceDir?: string | null, cloudSessionId?: string | null, cloudProvider?: string | null, prUrl?: string | null, version?: string | null, remoteSessionId?: string | null, name?: string | null, after?: string[], effort?: EffortLevel | null, model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null);
104
106
  get isEditMode(): boolean;
105
107
  getAgentDir(): Promise<string>;
106
108
  /**
@@ -191,7 +193,7 @@ export declare class AgentManager {
191
193
  */
192
194
  rescanFromDisk(): Promise<number>;
193
195
  private loadExistingAgents;
194
- spawn(taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode | null, effort?: EffortLevel, parentSessionId?: string | null, workspaceDir?: string | null, version?: string | null, name?: string | null, after?: string[], model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudProvider?: string | null, cloudSessionId?: string | null, cloudRepo?: string | null, cloudBranch?: string | null): Promise<AgentProcess>;
196
+ spawn(taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode | null, effort?: EffortLevel, parentSessionId?: string | null, workspaceDir?: string | null, version?: string | null, name?: string | null, after?: string[], model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudProvider?: string | null, cloudSessionId?: string | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null): Promise<AgentProcess>;
195
197
  /**
196
198
  * Actually spawn the OS process for a teammate. Extracted from spawn() so
197
199
  * staged teammates can be launched later by startReady().
@@ -468,10 +468,13 @@ export class AgentProcess {
468
468
  // options the user originally supplied.
469
469
  cloudRepo = null;
470
470
  cloudBranch = null;
471
+ // Worktree isolation: when non-null, this teammate runs in its own git worktree.
472
+ worktreeName = null;
473
+ worktreePath = null;
471
474
  eventsCache = [];
472
475
  lastReadPos = 0;
473
476
  baseDir = null;
474
- constructor(agentId, taskName, agentType, prompt, cwd = null, mode = 'plan', pid = null, status = AgentStatus.RUNNING, startedAt = new Date(), completedAt = null, baseDir = null, parentSessionId = null, workspaceDir = null, cloudSessionId = null, cloudProvider = null, prUrl = null, version = null, remoteSessionId = null, name = null, after = [], effort = null, model = null, envOverrides = null, taskType = null, cloudRepo = null, cloudBranch = null) {
477
+ constructor(agentId, taskName, agentType, prompt, cwd = null, mode = 'plan', pid = null, status = AgentStatus.RUNNING, startedAt = new Date(), completedAt = null, baseDir = null, parentSessionId = null, workspaceDir = null, cloudSessionId = null, cloudProvider = null, prUrl = null, version = null, remoteSessionId = null, name = null, after = [], effort = null, model = null, envOverrides = null, taskType = null, cloudRepo = null, cloudBranch = null, worktreeName = null, worktreePath = null) {
475
478
  this.agentId = agentId;
476
479
  this.remoteSessionId = remoteSessionId;
477
480
  this.name = name;
@@ -482,6 +485,8 @@ export class AgentProcess {
482
485
  this.taskType = taskType;
483
486
  this.cloudRepo = cloudRepo;
484
487
  this.cloudBranch = cloudBranch;
488
+ this.worktreeName = worktreeName;
489
+ this.worktreePath = worktreePath;
485
490
  this.taskName = taskName;
486
491
  this.agentType = agentType;
487
492
  this.prompt = prompt;
@@ -691,6 +696,8 @@ export class AgentProcess {
691
696
  task_type: this.taskType,
692
697
  cloud_repo: this.cloudRepo,
693
698
  cloud_branch: this.cloudBranch,
699
+ worktree_name: this.worktreeName,
700
+ worktree_path: this.worktreePath,
694
701
  };
695
702
  const metaPath = await this.getMetaPath();
696
703
  await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
@@ -726,7 +733,7 @@ export class AgentProcess {
726
733
  : AgentStatus.RUNNING;
727
734
  const agent = new AgentProcess(meta.agent_id, meta.task_name || 'default', meta.agent_type, meta.prompt, meta.cwd || null, resolvedMode, meta.pid || null, resolvedStatus, new Date(meta.started_at), meta.completed_at ? new Date(meta.completed_at) : null, baseDir, meta.parent_session_id || null, meta.workspace_dir || null, meta.cloud_session_id || null, meta.cloud_provider || null, meta.pr_url || null, meta.version || null, meta.remote_session_id || null, meta.name || null, Array.isArray(meta.after) ? meta.after : [], meta.effort || null, meta.model || null, meta.env_overrides || null, meta.task_type && VALID_TASK_TYPES.includes(meta.task_type)
728
735
  ? meta.task_type
729
- : null, meta.cloud_repo || null, meta.cloud_branch || null);
736
+ : null, meta.cloud_repo || null, meta.cloud_branch || null, meta.worktree_name || null, meta.worktree_path || null);
730
737
  agent.startTime = typeof meta.start_time === 'string' ? meta.start_time : null;
731
738
  return agent;
732
739
  }
@@ -971,7 +978,7 @@ export class AgentManager {
971
978
  }
972
979
  debug(`Loaded ${loadedCount} agents from disk`);
973
980
  }
974
- async spawn(taskName, agentType, prompt, cwd = null, mode = null, effort = 'medium', parentSessionId = null, workspaceDir = null, version = null, name = null, after = [], model = null, envOverrides = null, taskType = null, cloudProvider = null, cloudSessionId = null, cloudRepo = null, cloudBranch = null) {
981
+ async spawn(taskName, agentType, prompt, cwd = null, mode = null, effort = 'medium', parentSessionId = null, workspaceDir = null, version = null, name = null, after = [], model = null, envOverrides = null, taskType = null, cloudProvider = null, cloudSessionId = null, cloudRepo = null, cloudBranch = null, worktreeName = null, worktreePath = null) {
975
982
  await this.initialize();
976
983
  const resolvedMode = resolveMode(mode, this.defaultMode);
977
984
  // Enforce: teammate names are unique within a team.
@@ -1030,7 +1037,7 @@ export class AgentManager {
1030
1037
  const initialStatus = isStaged || !isCloudBacked
1031
1038
  ? AgentStatus.PENDING
1032
1039
  : AgentStatus.RUNNING;
1033
- const agent = new AgentProcess(agentId, taskName, agentType, prompt, resolvedCwd, resolvedMode, null, initialStatus, new Date(), null, this.agentsDir, parentSessionId, workspaceDir, cloudSessionId, cloudProvider, null, version, null, name, cleanAfter, effort, model, envOverrides && Object.keys(envOverrides).length > 0 ? envOverrides : null, taskType, cloudRepo, cloudBranch);
1040
+ const agent = new AgentProcess(agentId, taskName, agentType, prompt, resolvedCwd, resolvedMode, null, initialStatus, new Date(), null, this.agentsDir, parentSessionId, workspaceDir, cloudSessionId, cloudProvider, null, version, null, name, cleanAfter, effort, model, envOverrides && Object.keys(envOverrides).length > 0 ? envOverrides : null, taskType, cloudRepo, cloudBranch, worktreeName, worktreePath);
1034
1041
  const agentDir = await agent.getAgentDir();
1035
1042
  try {
1036
1043
  await fs.mkdir(agentDir, { recursive: true });
@@ -88,7 +88,7 @@ export interface TasksResult {
88
88
  tasks: TaskInfo[];
89
89
  }
90
90
  /** Spawn a new teammate in a task and return its initial metadata. */
91
- export declare function handleSpawn(manager: AgentManager, taskName: string, agentType: AgentType, prompt: string, cwd: string | null, mode: string | null, effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto' | null, parentSessionId?: string | null, workspaceDir?: string | null, version?: string | null, name?: string | null, after?: string[], model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudProvider?: string | null, cloudSessionId?: string | null, cloudRepo?: string | null, cloudBranch?: string | null): Promise<SpawnResult>;
91
+ export declare function handleSpawn(manager: AgentManager, taskName: string, agentType: AgentType, prompt: string, cwd: string | null, mode: string | null, effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto' | null, parentSessionId?: string | null, workspaceDir?: string | null, version?: string | null, name?: string | null, after?: string[], model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudProvider?: string | null, cloudSessionId?: string | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null): Promise<SpawnResult>;
92
92
  /** Retrieve the current status of all teammates in a task, with optional timestamp-based delta filtering. */
93
93
  export declare function handleStatus(manager: AgentManager, taskName: string | null | undefined, filter?: string, since?: string, // Optional ISO timestamp - return only events after this time
94
94
  parentSessionId?: string | null): Promise<TaskStatusResult>;
@@ -56,12 +56,12 @@ function recentToolCalls(events, max = 10) {
56
56
  }));
57
57
  }
58
58
  /** Spawn a new teammate in a task and return its initial metadata. */
59
- export async function handleSpawn(manager, taskName, agentType, prompt, cwd, mode, effort = 'medium', parentSessionId = null, workspaceDir = null, version = null, name = null, after = [], model = null, envOverrides = null, taskType = null, cloudProvider = null, cloudSessionId = null, cloudRepo = null, cloudBranch = null) {
59
+ export async function handleSpawn(manager, taskName, agentType, prompt, cwd, mode, effort = 'medium', parentSessionId = null, workspaceDir = null, version = null, name = null, after = [], model = null, envOverrides = null, taskType = null, cloudProvider = null, cloudSessionId = null, cloudRepo = null, cloudBranch = null, worktreeName = null, worktreePath = null) {
60
60
  const defaultMode = manager.getDefaultMode();
61
61
  const resolvedMode = resolveMode(mode, defaultMode);
62
62
  const resolvedEffort = effort ?? 'medium';
63
63
  debug(`[spawn] Spawning ${agentType} agent for task "${taskName}" [${resolvedMode}] effort=${resolvedEffort}...`);
64
- const agent = await manager.spawn(taskName, agentType, prompt, cwd, resolvedMode, resolvedEffort, parentSessionId, workspaceDir, version, name, after, model, envOverrides, taskType, cloudProvider, cloudSessionId, cloudRepo, cloudBranch);
64
+ const agent = await manager.spawn(taskName, agentType, prompt, cwd, resolvedMode, resolvedEffort, parentSessionId, workspaceDir, version, name, after, model, envOverrides, taskType, cloudProvider, cloudSessionId, cloudRepo, cloudBranch, worktreeName, worktreePath);
65
65
  debug(`[spawn] Spawned ${agentType} agent ${agent.agentId} for task "${taskName}"`);
66
66
  return {
67
67
  task_name: taskName,
@@ -13,3 +13,4 @@ export { readConfig, resolveAgentsDir, resolveBaseDir, type EffortLevel, type Mo
13
13
  export { collapseEvents, getToolBreakdown, groupAndFlattenEvents, summarizeEvents, getDelta, filterEventsByPriority, getLastTool, getToolUses, getLastMessages, getQuickStatus, getStatusSummary, AgentSummary, PRIORITY, type QuickStatus, } from './summarizer.js';
14
14
  export { extractFileOpsFromBash } from './file_ops.js';
15
15
  export { debug } from './debug.js';
16
+ export { createWorktree, removeWorktree, isGitRepo, getGitRoot, hasUncommittedChanges, getWorktreePath, getWorktreeBranch, } from './worktree.js';
@@ -12,3 +12,4 @@ export { readConfig, resolveAgentsDir, resolveBaseDir, } from './persistence.js'
12
12
  export { collapseEvents, getToolBreakdown, groupAndFlattenEvents, summarizeEvents, getDelta, filterEventsByPriority, getLastTool, getToolUses, getLastMessages, getQuickStatus, getStatusSummary, AgentSummary, PRIORITY, } from './summarizer.js';
13
13
  export { extractFileOpsFromBash } from './file_ops.js';
14
14
  export { debug } from './debug.js';
15
+ export { createWorktree, removeWorktree, isGitRepo, getGitRoot, hasUncommittedChanges, getWorktreePath, getWorktreeBranch, } from './worktree.js';
@@ -13,7 +13,7 @@ import { homedir, tmpdir } from 'os';
13
13
  import { constants as fsConstants } from 'fs';
14
14
  import { randomBytes } from 'crypto';
15
15
  import lockfile from 'proper-lockfile';
16
- import { getAgentsDir } from '../state.js';
16
+ import { getUserAgentsDir } from '../state.js';
17
17
  /**
18
18
  * Atomic JSON write: writes to a sibling tmp file then renames over the
19
19
  * target. rename(2) is atomic on POSIX, so a crashed/interrupted write
@@ -62,8 +62,8 @@ async function withConfigLock(p, fn) {
62
62
  }
63
63
  // All supported teammate agent types
64
64
  const ALL_AGENTS = ['claude', 'codex', 'gemini', 'cursor', 'opencode'];
65
- // Teams data lives under ~/.agents-system/teams/
66
- const TEAMS_DIR = path.join(getAgentsDir(), 'teams');
65
+ // Teams data lives under ~/.agents/teams/
66
+ const TEAMS_DIR = path.join(getUserAgentsDir(), 'teams');
67
67
  // Legacy paths (for migration)
68
68
  const LEGACY_CONFIG_DIR = path.join(homedir(), '.agents');
69
69
  // Legacy migration from pre-OSS brand; safe to remove after 2026-07
@@ -2,6 +2,7 @@
2
2
  export interface TeamMeta {
3
3
  created_at: string;
4
4
  description?: string;
5
+ enable_worktrees?: boolean;
5
6
  }
6
7
  /** Map of team name to team metadata. */
7
8
  export type TeamRegistry = Record<string, TeamMeta>;
@@ -12,11 +13,17 @@ export type TeamRegistry = Record<string, TeamMeta>;
12
13
  * write, which is exactly the data-loss path we are trying to close.
13
14
  */
14
15
  export declare function loadTeams(): Promise<TeamRegistry>;
16
+ export interface CreateTeamOptions {
17
+ description?: string;
18
+ enableWorktrees?: boolean;
19
+ }
15
20
  /** Create a new team. Throws if a team with the same name already exists. */
16
- export declare function createTeam(name: string, description?: string): Promise<TeamMeta>;
21
+ export declare function createTeam(name: string, options?: CreateTeamOptions): Promise<TeamMeta>;
17
22
  /** Return existing team metadata or create a new team if it does not exist. */
18
23
  export declare function ensureTeam(name: string): Promise<TeamMeta>;
19
24
  /** Remove a team from the registry. Returns false if the team did not exist. */
20
25
  export declare function removeTeam(name: string): Promise<boolean>;
21
26
  /** Check whether a team with the given name exists in the registry. */
22
27
  export declare function teamExists(name: string): Promise<boolean>;
28
+ /** Get metadata for a specific team. Returns null if team does not exist. */
29
+ export declare function getTeam(name: string): Promise<TeamMeta | null>;
@@ -91,7 +91,7 @@ async function saveTeams(reg) {
91
91
  await atomicWriteJson(p, reg);
92
92
  }
93
93
  /** Create a new team. Throws if a team with the same name already exists. */
94
- export async function createTeam(name, description) {
94
+ export async function createTeam(name, options) {
95
95
  const p = await registryPath();
96
96
  return withRegistryLock(p, async () => {
97
97
  const reg = await loadTeams();
@@ -100,7 +100,8 @@ export async function createTeam(name, description) {
100
100
  }
101
101
  const meta = {
102
102
  created_at: new Date().toISOString(),
103
- ...(description ? { description } : {}),
103
+ ...(options?.description ? { description: options.description } : {}),
104
+ ...(options?.enableWorktrees ? { enable_worktrees: true } : {}),
104
105
  };
105
106
  reg[name] = meta;
106
107
  await saveTeams(reg);
@@ -137,3 +138,8 @@ export async function teamExists(name) {
137
138
  const reg = await loadTeams();
138
139
  return Boolean(reg[name]);
139
140
  }
141
+ /** Get metadata for a specific team. Returns null if team does not exist. */
142
+ export async function getTeam(name) {
143
+ const reg = await loadTeams();
144
+ return reg[name] ?? null;
145
+ }
@@ -0,0 +1,30 @@
1
+ export declare function isGitRepo(dir: string): Promise<boolean>;
2
+ export declare function getGitRoot(dir: string): Promise<string>;
3
+ /**
4
+ * Check if a worktree directory has uncommitted changes.
5
+ */
6
+ export declare function hasUncommittedChanges(worktreePath: string): Promise<boolean>;
7
+ /**
8
+ * Create a new git worktree for a teammate.
9
+ *
10
+ * @param repoDir - Directory inside the git repository
11
+ * @param worktreeName - Name for the worktree (used in path and branch)
12
+ * @returns The absolute path to the created worktree
13
+ */
14
+ export declare function createWorktree(repoDir: string, worktreeName: string): Promise<string>;
15
+ /**
16
+ * Remove a git worktree and optionally its branch.
17
+ *
18
+ * @param repoDir - Directory inside the main git repository (not the worktree)
19
+ * @param worktreeName - Name of the worktree to remove
20
+ * @param deleteBranch - Whether to delete the associated branch
21
+ */
22
+ export declare function removeWorktree(repoDir: string, worktreeName: string, deleteBranch?: boolean): Promise<void>;
23
+ /**
24
+ * Get the worktree path for a given name.
25
+ */
26
+ export declare function getWorktreePath(gitRoot: string, worktreeName: string): string;
27
+ /**
28
+ * Get the branch name for a worktree.
29
+ */
30
+ export declare function getWorktreeBranch(worktreeName: string): string;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Git worktree utilities for isolated agent execution.
3
+ *
4
+ * Creates/removes temporary worktrees so each teammate can work on
5
+ * its own branch without interfering with others or the main checkout.
6
+ */
7
+ import { execFile } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import * as fs from 'fs/promises';
10
+ import * as path from 'path';
11
+ const execFileAsync = promisify(execFile);
12
+ export async function isGitRepo(dir) {
13
+ try {
14
+ await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: dir });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ export async function getGitRoot(dir) {
22
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd: dir });
23
+ return stdout.trim();
24
+ }
25
+ /**
26
+ * Check if a worktree directory has uncommitted changes.
27
+ */
28
+ export async function hasUncommittedChanges(worktreePath) {
29
+ try {
30
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], { cwd: worktreePath });
31
+ return stdout.trim().length > 0;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ /**
38
+ * Create a new git worktree for a teammate.
39
+ *
40
+ * @param repoDir - Directory inside the git repository
41
+ * @param worktreeName - Name for the worktree (used in path and branch)
42
+ * @returns The absolute path to the created worktree
43
+ */
44
+ export async function createWorktree(repoDir, worktreeName) {
45
+ const gitRoot = await getGitRoot(repoDir);
46
+ const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
47
+ const branchName = `agents/${worktreeName}`;
48
+ await fs.mkdir(path.dirname(worktreePath), { recursive: true });
49
+ await execFileAsync('git', ['worktree', 'add', '-b', branchName, worktreePath, 'HEAD'], {
50
+ cwd: gitRoot,
51
+ });
52
+ return worktreePath;
53
+ }
54
+ /**
55
+ * Remove a git worktree and optionally its branch.
56
+ *
57
+ * @param repoDir - Directory inside the main git repository (not the worktree)
58
+ * @param worktreeName - Name of the worktree to remove
59
+ * @param deleteBranch - Whether to delete the associated branch
60
+ */
61
+ export async function removeWorktree(repoDir, worktreeName, deleteBranch = true) {
62
+ const gitRoot = await getGitRoot(repoDir);
63
+ const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
64
+ const branchName = `agents/${worktreeName}`;
65
+ try {
66
+ await execFileAsync('git', ['worktree', 'remove', '--force', worktreePath], { cwd: gitRoot });
67
+ }
68
+ catch (err) {
69
+ if (err.message?.includes('is not a working tree')) {
70
+ await execFileAsync('git', ['worktree', 'prune'], { cwd: gitRoot });
71
+ }
72
+ else {
73
+ throw err;
74
+ }
75
+ }
76
+ if (deleteBranch) {
77
+ try {
78
+ await execFileAsync('git', ['branch', '-D', branchName], { cwd: gitRoot });
79
+ }
80
+ catch {
81
+ // Branch might not exist; ignore
82
+ }
83
+ }
84
+ }
85
+ /**
86
+ * Get the worktree path for a given name.
87
+ */
88
+ export function getWorktreePath(gitRoot, worktreeName) {
89
+ return path.join(gitRoot, '.agents', 'worktrees', worktreeName);
90
+ }
91
+ /**
92
+ * Get the branch name for a worktree.
93
+ */
94
+ export function getWorktreeBranch(worktreeName) {
95
+ return `agents/${worktreeName}`;
96
+ }