@phnx-labs/agents-cli 1.21.2 → 1.22.0

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 (102) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +32 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer-actions.d.ts +4 -0
  5. package/dist/commands/computer-actions.js +35 -0
  6. package/dist/commands/computer.js +4 -2
  7. package/dist/commands/exec.d.ts +27 -0
  8. package/dist/commands/exec.js +123 -6
  9. package/dist/commands/models.js +36 -1
  10. package/dist/commands/perf.d.ts +16 -0
  11. package/dist/commands/perf.js +11 -1
  12. package/dist/commands/projects.d.ts +11 -1
  13. package/dist/commands/projects.js +38 -4
  14. package/dist/commands/sessions-backfill.d.ts +32 -0
  15. package/dist/commands/sessions-backfill.js +186 -0
  16. package/dist/commands/sessions-picker.js +17 -2
  17. package/dist/commands/sessions.d.ts +22 -1
  18. package/dist/commands/sessions.js +331 -18
  19. package/dist/commands/teams.js +1 -1
  20. package/dist/commands/worktree.d.ts +3 -3
  21. package/dist/commands/worktree.js +35 -4
  22. package/dist/index.js +8 -0
  23. package/dist/lib/browser/service.js +13 -0
  24. package/dist/lib/computer/dispatch.d.ts +3 -1
  25. package/dist/lib/computer/dispatch.js +10 -2
  26. package/dist/lib/daemon.d.ts +5 -1
  27. package/dist/lib/daemon.js +63 -14
  28. package/dist/lib/devices/resolve-target.d.ts +6 -0
  29. package/dist/lib/devices/resolve-target.js +9 -3
  30. package/dist/lib/event-stream.d.ts +2 -0
  31. package/dist/lib/event-stream.js +3 -0
  32. package/dist/lib/events.d.ts +3 -1
  33. package/dist/lib/events.js +4 -2
  34. package/dist/lib/exec.js +39 -8
  35. package/dist/lib/git.d.ts +14 -0
  36. package/dist/lib/git.js +36 -0
  37. package/dist/lib/hooks/profile.js +1 -14
  38. package/dist/lib/hosts/dispatch.d.ts +12 -0
  39. package/dist/lib/hosts/dispatch.js +23 -6
  40. package/dist/lib/hosts/reconnect.d.ts +38 -0
  41. package/dist/lib/hosts/reconnect.js +85 -4
  42. package/dist/lib/hosts/run-target.js +14 -2
  43. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  44. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  45. package/dist/lib/model-tiers.d.ts +54 -0
  46. package/dist/lib/model-tiers.js +229 -0
  47. package/dist/lib/models.d.ts +3 -0
  48. package/dist/lib/models.js +44 -7
  49. package/dist/lib/percentile.d.ts +12 -0
  50. package/dist/lib/percentile.js +24 -0
  51. package/dist/lib/perf/db.d.ts +1 -2
  52. package/dist/lib/perf/db.js +2 -14
  53. package/dist/lib/plugins.js +12 -1
  54. package/dist/lib/pricing/prices.json +16 -1
  55. package/dist/lib/project-focus.d.ts +42 -0
  56. package/dist/lib/project-focus.js +80 -0
  57. package/dist/lib/project-schedule.d.ts +75 -0
  58. package/dist/lib/project-schedule.js +110 -0
  59. package/dist/lib/redact.d.ts +2 -0
  60. package/dist/lib/redact.js +22 -0
  61. package/dist/lib/remote-agents-json.d.ts +2 -0
  62. package/dist/lib/remote-agents-json.js +3 -3
  63. package/dist/lib/resources.d.ts +16 -0
  64. package/dist/lib/resources.js +25 -14
  65. package/dist/lib/rotate.d.ts +84 -1
  66. package/dist/lib/rotate.js +155 -5
  67. package/dist/lib/routines.js +1 -14
  68. package/dist/lib/runner.d.ts +4 -2
  69. package/dist/lib/runner.js +21 -5
  70. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  71. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  72. package/dist/lib/session/bash-command.js +60 -9
  73. package/dist/lib/session/db.d.ts +22 -1
  74. package/dist/lib/session/db.js +516 -24
  75. package/dist/lib/session/discover.d.ts +68 -7
  76. package/dist/lib/session/discover.js +186 -84
  77. package/dist/lib/session/highlights.d.ts +24 -4
  78. package/dist/lib/session/highlights.js +52 -7
  79. package/dist/lib/session/parse.d.ts +8 -1
  80. package/dist/lib/session/parse.js +102 -35
  81. package/dist/lib/session/prompt.d.ts +19 -0
  82. package/dist/lib/session/prompt.js +43 -0
  83. package/dist/lib/session/remote-list.d.ts +71 -0
  84. package/dist/lib/session/remote-list.js +410 -2
  85. package/dist/lib/session/shell-programs.d.ts +15 -0
  86. package/dist/lib/session/shell-programs.js +359 -0
  87. package/dist/lib/session/tool-calls.d.ts +88 -0
  88. package/dist/lib/session/tool-calls.js +612 -0
  89. package/dist/lib/session/tool-index.d.ts +100 -0
  90. package/dist/lib/session/tool-index.js +773 -0
  91. package/dist/lib/session/tool-store.d.ts +15 -0
  92. package/dist/lib/session/tool-store.js +198 -0
  93. package/dist/lib/session/types.d.ts +49 -0
  94. package/dist/lib/state.d.ts +10 -1
  95. package/dist/lib/state.js +11 -2
  96. package/dist/lib/teams/remoteWorktree.d.ts +3 -4
  97. package/dist/lib/teams/remoteWorktree.js +3 -4
  98. package/dist/lib/teams/worktree.d.ts +11 -1
  99. package/dist/lib/teams/worktree.js +42 -4
  100. package/dist/lib/types.d.ts +31 -0
  101. package/dist/lib/types.js +17 -0
  102. package/package.json +3 -1
