@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.
- package/README.md +17 -7
- package/dist/commands/browser.d.ts +2 -0
- package/dist/commands/browser.js +388 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +16 -9
- package/dist/commands/doctor.js +248 -12
- package/dist/commands/prune.js +9 -3
- package/dist/commands/refresh-rules.d.ts +15 -0
- package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
- package/dist/commands/routines.js +1 -1
- package/dist/commands/rules.js +100 -4
- package/dist/commands/secrets.js +198 -11
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +162 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +46 -13
- package/dist/index.js +62 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/browser/cdp.d.ts +24 -0
- package/dist/lib/browser/cdp.js +94 -0
- package/dist/lib/browser/chrome.d.ts +16 -0
- package/dist/lib/browser/chrome.js +157 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +22 -0
- package/dist/lib/browser/drivers/ssh.d.ts +9 -0
- package/dist/lib/browser/drivers/ssh.js +129 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.js +5 -0
- package/dist/lib/browser/input.d.ts +6 -0
- package/dist/lib/browser/input.js +52 -0
- package/dist/lib/browser/ipc.d.ts +12 -0
- package/dist/lib/browser/ipc.js +223 -0
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +61 -0
- package/dist/lib/browser/refs.d.ts +21 -0
- package/dist/lib/browser/refs.js +88 -0
- package/dist/lib/browser/service.d.ts +45 -0
- package/dist/lib/browser/service.js +404 -0
- package/dist/lib/browser/types.d.ts +73 -0
- package/dist/lib/browser/types.js +7 -0
- package/dist/lib/cloud/codex.js +1 -1
- package/dist/lib/cloud/registry.js +2 -2
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/daemon.d.ts +1 -1
- package/dist/lib/daemon.js +47 -11
- package/dist/lib/diff-text.d.ts +25 -0
- package/dist/lib/diff-text.js +47 -0
- package/dist/lib/doctor-diff.d.ts +64 -0
- package/dist/lib/doctor-diff.js +497 -0
- package/dist/lib/git.js +3 -3
- package/dist/lib/hooks.d.ts +6 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/migrate.js +77 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- package/dist/lib/resources.js +1 -1
- package/dist/lib/rotate.d.ts +8 -1
- package/dist/lib/rotate.js +17 -4
- package/dist/lib/rules/compile.d.ts +104 -0
- package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
- package/dist/lib/rules/compose.d.ts +78 -0
- package/dist/lib/rules/compose.js +170 -0
- package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
- package/dist/lib/{memory.js → rules/rules.js} +10 -10
- package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/dist/lib/secrets/bundles.d.ts +61 -4
- package/dist/lib/secrets/bundles.js +222 -54
- package/dist/lib/secrets/index.d.ts +24 -5
- package/dist/lib/secrets/index.js +70 -41
- package/dist/lib/session/active.js +5 -5
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.js +2 -2
- package/dist/lib/session/render.js +21 -7
- package/dist/lib/shims.d.ts +28 -4
- package/dist/lib/shims.js +72 -14
- package/dist/lib/state.d.ts +22 -28
- package/dist/lib/state.js +83 -76
- package/dist/lib/sync-manifest.d.ts +2 -2
- package/dist/lib/sync-manifest.js +5 -5
- package/dist/lib/teams/agents.d.ts +4 -2
- package/dist/lib/teams/agents.js +11 -4
- package/dist/lib/teams/api.d.ts +1 -1
- package/dist/lib/teams/api.js +2 -2
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/persistence.js +3 -3
- package/dist/lib/teams/registry.d.ts +8 -1
- package/dist/lib/teams/registry.js +8 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +12 -6
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +30 -2
- package/dist/lib/versions.js +127 -105
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- 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
|
|
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
|
-
//
|
|
41
|
-
const PACKAGES_DIR = path.join(
|
|
42
|
-
const ROUTINES_DIR = path.join(
|
|
43
|
-
const RUNS_DIR = path.join(
|
|
44
|
-
const VERSIONS_DIR = path.join(
|
|
45
|
-
const SHIMS_DIR = path.join(
|
|
46
|
-
const BACKUPS_DIR = path.join(
|
|
47
|
-
const PLUGINS_DIR = path.join(
|
|
48
|
-
const DRIVE_DIR = path.join(
|
|
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/).
|
|
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
|
-
/**
|
|
142
|
-
export function
|
|
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
|
-
// ───
|
|
206
|
-
/** Path to cloned packages (~/.agents
|
|
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
|
|
178
|
+
/** Path to routine YAML definitions (~/.agents/routines/). */
|
|
209
179
|
export function getRoutinesDir() { return ROUTINES_DIR; }
|
|
210
|
-
/** Path to routine execution logs (~/.agents
|
|
180
|
+
/** Path to routine execution logs (~/.agents/runs/). */
|
|
211
181
|
export function getRunsDir() { return RUNS_DIR; }
|
|
212
|
-
/** Path to installed agent CLI binaries (~/.agents
|
|
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
|
|
184
|
+
/** Path to version-switching shim scripts (~/.agents/shims/). */
|
|
215
185
|
export function getShimsDir() { return SHIMS_DIR; }
|
|
216
|
-
/** Path to config backups (~/.agents
|
|
186
|
+
/** Path to config backups (~/.agents/backups/). */
|
|
217
187
|
export function getBackupsDir() { return BACKUPS_DIR; }
|
|
218
|
-
/** Path to plugin bundles (~/.agents
|
|
188
|
+
/** Path to plugin bundles (~/.agents/plugins/). */
|
|
219
189
|
export function getPluginsDir() { return PLUGINS_DIR; }
|
|
220
|
-
/** Path to synced remote session data (~/.agents
|
|
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
|
-
//
|
|
355
|
-
//
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
78
|
-
* for agents that pre-compile their
|
|
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 {
|
|
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
|
|
355
|
-
* for agents that pre-compile their
|
|
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
|
|
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 (
|
|
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().
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -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 });
|
package/dist/lib/teams/api.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/teams/api.js
CHANGED
|
@@ -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';
|
package/dist/lib/teams/index.js
CHANGED
|
@@ -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 {
|
|
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
|
|
66
|
-
const TEAMS_DIR = path.join(
|
|
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,
|
|
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,
|
|
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
|
+
}
|