@@ -0,0 +1,15 @@
1
+ import type Database from '../sqlite.js';
2
+ import { type IndexedToolCall } from './tool-calls.js';
3
+ import type { SessionMeta } from './types.js';
4
+ export declare function canonicalToolLedgerPath(filePath: string): string;
5
+ /** Resolve the transcript that actually carries tool events for split-file harnesses. */
6
+ export declare function toolEvidenceSourcePath(filePath: string, agent: string): string;
7
+ /** Remove cached tool evidence when its source transcript no longer exists. */
8
+ export declare function purgeToolCalls(db: Database.Database, sessionId: string): void;
9
+ /** Purge deleted direct children when a transcript directory's stamp changes. */
10
+ export declare function purgeMissingToolCallsInDirectory(db: Database.Database, dirPath: string, currentFilePaths: string[]): number;
11
+ /** Persist one parser batch and its file stamp atomically. */
12
+ export declare function persistToolCalls(db: Database.Database, session: SessionMeta, calls: IndexedToolCall[], sourceStamp: {
13
+ fileMtimeMs: number;
14
+ fileSize: number;
15
+ }, mode?: 'replace' | 'append', maxSessionBytes?: number): void;
@@ -0,0 +1,198 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { TOOL_INDEX_VERSION, TOOL_INDEX_LIMIT_ORDINAL, TOOL_SESSION_EVIDENCE_MAX_BYTES, toolCallEvidenceBytes, toolCallKey, } from './tool-calls.js';
4
+ export function canonicalToolLedgerPath(filePath) {
5
+ let probe = path.resolve(filePath);
6
+ const suffix = [];
7
+ while (true) {
8
+ try {
9
+ return path.join(fs.realpathSync(probe), ...suffix.reverse());
10
+ }
11
+ catch {
12
+ const parent = path.dirname(probe);
13
+ if (parent === probe)
14
+ return path.resolve(filePath);
15
+ suffix.push(path.basename(probe));
16
+ probe = parent;
17
+ }
18
+ }
19
+ }
20
+ /** Resolve the transcript that actually carries tool events for split-file harnesses. */
21
+ export function toolEvidenceSourcePath(filePath, agent) {
22
+ if (agent === 'kimi' && path.basename(filePath) === 'state.json') {
23
+ return path.join(path.dirname(filePath), 'agents', 'main', 'wire.jsonl');
24
+ }
25
+ if (agent === 'grok' && path.basename(filePath) === 'summary.json') {
26
+ return path.join(path.dirname(filePath), 'chat_history.jsonl');
27
+ }
28
+ return filePath;
29
+ }
30
+ function deleteSessionCalls(db, sessionId) {
31
+ const keys = db.prepare(`SELECT call_key FROM tool_calls WHERE session_id = ?`).all(sessionId);
32
+ for (const { call_key } of keys) {
33
+ db.prepare(`DELETE FROM tool_call_programs WHERE call_key = ?`).run(call_key);
34
+ db.prepare(`DELETE FROM tool_program_occurrences WHERE call_key = ?`).run(call_key);
35
+ db.prepare(`DELETE FROM tool_call_text WHERE call_key = ?`).run(call_key);
36
+ }
37
+ db.prepare(`DELETE FROM tool_calls WHERE session_id = ?`).run(sessionId);
38
+ }
39
+ /** Remove cached tool evidence when its source transcript no longer exists. */
40
+ export function purgeToolCalls(db, sessionId) {
41
+ deleteSessionCalls(db, sessionId);
42
+ db.prepare(`DELETE FROM tool_scan_ledger WHERE session_id = ?`).run(sessionId);
43
+ }
44
+ /** Purge deleted direct children when a transcript directory's stamp changes. */
45
+ export function purgeMissingToolCallsInDirectory(db, dirPath, currentFilePaths) {
46
+ const normalizedDir = path.normalize(dirPath);
47
+ const prefix = normalizedDir.endsWith(path.sep) ? normalizedDir : normalizedDir + path.sep;
48
+ const present = new Set(currentFilePaths.map((filePath) => path.normalize(filePath)));
49
+ const rows = db.prepare(`
50
+ SELECT s.id, s.file_path, s.agent
51
+ FROM sessions s
52
+ WHERE substr(s.file_path, 1, ?) = ?
53
+ `).all(prefix.length, prefix);
54
+ let purged = 0;
55
+ for (const row of rows) {
56
+ const filePath = path.normalize(row.file_path);
57
+ if (path.dirname(filePath) !== normalizedDir || present.has(filePath))
58
+ continue;
59
+ purgeToolCalls(db, row.id);
60
+ purged++;
61
+ }
62
+ return purged;
63
+ }
64
+ /** Persist one parser batch and its file stamp atomically. */
65
+ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replace', maxSessionBytes = TOOL_SESSION_EVIDENCE_MAX_BYTES) {
66
+ const sourcePath = toolEvidenceSourcePath(session.filePath, session.agent);
67
+ const insertCall = db.prepare(`
68
+ INSERT INTO tool_calls (
69
+ call_key, session_id, ordinal, source_call_id, timestamp, tool, input,
70
+ outcome, exit_code, status_code, error_code, output, error, parse_error
71
+ , evidence_bytes
72
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
73
+ ON CONFLICT(session_id, ordinal) DO UPDATE SET
74
+ source_call_id = excluded.source_call_id,
75
+ timestamp = excluded.timestamp,
76
+ tool = excluded.tool,
77
+ input = excluded.input,
78
+ outcome = excluded.outcome,
79
+ exit_code = excluded.exit_code,
80
+ status_code = excluded.status_code,
81
+ error_code = excluded.error_code,
82
+ output = excluded.output,
83
+ error = excluded.error,
84
+ parse_error = excluded.parse_error,
85
+ evidence_bytes = excluded.evidence_bytes
86
+ `);
87
+ const insertProgram = db.prepare(`INSERT OR IGNORE INTO tool_call_programs (call_key, program) VALUES (?, ?)`);
88
+ const insertOccurrence = db.prepare(`
89
+ INSERT INTO tool_program_occurrences (call_key, occurrence_ordinal, program, role)
90
+ VALUES (?, ?, ?, ?)
91
+ `);
92
+ const insertText = db.prepare(`INSERT INTO tool_call_text (call_key, tool, input, output, error) VALUES (?, ?, ?, ?, ?)`);
93
+ const deletePrograms = db.prepare(`DELETE FROM tool_call_programs WHERE call_key = ?`);
94
+ const deleteOccurrences = db.prepare(`DELETE FROM tool_program_occurrences WHERE call_key = ?`);
95
+ const deleteText = db.prepare(`DELETE FROM tool_call_text WHERE call_key = ?`);
96
+ const deleteCall = db.prepare(`DELETE FROM tool_calls WHERE call_key = ?`);
97
+ const writeLedger = db.prepare(`
98
+ INSERT INTO tool_scan_ledger (
99
+ session_id, file_path, file_mtime_ms, file_size, extractor_version, indexed_at, call_count,
100
+ evidence_bytes
101
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
102
+ ON CONFLICT(session_id) DO UPDATE SET
103
+ file_path = excluded.file_path,
104
+ file_mtime_ms = excluded.file_mtime_ms,
105
+ file_size = excluded.file_size,
106
+ extractor_version = excluded.extractor_version,
107
+ indexed_at = excluded.indexed_at,
108
+ call_count = excluded.call_count,
109
+ evidence_bytes = excluded.evidence_bytes
110
+ `);
111
+ const txn = db.transaction(() => {
112
+ if (mode === 'replace')
113
+ deleteSessionCalls(db, session.id);
114
+ const ledgerPath = canonicalToolLedgerPath(sourcePath);
115
+ const priorLedger = mode === 'append'
116
+ ? db.prepare(`SELECT call_count, evidence_bytes FROM tool_scan_ledger WHERE session_id = ?`).get(session.id)
117
+ : undefined;
118
+ if (mode === 'append' && !priorLedger) {
119
+ throw new Error('Append tool evidence requires an existing tool ledger row.');
120
+ }
121
+ if (mode === 'append' && calls.length === 0) {
122
+ writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), priorLedger.call_count, priorLedger.evidence_bytes);
123
+ return;
124
+ }
125
+ const limitKey = toolCallKey(session.id, TOOL_INDEX_LIMIT_ORDINAL);
126
+ const existingSize = db.prepare(`
127
+ SELECT evidence_bytes FROM tool_calls WHERE session_id = ? AND ordinal = ?
128
+ `);
129
+ const priorLimit = mode === 'append'
130
+ ? existingSize.get(session.id, TOOL_INDEX_LIMIT_ORDINAL)
131
+ : undefined;
132
+ deletePrograms.run(limitKey);
133
+ deleteOccurrences.run(limitKey);
134
+ deleteText.run(limitKey);
135
+ deleteCall.run(limitKey);
136
+ const existingRows = mode === 'append'
137
+ ? calls.map((call) => ({
138
+ ordinal: call.ordinal,
139
+ bytes: existingSize.get(session.id, call.ordinal)?.evidence_bytes ?? 0,
140
+ }))
141
+ : [];
142
+ const existingByOrdinal = new Map(existingRows.map((row) => [row.ordinal, row.bytes]));
143
+ let storedBytes = priorLedger?.evidence_bytes ?? 0;
144
+ let count = priorLedger?.call_count ?? 0;
145
+ if (priorLimit) {
146
+ storedBytes -= priorLimit.evidence_bytes;
147
+ count--;
148
+ }
149
+ const limitCall = {
150
+ ordinal: TOOL_INDEX_LIMIT_ORDINAL,
151
+ timestamp: session.timestamp,
152
+ tool: 'index_limit',
153
+ programs: [],
154
+ programOccurrences: [],
155
+ input: `Tool evidence stopped at the ${Math.floor(maxSessionBytes / 1024 / 1024)} MiB per-session payload limit.`,
156
+ outcome: 'unknown',
157
+ parseError: 'Additional tool calls were not indexed for this session.',
158
+ };
159
+ const limitBytes = toolCallEvidenceBytes(limitCall);
160
+ const ordinaryLimit = Math.max(0, maxSessionBytes - limitBytes);
161
+ const accepted = [];
162
+ let truncated = false;
163
+ for (const call of [...calls].sort((a, b) => a.ordinal - b.ordinal)) {
164
+ const replacingBytes = existingByOrdinal.get(call.ordinal) ?? 0;
165
+ const nextBytes = storedBytes - replacingBytes + toolCallEvidenceBytes(call);
166
+ if (nextBytes > ordinaryLimit) {
167
+ truncated = true;
168
+ break;
169
+ }
170
+ storedBytes = nextBytes;
171
+ accepted.push(call);
172
+ if (!existingByOrdinal.has(call.ordinal) || replacingBytes === 0)
173
+ count++;
174
+ }
175
+ if (truncated) {
176
+ if (storedBytes + limitBytes <= maxSessionBytes) {
177
+ accepted.push(limitCall);
178
+ storedBytes += limitBytes;
179
+ count++;
180
+ }
181
+ }
182
+ for (const call of accepted) {
183
+ const key = toolCallKey(session.id, call.ordinal);
184
+ insertCall.run(key, session.id, call.ordinal, call.sourceCallId ?? null, call.timestamp, call.tool, call.input, call.outcome, call.exitCode ?? null, call.statusCode ?? null, call.errorCode ?? null, call.output ?? null, call.error ?? null, call.parseError ?? null, toolCallEvidenceBytes(call));
185
+ deletePrograms.run(key);
186
+ deleteOccurrences.run(key);
187
+ deleteText.run(key);
188
+ for (const program of call.programs)
189
+ insertProgram.run(key, program);
190
+ call.programOccurrences.forEach((occurrence, occurrenceOrdinal) => {
191
+ insertOccurrence.run(key, occurrenceOrdinal, occurrence.program, occurrence.role);
192
+ });
193
+ insertText.run(key, call.tool, call.input, call.output ?? '', call.error ?? '');
194
+ }
195
+ writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), count, storedBytes);
196
+ });
197
+ txn();
198
+ }
@@ -24,10 +24,17 @@ export interface SessionEvent {
24
24
  role?: 'user' | 'assistant';
25
25
  content?: string;
26
26
  tool?: string;
27
+ /** Harness-native call identity, used to correlate concurrent results. */
28
+ callId?: string;
27
29
  args?: Record<string, any>;
28
30
  path?: string;
29
31
  command?: string;
30
32
  success?: boolean;
33
+ /** Structured harness outcome; never inferred from free-text output. */
34
+ outcome?: 'ok' | 'error' | 'unknown';
35
+ exitCode?: number;
36
+ statusCode?: number;
37
+ errorCode?: string;
31
38
  output?: string;
32
39
  /** Internal: marks tool_use events from local commands */
33
40
  _local?: boolean;
@@ -52,6 +59,15 @@ export interface SessionEvent {
52
59
  hookName?: string;
53
60
  /** Lifecycle event the hook fired on (SessionStart, PreToolUse, …). */
54
61
  hookEvent?: string;
62
+ /**
63
+ * Slash-command invocation name (e.g. `/recap`, `/code:commit`), captured
64
+ * two ways in a Claude transcript: a `role=user` message whose content is
65
+ * the `<command-name>` wrapper Claude injects for a typed slash command
66
+ * (`parseClaudeContent`, see `prompt.ts`'s `extractSlashCommandName`), or a
67
+ * `tool_use` event for the `SlashCommand` tool (a command the MODEL invoked
68
+ * programmatically, not the user). Undefined for every other event.
69
+ */
70
+ slashCommand?: string;
55
71
  }
56
72
  /** A displayable file attachment discovered in a session transcript. */
57
73
  export interface SessionAttachment {
@@ -186,6 +202,39 @@ export interface SessionMeta {
186
202
  todos?: TodoProgress;
187
203
  /** Most-recent unique directories changed or used as a shell working directory. */
188
204
  recentDirectoriesTouched?: string[];
205
+ /**
206
+ * Skills invoked during the session (structurally identical to
207
+ * session/highlights.ts's SkillUse — declared inline here rather than
208
+ * imported, to avoid a circular import: highlights.ts imports SessionEvent
209
+ * from this file). Populated by discover.ts's incremental Claude
210
+ * accumulator (ClaudeParseState.skillEvents, run through extractSkills at
211
+ * finalize) so session/db.ts's upsertSessionsBatch can write
212
+ * session_resource_usage rows WITHOUT re-parsing the whole transcript —
213
+ * the same reason meta.todos/recentDirectoriesTouched are pre-computed by
214
+ * the caller for claude/codex instead of left to db.ts's re-parse path.
215
+ */
216
+ skillsUsed?: Array<{
217
+ name: string;
218
+ count: number;
219
+ }>;
220
+ /** Sibling of {@link skillsUsed} for slash-command invocations (SessionEvent.slashCommand). */
221
+ slashCommandsUsed?: Array<{
222
+ name: string;
223
+ count: number;
224
+ }>;
225
+ /**
226
+ * Whether this session emitted at least one `browser.navigate` /
227
+ * `browser.screenshot` event, computed at scan time from a sessionId-scoped
228
+ * read of the events log (events.ts `query({ sessionId })`) rather than a
229
+ * transcript re-scan — see `detectToolUsage` in session/db.ts. `undefined`
230
+ * means a legacy row this scanner hasn't computed the field for yet (never
231
+ * collapsed to `false`, so a consumer — e.g. sessions-picker.ts's
232
+ * `classifySessionTool` — knows to fall back to a transcript-derived guess
233
+ * instead of trusting a false negative).
234
+ */
235
+ usedBrowser?: boolean;
236
+ /** Sibling of {@link usedBrowser} for `computer.action` events. */
237
+ usedComputer?: boolean;
189
238
  /** Linear project containing ticketId, resolved lazily and cached in SQLite. */
190
239
  linearProject?: string;
191
240
  /** Browser URL for linearProject. */
@@ -272,7 +272,16 @@ export declare function getCompanionDir(): string;
272
272
  export declare function getBrowserRuntimeDir(): string;
273
273
  /** Path to helper subprocess scratch (~/.agents/.cache/helpers/). */
274
274
  export declare function getHelpersDir(): string;
275
- /** Path to scheduler daemon scratch (~/.agents/.cache/helpers/daemon/). */
275
+ /**
276
+ * Path to scheduler daemon scratch (~/.agents/.cache/helpers/daemon/) — holds
277
+ * the daemon pid file, heartbeat, start lock, and log. AGENTS_DAEMON_DIR
278
+ * redirects it to a fork-private temp so daemon tests (which write pid/heartbeat
279
+ * files and acquire the real start lock) can never clobber a live daemon's state
280
+ * on a dev machine — the surgical mirror of AGENTS_DEVICES_DIR /
281
+ * AGENTS_HOOK_SHIMS_DIR, leaving HOME untouched. Read at CALL time (daemon.ts
282
+ * resolves every path helper through this), so tests/setup.ts can set it before
283
+ * the daemon module is exercised. Never set in production code.
284
+ */
276
285
  export declare function getDaemonDir(): string;
277
286
  /** Path to PTY server scratch (~/.agents/.cache/helpers/pty/). */
278
287
  export declare function getPtyDir(): string;
package/dist/lib/state.js CHANGED
@@ -579,8 +579,17 @@ export function getCompanionDir() { return COMPANION_CACHE_DIR; }
579
579
  export function getBrowserRuntimeDir() { return BROWSER_RUNTIME_DIR; }
580
580
  /** Path to helper subprocess scratch (~/.agents/.cache/helpers/). */
581
581
  export function getHelpersDir() { return HELPERS_DIR; }
582
- /** Path to scheduler daemon scratch (~/.agents/.cache/helpers/daemon/). */
583
- export function getDaemonDir() { return DAEMON_DIR; }
582
+ /**
583
+ * Path to scheduler daemon scratch (~/.agents/.cache/helpers/daemon/) — holds
584
+ * the daemon pid file, heartbeat, start lock, and log. AGENTS_DAEMON_DIR
585
+ * redirects it to a fork-private temp so daemon tests (which write pid/heartbeat
586
+ * files and acquire the real start lock) can never clobber a live daemon's state
587
+ * on a dev machine — the surgical mirror of AGENTS_DEVICES_DIR /
588
+ * AGENTS_HOOK_SHIMS_DIR, leaving HOME untouched. Read at CALL time (daemon.ts
589
+ * resolves every path helper through this), so tests/setup.ts can set it before
590
+ * the daemon module is exercised. Never set in production code.
591
+ */
592
+ export function getDaemonDir() { return process.env.AGENTS_DAEMON_DIR ?? DAEMON_DIR; }
584
593
  /** Path to PTY server scratch (~/.agents/.cache/helpers/pty/). */
585
594
  export function getPtyDir() { return PTY_DIR; }
586
595
  /** Path to tmux scratch (~/.agents/.cache/helpers/tmux/) — shared server socket + per-session meta JSONs. */
@@ -26,10 +26,9 @@ export declare function remoteDefaultBranch(target: string, repoPath: string): s
26
26
  * Create a worktree on the host for a teammate, branched off the freshly-fetched
27
27
  * default branch. Returns the absolute worktree path on the host.
28
28
  *
29
- * Mirrors local `createWorktree`, with two deliberate differences: it fetches
30
- * origin first (the orchestrator can't reach into the host's tree to do it), and
31
- * it bases the branch on `origin/<default>` rather than local `HEAD` so a stale
32
- * checkout on the host can't fork the teammate off old code.
29
+ * Same base policy as local `createWorktree`: fetch origin, then
30
+ * `worktree add -b … origin/<default>` so a stale checkout can't fork the
31
+ * teammate off old code.
33
32
  */
34
33
  export declare function createRemoteWorktree(target: string, repoPath: string, worktreeName: string): string;
35
34
  /**
@@ -87,10 +87,9 @@ export function remoteDefaultBranch(target, repoPath) {
87
87
  * Create a worktree on the host for a teammate, branched off the freshly-fetched
88
88
  * default branch. Returns the absolute worktree path on the host.
89
89
  *
90
- * Mirrors local `createWorktree`, with two deliberate differences: it fetches
91
- * origin first (the orchestrator can't reach into the host's tree to do it), and
92
- * it bases the branch on `origin/<default>` rather than local `HEAD` so a stale
93
- * checkout on the host can't fork the teammate off old code.
90
+ * Same base policy as local `createWorktree`: fetch origin, then
91
+ * `worktree add -b … origin/<default>` so a stale checkout can't fork the
92
+ * teammate off old code.
94
93
  */
95
94
  export function createRemoteWorktree(target, repoPath, worktreeName) {
96
95
  assertValidSshTarget(target);
@@ -22,7 +22,17 @@ export declare const DEFAULT_DIFF_MAX_BYTES = 200000;
22
22
  */
23
23
  export declare function gitDiff(worktreePath: string, maxBytes?: number): Promise<string>;
24
24
  /**
25
- * Create a new git worktree for a teammate.
25
+ * Resolve the repo's default branch name (origin/HEAD → `main`/`master`/…).
26
+ * Refreshes origin/HEAD first so a repo cloned before the default was set resolves.
27
+ * Falls back to `main` when origin/HEAD isn't set.
28
+ */
29
+ export declare function localDefaultBranch(gitRoot: string): Promise<string>;
30
+ /**
31
+ * Create a new git worktree for a teammate, branched off the freshly-fetched
32
+ * default branch. Fetches `origin` first and bases the branch on
33
+ * `origin/<default>` so a stale local checkout cannot fork teammates off old
34
+ * code. Matches `createRemoteWorktree` — local and remote isolation share one
35
+ * base policy.
26
36
  *
27
37
  * @param repoDir - Directory inside the git repository
28
38
  * @param worktreeName - Name for the worktree (used in path and branch)
@@ -77,7 +77,34 @@ export async function gitDiff(worktreePath, maxBytes = DEFAULT_DIFF_MAX_BYTES) {
77
77
  }
78
78
  }
79
79
  /**
80
- * Create a new git worktree for a teammate.
80
+ * Resolve the repo's default branch name (origin/HEAD → `main`/`master`/…).
81
+ * Refreshes origin/HEAD first so a repo cloned before the default was set resolves.
82
+ * Falls back to `main` when origin/HEAD isn't set.
83
+ */
84
+ export async function localDefaultBranch(gitRoot) {
85
+ try {
86
+ await execFileAsync('git', ['remote', 'set-head', 'origin', '--auto'], { cwd: gitRoot });
87
+ }
88
+ catch {
89
+ // Offline / no origin — fall through to symbolic-ref or main.
90
+ }
91
+ try {
92
+ const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: gitRoot });
93
+ const base = stdout.trim().replace(/^origin\//, '');
94
+ if (base)
95
+ return base;
96
+ }
97
+ catch {
98
+ // no origin/HEAD
99
+ }
100
+ return 'main';
101
+ }
102
+ /**
103
+ * Create a new git worktree for a teammate, branched off the freshly-fetched
104
+ * default branch. Fetches `origin` first and bases the branch on
105
+ * `origin/<default>` so a stale local checkout cannot fork teammates off old
106
+ * code. Matches `createRemoteWorktree` — local and remote isolation share one
107
+ * base policy.
81
108
  *
82
109
  * @param repoDir - Directory inside the git repository
83
110
  * @param worktreeName - Name for the worktree (used in path and branch)
@@ -90,10 +117,21 @@ export async function createWorktree(repoDir, worktreeName) {
90
117
  const gitRoot = await getGitRoot(repoDir);
91
118
  const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
92
119
  const branchName = `agents/${worktreeName}`;
120
+ const base = await localDefaultBranch(gitRoot);
93
121
  await fs.mkdir(path.dirname(worktreePath), { recursive: true });
94
- await execFileAsync('git', ['worktree', 'add', '-b', branchName, worktreePath, 'HEAD'], {
95
- cwd: gitRoot,
96
- });
122
+ // Fetch first so origin/<default> is current. Fail loud on network errors —
123
+ // silent fallback to HEAD is how swarms write on a days-stale base and only
124
+ // discover it at merge time.
125
+ try {
126
+ await execFileAsync('git', ['fetch', 'origin'], { cwd: gitRoot });
127
+ }
128
+ catch (err) {
129
+ const detail = (err?.stderr || err?.message || String(err)).toString().trim();
130
+ throw new Error(`createWorktree: git fetch origin failed in ${gitRoot}` +
131
+ (detail ? `: ${detail}` : '') +
132
+ `. Cannot base a teammate worktree on a stale remote-tracking ref.`);
133
+ }
134
+ await execFileAsync('git', ['worktree', 'add', '-b', branchName, worktreePath, `origin/${base}`], { cwd: gitRoot });
97
135
  return worktreePath;
98
136
  }
99
137
  /**
@@ -11,6 +11,23 @@ import type { FeedBroadcastConfig } from './feed-broadcast.js';
11
11
  export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes';
12
12
  /** How `agents run <agent>` chooses an installed version when none is pinned. */
13
13
  export type RunStrategy = 'pinned' | 'available' | 'balanced';
14
+ /**
15
+ * Reserved `<agent>` keyword for `agents run auto` — full-auto dispatch:
16
+ * host (14d launch affinity) → harness (best-account headroom, weighted) →
17
+ * account (balanced). Lives in lib (not commands/) because both the run
18
+ * command (exec.ts) and the host dispatch layer (hosts/dispatch.ts, which
19
+ * arms the chain-hop guard for remote `run auto`) must agree on it.
20
+ */
21
+ export declare const RUN_AUTO_KEYWORD = "auto";
22
+ /**
23
+ * Env var a host dispatcher exports into the remote SHELL when it dispatches
24
+ * `agents run auto`: tells the remote CLI its host layer is already resolved,
25
+ * so it must not re-run affinity and chain-hop to a third host. It rides the
26
+ * shell-export prelude (hosts/dispatch.ts `remoteRunShellPrelude`) because
27
+ * `--env` flags only reach the spawned AGENT's env — the remote CLI's own
28
+ * process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
29
+ */
30
+ export declare const RUN_AUTO_HOST_RESOLVED_ENV = "AGENTS_RUN_AUTO_HOST_RESOLVED";
14
31
  /** Per-agent run strategy config. */
15
32
  export interface AgentRunConfig {
16
33
  strategy?: RunStrategy;
@@ -605,6 +622,20 @@ export interface DiscoveredPlugin {
605
622
  * (e.g. workflow-scoped) — those default to the user marketplace on sync.
606
623
  */
607
624
  marketplace?: string;
625
+ /**
626
+ * Absolute path to the DotAgents repo root containing this plugin — the
627
+ * grandparent of `root` (`<repo>/plugins/<name>` → `<repo>`), true for every
628
+ * marketplace kind (user/system/extra/project). DotAgents repos are
629
+ * git-tracked (plugins.ts), so this pairs with {@link snapshotSha}.
630
+ */
631
+ repoRoot: string;
632
+ /**
633
+ * Short HEAD sha of `repoRoot`'s git checkout, lazily resolved (a getter,
634
+ * not computed at discovery time) and memoized per repoRoot
635
+ * (`git.ts` `resolveSnapshotSha`) — see `ResolvedResource.snapshotSha` for
636
+ * the identical rationale. `undefined` when `repoRoot` isn't a git repo.
637
+ */
638
+ readonly snapshotSha: string | undefined;
608
639
  }
609
640
  /**
610
641
  * Identifies one DotAgents repo that contributes a plugin marketplace. Each
package/dist/lib/types.js CHANGED
@@ -5,6 +5,23 @@
5
5
  * configuration schemas, resource tracking, registry types, and permission
6
6
  * formats for each supported agent.
7
7
  */
8
+ /**
9
+ * Reserved `<agent>` keyword for `agents run auto` — full-auto dispatch:
10
+ * host (14d launch affinity) → harness (best-account headroom, weighted) →
11
+ * account (balanced). Lives in lib (not commands/) because both the run
12
+ * command (exec.ts) and the host dispatch layer (hosts/dispatch.ts, which
13
+ * arms the chain-hop guard for remote `run auto`) must agree on it.
14
+ */
15
+ export const RUN_AUTO_KEYWORD = 'auto';
16
+ /**
17
+ * Env var a host dispatcher exports into the remote SHELL when it dispatches
18
+ * `agents run auto`: tells the remote CLI its host layer is already resolved,
19
+ * so it must not re-run affinity and chain-hop to a third host. It rides the
20
+ * shell-export prelude (hosts/dispatch.ts `remoteRunShellPrelude`) because
21
+ * `--env` flags only reach the spawned AGENT's env — the remote CLI's own
22
+ * process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
23
+ */
24
+ export const RUN_AUTO_HOST_RESOLVED_ENV = 'AGENTS_RUN_AUTO_HOST_RESOLVED';
8
25
  /** Every canonical mode in declaration order. Useful for iteration / validation. */
9
26
  export const ALL_MODES = ['plan', 'edit', 'auto', 'skip'];
10
27
  /** Canonical system repo cloned into ~/.agents/.system/. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.21.2",
3
+ "version": "1.22.0",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -89,6 +89,7 @@
89
89
  "@types/proper-lockfile": "4.1.4",
90
90
  "@xterm/headless": "6.0.0",
91
91
  "@zed-industries/agent-client-protocol": "0.4.5",
92
+ "acorn": "8.18.0",
92
93
  "age-encryption": "0.3.0",
93
94
  "chalk": "5.6.2",
94
95
  "commander": "15.0.0",
@@ -101,6 +102,7 @@
101
102
  "shlex": "3.0.0",
102
103
  "simple-git": "3.36.0",
103
104
  "smol-toml": "1.7.0",
105
+ "unbash": "4.0.5",
104
106
  "ws": "^8.21.0",
105
107
  "yaml": "2.9.0"
106
108
  },