@phnx-labs/agents-cli 1.20.5 → 1.20.6

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/browser.js +31 -4
  3. package/dist/commands/computer.js +10 -2
  4. package/dist/commands/defaults.d.ts +7 -0
  5. package/dist/commands/defaults.js +89 -0
  6. package/dist/commands/exec.js +24 -6
  7. package/dist/commands/rules.js +3 -3
  8. package/dist/commands/secrets.js +46 -9
  9. package/dist/commands/setup.js +2 -2
  10. package/dist/commands/teams.js +108 -11
  11. package/dist/commands/view.d.ts +12 -1
  12. package/dist/commands/view.js +121 -38
  13. package/dist/index.js +38 -21
  14. package/dist/lib/agents.d.ts +10 -6
  15. package/dist/lib/agents.js +23 -14
  16. package/dist/lib/browser/chrome.d.ts +10 -0
  17. package/dist/lib/browser/chrome.js +84 -3
  18. package/dist/lib/exec.js +24 -4
  19. package/dist/lib/migrate.js +6 -4
  20. package/dist/lib/permissions.d.ts +23 -0
  21. package/dist/lib/permissions.js +89 -7
  22. package/dist/lib/plugin-marketplace.js +1 -1
  23. package/dist/lib/project-launch.d.ts +5 -0
  24. package/dist/lib/project-launch.js +37 -0
  25. package/dist/lib/pty-server.js +7 -4
  26. package/dist/lib/resources/rules.js +1 -1
  27. package/dist/lib/resources/skills.js +1 -1
  28. package/dist/lib/resources.d.ts +2 -0
  29. package/dist/lib/resources.js +2 -1
  30. package/dist/lib/rotate.js +6 -18
  31. package/dist/lib/run-config.d.ts +9 -0
  32. package/dist/lib/run-config.js +35 -0
  33. package/dist/lib/run-defaults.d.ts +42 -0
  34. package/dist/lib/run-defaults.js +180 -0
  35. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  36. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  37. package/dist/lib/secrets/install-helper.d.ts +11 -3
  38. package/dist/lib/secrets/install-helper.js +48 -6
  39. package/dist/lib/secrets/linux.d.ts +12 -0
  40. package/dist/lib/secrets/linux.js +30 -16
  41. package/dist/lib/shims.d.ts +9 -1
  42. package/dist/lib/shims.js +35 -3
  43. package/dist/lib/staleness/detectors/hooks.js +1 -1
  44. package/dist/lib/staleness/writers/hooks.js +1 -1
  45. package/dist/lib/teams/api.d.ts +67 -0
  46. package/dist/lib/teams/api.js +78 -0
  47. package/dist/lib/types.d.ts +15 -6
  48. package/dist/lib/versions.js +4 -4
  49. package/package.json +5 -2
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Selector-based defaults for `agents run`.
3
+ *
4
+ * Stored under agents.yaml:
5
+ *
6
+ * run:
7
+ * defaults:
8
+ * "claude:*":
9
+ * mode: auto
10
+ * model: opus
11
+ * "claude:2.1.45":
12
+ * mode: plan
13
+ */
14
+ import { ALL_MODES } from './types.js';
15
+ import { AGENTS } from './agents.js';
16
+ import { readMeta, updateMeta } from './state.js';
17
+ import { getProjectRunConfigs } from './run-config.js';
18
+ const VERSION_RE = /^(?:\*|latest|(?!.*\.\.)[A-Za-z0-9._+-]{1,64})$/;
19
+ function isAgentId(value) {
20
+ return value in AGENTS;
21
+ }
22
+ export function normalizeRunDefaultMode(input) {
23
+ const mode = input.trim().toLowerCase();
24
+ if (mode === 'full')
25
+ return 'skip';
26
+ if (ALL_MODES.includes(mode))
27
+ return mode;
28
+ throw new Error(`Invalid mode '${input}'. Use one of: ${ALL_MODES.join(', ')} (or 'full' as an alias for 'skip').`);
29
+ }
30
+ function normalizeRunDefaults(defaults, selector) {
31
+ const out = {};
32
+ if (defaults.mode !== undefined) {
33
+ if (typeof defaults.mode !== 'string') {
34
+ throw new Error(`Invalid mode in run.defaults.${selector}: expected a string.`);
35
+ }
36
+ out.mode = normalizeRunDefaultMode(defaults.mode);
37
+ }
38
+ if (defaults.model !== undefined) {
39
+ if (typeof defaults.model !== 'string' || defaults.model.trim() === '') {
40
+ throw new Error(`Invalid model in run.defaults.${selector}: expected a non-empty string.`);
41
+ }
42
+ out.model = defaults.model.trim();
43
+ }
44
+ return out;
45
+ }
46
+ export function parseRunDefaultSelector(input) {
47
+ const raw = input.trim();
48
+ if (!raw)
49
+ throw new Error('Selector is required. Use <agent>:<version>, <agent>@<version>, or <agent>:*.');
50
+ let agentPart;
51
+ let versionPart;
52
+ if (raw.includes('@')) {
53
+ const parts = raw.split('@');
54
+ if (parts.length !== 2)
55
+ throw new Error(`Invalid selector '${input}'. Use <agent>@<version>.`);
56
+ [agentPart, versionPart] = parts;
57
+ }
58
+ else if (raw.includes(':')) {
59
+ const idx = raw.indexOf(':');
60
+ agentPart = raw.slice(0, idx);
61
+ versionPart = raw.slice(idx + 1);
62
+ }
63
+ else {
64
+ agentPart = raw;
65
+ versionPart = '*';
66
+ }
67
+ const agent = agentPart.toLowerCase();
68
+ if (!isAgentId(agent)) {
69
+ throw new Error(`Invalid agent '${agentPart}'. Available agents: ${Object.keys(AGENTS).join(', ')}.`);
70
+ }
71
+ if (!VERSION_RE.test(versionPart)) {
72
+ throw new Error(`Invalid selector version '${versionPart}'. Use *, latest, or [A-Za-z0-9._+-]{1,64}.`);
73
+ }
74
+ return {
75
+ agent,
76
+ version: versionPart,
77
+ selector: `${agent}:${versionPart}`,
78
+ };
79
+ }
80
+ function sortedDefaults(defaults) {
81
+ return Object.fromEntries(Object.entries(defaults).sort(([a], [b]) => a.localeCompare(b)));
82
+ }
83
+ export function resolveRunDefaultsFromConfig(runConfig, agent, version) {
84
+ const defaults = runConfig?.defaults ?? {};
85
+ const wildcardSelector = `${agent}:*`;
86
+ const exactSelector = version ? `${agent}:${version}` : null;
87
+ const resolved = { sources: {} };
88
+ const wildcard = defaults[wildcardSelector]
89
+ ? normalizeRunDefaults(defaults[wildcardSelector], wildcardSelector)
90
+ : null;
91
+ if (wildcard?.mode) {
92
+ resolved.mode = wildcard.mode;
93
+ resolved.sources.mode = wildcardSelector;
94
+ }
95
+ if (wildcard?.model) {
96
+ resolved.model = wildcard.model;
97
+ resolved.sources.model = wildcardSelector;
98
+ }
99
+ if (exactSelector && defaults[exactSelector]) {
100
+ const exact = normalizeRunDefaults(defaults[exactSelector], exactSelector);
101
+ if (exact.mode) {
102
+ resolved.mode = exact.mode;
103
+ resolved.sources.mode = exactSelector;
104
+ }
105
+ if (exact.model) {
106
+ resolved.model = exact.model;
107
+ resolved.sources.model = exactSelector;
108
+ }
109
+ }
110
+ return resolved;
111
+ }
112
+ export function resolveRunDefaultsFromConfigs(runConfigs, agent, version) {
113
+ const resolved = { sources: {} };
114
+ for (const runConfig of runConfigs) {
115
+ const next = resolveRunDefaultsFromConfig(runConfig, agent, version);
116
+ if (next.mode) {
117
+ resolved.mode = next.mode;
118
+ resolved.sources.mode = next.sources.mode;
119
+ }
120
+ if (next.model) {
121
+ resolved.model = next.model;
122
+ resolved.sources.model = next.sources.model;
123
+ }
124
+ }
125
+ return resolved;
126
+ }
127
+ export function resolveRunDefaults(agent, version, startPath = process.cwd()) {
128
+ const projectRunConfigs = getProjectRunConfigs(startPath).reverse();
129
+ return resolveRunDefaultsFromConfigs([readMeta().run, ...projectRunConfigs], agent, version);
130
+ }
131
+ export function listRunDefaults() {
132
+ const defaults = readMeta().run?.defaults ?? {};
133
+ return Object.entries(defaults)
134
+ .sort(([a], [b]) => a.localeCompare(b))
135
+ .map(([selector, value]) => ({
136
+ selector,
137
+ defaults: normalizeRunDefaults(value, selector),
138
+ }));
139
+ }
140
+ export function setRunDefault(selectorInput, defaultsInput) {
141
+ const parsed = parseRunDefaultSelector(selectorInput);
142
+ const defaults = normalizeRunDefaults(defaultsInput, parsed.selector);
143
+ if (!defaults.mode && !defaults.model) {
144
+ throw new Error('Set at least one default: --mode <mode> or --model <model>.');
145
+ }
146
+ updateMeta((meta) => {
147
+ const run = { ...(meta.run ?? {}) };
148
+ const currentDefaults = { ...(run.defaults ?? {}) };
149
+ currentDefaults[parsed.selector] = {
150
+ ...(currentDefaults[parsed.selector] ?? {}),
151
+ ...defaults,
152
+ };
153
+ run.defaults = sortedDefaults(currentDefaults);
154
+ return { ...meta, run };
155
+ });
156
+ return {
157
+ selector: parsed.selector,
158
+ defaults: {
159
+ ...(readMeta().run?.defaults?.[parsed.selector] ?? {}),
160
+ },
161
+ };
162
+ }
163
+ export function unsetRunDefault(selectorInput) {
164
+ const parsed = parseRunDefaultSelector(selectorInput);
165
+ let removed = false;
166
+ updateMeta((meta) => {
167
+ const run = { ...(meta.run ?? {}) };
168
+ const currentDefaults = { ...(run.defaults ?? {}) };
169
+ removed = Object.prototype.hasOwnProperty.call(currentDefaults, parsed.selector);
170
+ delete currentDefaults[parsed.selector];
171
+ if (Object.keys(currentDefaults).length > 0) {
172
+ run.defaults = sortedDefaults(currentDefaults);
173
+ }
174
+ else {
175
+ delete run.defaults;
176
+ }
177
+ return { ...meta, run };
178
+ });
179
+ return removed;
180
+ }
@@ -12,10 +12,14 @@
12
12
  * modules in `src/lib/secrets/` must import `getKeychainHelperPath()` rather
13
13
  * than recomputing it.
14
14
  */
15
+ /** Redirect the install root (test only). Returns the previous override so callers can restore. */
16
+ export declare function setInstallRootForTest(dir: string | null): string | null;
15
17
  /**
16
18
  * Idempotent install. Copies the bundled `.app` to the stable user path. Skips
17
- * if the destination already exists and `codesign --verify` passes, unless
18
- * `forceReinstall=true`.
19
+ * if the destination already exists, `codesign --verify` passes, AND the
20
+ * installed executable matches the bundled one byte-for-byte — a valid
21
+ * signature alone is not enough, because an outdated helper signs clean too.
22
+ * `forceReinstall=true` skips all checks and always copies.
19
23
  *
20
24
  * Notarization is checked via `spctl --assess` after install — a failure is
21
25
  * logged as a warning but does NOT throw. Notarization checks require network
@@ -27,7 +31,11 @@ export declare function ensureKeychainHelperInstalled(opts?: {
27
31
  }): void;
28
32
  /**
29
33
  * Return the absolute path to the helper executable. If the installed bundle
30
- * is missing, performs a lazy install first.
34
+ * is missing, or is stale relative to the bundled source helper, performs a
35
+ * lazy (re)install first. The staleness check is what lets an upgraded CLI
36
+ * replace a helper a previous version installed — `agents helper install`
37
+ * never runs on `npm i -g`, so this call site is the only one every machine
38
+ * is guaranteed to pass through.
31
39
  *
32
40
  * Throws on non-darwin.
33
41
  */
@@ -14,14 +14,23 @@
14
14
  */
15
15
  import { fileURLToPath } from 'url';
16
16
  import { spawnSync } from 'child_process';
17
+ import { createHash } from 'crypto';
17
18
  import * as fs from 'fs';
18
19
  import * as os from 'os';
19
20
  import * as path from 'path';
20
21
  const APP_BUNDLE_NAME = 'Agents CLI.app';
21
22
  const INSTALL_DIR_NAME = 'agents-cli';
23
+ let installRootOverride = null;
24
+ /** Redirect the install root (test only). Returns the previous override so callers can restore. */
25
+ export function setInstallRootForTest(dir) {
26
+ const prev = installRootOverride;
27
+ installRootOverride = dir;
28
+ return prev;
29
+ }
22
30
  /** Absolute path to the installed `.app` bundle directory (not the executable). */
23
31
  function installedAppPath() {
24
- return path.join(os.homedir(), 'Library', 'Application Support', INSTALL_DIR_NAME, APP_BUNDLE_NAME);
32
+ const root = installRootOverride ?? os.homedir();
33
+ return path.join(root, 'Library', 'Application Support', INSTALL_DIR_NAME, APP_BUNDLE_NAME);
25
34
  }
26
35
  /** Absolute path to the executable inside the installed `.app` bundle. */
27
36
  function installedExecutablePath() {
@@ -57,6 +66,33 @@ function assertDarwin() {
57
66
  throw new Error('Keychain helper is macOS only.');
58
67
  }
59
68
  }
69
+ function sha256File(filePath) {
70
+ return createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
71
+ }
72
+ /**
73
+ * True when the installed helper executable differs byte-for-byte from the
74
+ * bundled source helper. A stale-but-validly-signed helper is exactly how the
75
+ * broken 1.20.4 build (signed, but missing the keychain-access-groups
76
+ * entitlement) survived upgrades: codesign --verify passes on it, so the
77
+ * "exists and verifies" early-return kept it installed forever. Returns false
78
+ * when there is no bundled source to compare against (dev installs) or no
79
+ * installed copy yet — both cases are decided by the existence checks in the
80
+ * callers, not by staleness.
81
+ */
82
+ function installedHelperIsStale() {
83
+ let srcApp;
84
+ try {
85
+ srcApp = sourceAppPath();
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ const srcExec = path.join(srcApp, 'Contents', 'MacOS', 'Agents CLI');
91
+ const destExec = installedExecutablePath();
92
+ if (!fs.existsSync(srcExec) || !fs.existsSync(destExec))
93
+ return false;
94
+ return sha256File(srcExec) !== sha256File(destExec);
95
+ }
60
96
  function codesignVerify(appPath) {
61
97
  const r = spawnSync('codesign', ['--verify', '--deep', '--strict', appPath], {
62
98
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -86,8 +122,10 @@ function copyAppBundle(src, dest) {
86
122
  }
87
123
  /**
88
124
  * Idempotent install. Copies the bundled `.app` to the stable user path. Skips
89
- * if the destination already exists and `codesign --verify` passes, unless
90
- * `forceReinstall=true`.
125
+ * if the destination already exists, `codesign --verify` passes, AND the
126
+ * installed executable matches the bundled one byte-for-byte — a valid
127
+ * signature alone is not enough, because an outdated helper signs clean too.
128
+ * `forceReinstall=true` skips all checks and always copies.
91
129
  *
92
130
  * Notarization is checked via `spctl --assess` after install — a failure is
93
131
  * logged as a warning but does NOT throw. Notarization checks require network
@@ -99,7 +137,7 @@ export function ensureKeychainHelperInstalled(opts = {}) {
99
137
  const dest = installedAppPath();
100
138
  if (!opts.forceReinstall && fs.existsSync(dest)) {
101
139
  const { ok } = codesignVerify(dest);
102
- if (ok)
140
+ if (ok && !installedHelperIsStale())
103
141
  return;
104
142
  }
105
143
  const src = sourceAppPath();
@@ -119,14 +157,18 @@ export function ensureKeychainHelperInstalled(opts = {}) {
119
157
  }
120
158
  /**
121
159
  * Return the absolute path to the helper executable. If the installed bundle
122
- * is missing, performs a lazy install first.
160
+ * is missing, or is stale relative to the bundled source helper, performs a
161
+ * lazy (re)install first. The staleness check is what lets an upgraded CLI
162
+ * replace a helper a previous version installed — `agents helper install`
163
+ * never runs on `npm i -g`, so this call site is the only one every machine
164
+ * is guaranteed to pass through.
123
165
  *
124
166
  * Throws on non-darwin.
125
167
  */
126
168
  export function getKeychainHelperPath() {
127
169
  assertDarwin();
128
170
  const exec = installedExecutablePath();
129
- if (!fs.existsSync(exec)) {
171
+ if (!fs.existsSync(exec) || installedHelperIsStale()) {
130
172
  ensureKeychainHelperInstalled();
131
173
  }
132
174
  return exec;
@@ -43,6 +43,18 @@ export declare function hasSecretToolToken(item: string): boolean;
43
43
  export declare function getSecretToolToken(item: string): string;
44
44
  export declare function setSecretToolToken(item: string, value: string): void;
45
45
  export declare function deleteSecretToolToken(item: string): boolean;
46
+ /**
47
+ * Parse the item names out of `secret-tool search --all` output, keeping only
48
+ * those starting with `prefix`. Exported for tests.
49
+ *
50
+ * `output` must be the combined stdout+stderr of the search: libsecret splits
51
+ * the dump across both streams — the value/label/schema lines go to stdout
52
+ * while the `attribute.*` lines (which carry `attribute.item`, the only place
53
+ * the item name is reliably machine-readable) go to stderr (observed on
54
+ * libsecret 0.21.4). Which stream each line lands on has varied across
55
+ * libsecret versions, so callers concatenate both rather than bet on one.
56
+ */
57
+ export declare function parseSecretToolItems(output: string, prefix: string): string[];
46
58
  /**
47
59
  * List secrets by prefix. secret-tool doesn't have a list command,
48
60
  * so we use secret-tool search which outputs in a specific format.
@@ -345,6 +345,34 @@ export function deleteSecretToolToken(item) {
345
345
  // surface that rather than silently swallowing.
346
346
  return false;
347
347
  }
348
+ /**
349
+ * Parse the item names out of `secret-tool search --all` output, keeping only
350
+ * those starting with `prefix`. Exported for tests.
351
+ *
352
+ * `output` must be the combined stdout+stderr of the search: libsecret splits
353
+ * the dump across both streams — the value/label/schema lines go to stdout
354
+ * while the `attribute.*` lines (which carry `attribute.item`, the only place
355
+ * the item name is reliably machine-readable) go to stderr (observed on
356
+ * libsecret 0.21.4). Which stream each line lands on has varied across
357
+ * libsecret versions, so callers concatenate both rather than bet on one.
358
+ */
359
+ export function parseSecretToolItems(output, prefix) {
360
+ const items = [];
361
+ // Parse output format:
362
+ // [/org/freedesktop/secrets/collection/login/1]
363
+ // label = agents-cli: myitem
364
+ // ...
365
+ // attribute.item = myitem
366
+ const itemRegex = /attribute\.item\s*=\s*(.+)/g;
367
+ let match;
368
+ while ((match = itemRegex.exec(output)) !== null) {
369
+ const itemName = match[1].trim();
370
+ if (itemName.startsWith(prefix)) {
371
+ items.push(itemName);
372
+ }
373
+ }
374
+ return [...new Set(items)]; // dedupe
375
+ }
348
376
  /**
349
377
  * List secrets by prefix. secret-tool doesn't have a list command,
350
378
  * so we use secret-tool search which outputs in a specific format.
@@ -365,22 +393,8 @@ export function listSecretToolItems(prefix) {
365
393
  }
366
394
  return [];
367
395
  }
368
- const output = result.stdout?.toString() || '';
369
- const items = [];
370
- // Parse output format:
371
- // [/org/freedesktop/secrets/collection/login/1]
372
- // label = agents-cli: myitem
373
- // ...
374
- // attribute.item = myitem
375
- const itemRegex = /attribute\.item\s*=\s*(.+)/g;
376
- let match;
377
- while ((match = itemRegex.exec(output)) !== null) {
378
- const itemName = match[1].trim();
379
- if (itemName.startsWith(prefix)) {
380
- items.push(itemName);
381
- }
382
- }
383
- return [...new Set(items)]; // dedupe
396
+ const output = `${result.stdout?.toString() || ''}\n${result.stderr?.toString() || ''}`;
397
+ return parseSecretToolItems(output, prefix);
384
398
  }
385
399
  /** KeychainBackend implementation for Linux. Routes through secret-tool
386
400
  * with a transparent encrypted-file fallback when the default Secret
@@ -68,8 +68,16 @@ export interface ConflictInfo {
68
68
  * scoped plugin marketplaces (agents-cli/agents-system/extras-<alias>/
69
69
  * agents-project). Version-home reconciliation stays out of the hot
70
70
  * path — management commands still own that.
71
+ * v17 — bash-side skip-fast sentinel under ~/.agents/.cache/launch-sync/.
72
+ * When the sentinel mtime is newer than every source dir, exec the
73
+ * agent binary directly without spawning node. Cuts steady-state
74
+ * hot-path latency from ~680ms (node startup + module init) to ~11ms
75
+ * (a few stat calls). Node writes the sentinel after each successful
76
+ * sync. Documented limitation: POSIX dir mtime only updates on
77
+ * top-level entry add/remove — deep edits to plugin contents won't
78
+ * trigger auto-resync, run `agents sync` for that.
71
79
  */
72
- export declare const SHIM_SCHEMA_VERSION = 16;
80
+ export declare const SHIM_SCHEMA_VERSION = 17;
73
81
  /**
74
82
  * Generate the full bash shim script for the given agent. The returned string
75
83
  * is written to ~/.agents/shims/{cliCommand} and made executable.
package/dist/lib/shims.js CHANGED
@@ -192,8 +192,16 @@ async function promptConflictStrategy(conflictInfos) {
192
192
  * scoped plugin marketplaces (agents-cli/agents-system/extras-<alias>/
193
193
  * agents-project). Version-home reconciliation stays out of the hot
194
194
  * path — management commands still own that.
195
+ * v17 — bash-side skip-fast sentinel under ~/.agents/.cache/launch-sync/.
196
+ * When the sentinel mtime is newer than every source dir, exec the
197
+ * agent binary directly without spawning node. Cuts steady-state
198
+ * hot-path latency from ~680ms (node startup + module init) to ~11ms
199
+ * (a few stat calls). Node writes the sentinel after each successful
200
+ * sync. Documented limitation: POSIX dir mtime only updates on
201
+ * top-level entry add/remove — deep edits to plugin contents won't
202
+ * trigger auto-resync, run `agents sync` for that.
195
203
  */
196
- export const SHIM_SCHEMA_VERSION = 16;
204
+ export const SHIM_SCHEMA_VERSION = 17;
197
205
  /** Internal marker string used to embed the schema version in shim scripts. */
198
206
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
199
207
  function shellQuote(value) {
@@ -473,8 +481,32 @@ fi
473
481
  ${managedEnv}
474
482
 
475
483
  # Project-scoped compile (rules, workspace resources, scoped plugin marketplaces).
476
- # Filesystem-only sub-50ms steady state. Never blocks launch on failure.
477
- "$AGENTS_BIN" sync --agent "$AGENT" --agent-version "$VERSION" --launch --cwd "$PWD" --quiet 2>/dev/null || true
484
+ # Skip-fast: if a sentinel from the last sync exists and is newer than all
485
+ # source dirs (project .agents/, user plugins, system plugins), exec the
486
+ # agent binary directly without spawning node. Cuts steady-state hot-path
487
+ # latency from ~680ms (node startup + agents-cli module init) to ~11ms (a
488
+ # handful of stat calls). Never blocks launch on failure of the sync itself.
489
+ #
490
+ # Known limitation: POSIX dir mtime updates only on entry add/remove at that
491
+ # level. Deep edits to existing plugin contents (e.g. editing a SKILL.md
492
+ # inside a plugin) won't bump the parent dir's mtime — the marketplace copy
493
+ # stays stale until \`agents sync\` runs explicitly or a top-level entry
494
+ # changes. Advanced users hot-iterating on plugins know to run sync.
495
+ PROJECT_SLUG=\$(printf '%s' "\$PWD" | tr / _ | tr ' ' _)
496
+ LAUNCH_SENTINEL="\$AGENTS_USER_DIR/.cache/launch-sync/\${AGENT}@\${VERSION}@\${PROJECT_SLUG}"
497
+ LAUNCH_SKIP=0
498
+ if [ -f "\$LAUNCH_SENTINEL" ]; then
499
+ LAUNCH_SKIP=1
500
+ for LAUNCH_SRC in "\$PWD/.agents" "\$AGENTS_USER_DIR/plugins" "\$AGENTS_USER_DIR/.system/plugins"; do
501
+ if [ -e "\$LAUNCH_SRC" ] && [ "\$LAUNCH_SRC" -nt "\$LAUNCH_SENTINEL" ]; then
502
+ LAUNCH_SKIP=0
503
+ break
504
+ fi
505
+ done
506
+ fi
507
+ if [ "\$LAUNCH_SKIP" = "0" ]; then
508
+ "\$AGENTS_BIN" sync --agent "\$AGENT" --agent-version "\$VERSION" --launch --cwd "\$PWD" --quiet 2>/dev/null || true
509
+ fi
478
510
 
479
511
  exec "$BINARY"${launchArgs} "$@"
480
512
  `;
@@ -3,8 +3,8 @@
3
3
  * whose contents match the central source. Mirrors versions.ts:391-421.
4
4
  */
5
5
  import * as fs from 'fs';
6
- import * as path from 'path';
7
6
  import { agentConfigDirName } from '../../agents.js';
7
+ import * as path from 'path';
8
8
  import { capableAgents } from '../../capabilities.js';
9
9
  import { resolveHookSource } from '../writers/sources.js';
10
10
  import { lazyAgentMap } from '../writers/lazy-map.js';
@@ -5,8 +5,8 @@
5
5
  * stays in the orchestrator since it depends on the broader available set.
6
6
  */
7
7
  import * as fs from 'fs';
8
- import * as path from 'path';
9
8
  import { agentConfigDirName } from '../../agents.js';
9
+ import * as path from 'path';
10
10
  import { capableAgents } from '../../capabilities.js';
11
11
  import { safeJoin } from '../../paths.js';
12
12
  import { registerHooksToSettings } from '../../hooks.js';
@@ -64,6 +64,73 @@ export interface TaskStatusResult {
64
64
  };
65
65
  cursor: string;
66
66
  }
67
+ /**
68
+ * Compact per-teammate snapshot for the default `teams status` view.
69
+ *
70
+ * The detail shape (`AgentStatusDetail`) carries the full prompt (often many
71
+ * KB), every absolute path the agent has touched, and uncapped message
72
+ * bodies. That's the right shape for programmatic consumers, but it makes
73
+ * `teams status` unreadable for orchestrators who only need: what state are
74
+ * you in, what did you just do, what files have you touched.
75
+ *
76
+ * Use {@link toAgentStatusSummary} to derive this from a detail record.
77
+ */
78
+ export interface AgentStatusSummary {
79
+ agent_id: string;
80
+ name: string | null;
81
+ agent_type: string;
82
+ status: string;
83
+ duration: string | null;
84
+ tool_count: number;
85
+ has_errors: boolean;
86
+ pr_url: string | null;
87
+ files: {
88
+ /** Count of files modified since the cursor (delta), plus basenames. */
89
+ modified: {
90
+ count: number;
91
+ names: string[];
92
+ };
93
+ created: {
94
+ count: number;
95
+ names: string[];
96
+ };
97
+ deleted: {
98
+ count: number;
99
+ names: string[];
100
+ };
101
+ /** Read is noisy (per-Read events fire constantly); only emit a count. */
102
+ read: {
103
+ count: number;
104
+ };
105
+ };
106
+ /** Already capped at 15 × 120 chars by the detail builder. */
107
+ bash_commands: string[];
108
+ /** Last 3 messages, each body trimmed to ~400 chars. */
109
+ last_messages: string[];
110
+ /** ISO timestamp — feed back via --since for delta polling. */
111
+ cursor: string;
112
+ }
113
+ /** Compact aggregated result; mirrors {@link TaskStatusResult} but agents[] is the summary shape. */
114
+ export interface TaskStatusSummaryResult {
115
+ task_name: string;
116
+ agents: AgentStatusSummary[];
117
+ summary: {
118
+ pending: number;
119
+ running: number;
120
+ completed: number;
121
+ failed: number;
122
+ stopped: number;
123
+ };
124
+ cursor: string;
125
+ }
126
+ /**
127
+ * Project a full AgentStatusDetail down to a compact AgentStatusSummary.
128
+ * Drops `prompt` entirely (caller knows what they queued), folds file lists
129
+ * to basenames + counts, caps `last_messages` to 3 × {@link SUMMARY_MESSAGE_MAX_CHARS}.
130
+ */
131
+ export declare function toAgentStatusSummary(detail: AgentStatusDetail): AgentStatusSummary;
132
+ /** Project a full TaskStatusResult down to the compact summary shape. */
133
+ export declare function toTaskStatusSummary(result: TaskStatusResult): TaskStatusSummaryResult;
67
134
  /** Result of stopping one or more teammates. */
68
135
  export interface StopResult {
69
136
  task_name: string;
@@ -55,6 +55,84 @@ function recentToolCalls(events, max = 10) {
55
55
  timestamp: typeof event.timestamp === 'string' ? event.timestamp : null,
56
56
  }));
57
57
  }
58
+ /** Max files to name per category in the summary. Counts are always exact. */
59
+ const SUMMARY_MAX_FILE_NAMES = 6;
60
+ /** Max messages in the summary. */
61
+ const SUMMARY_MAX_MESSAGES = 3;
62
+ /** Max chars per message body in the summary. */
63
+ const SUMMARY_MESSAGE_MAX_CHARS = 400;
64
+ /**
65
+ * Reduce a file path to just its basename for compact rendering. Keeps the
66
+ * orchestrator oriented ("you touched types.ts") without dumping the full
67
+ * absolute path every time. The full paths are still in `AgentStatusDetail`
68
+ * for the verbose path.
69
+ */
70
+ function basenameOf(p) {
71
+ const ix = p.lastIndexOf('/');
72
+ return ix < 0 ? p : p.slice(ix + 1);
73
+ }
74
+ /**
75
+ * Trim long assistant messages to a fixed budget. We collapse leading
76
+ * whitespace so the budget covers actual content, not indent.
77
+ */
78
+ function trimMessage(msg, max = SUMMARY_MESSAGE_MAX_CHARS) {
79
+ const s = msg.replace(/^\s+/, '');
80
+ if (s.length <= max)
81
+ return s;
82
+ return s.slice(0, max - 1) + '…';
83
+ }
84
+ /** Pull at most `max` basenames out of a path list, preserving order. */
85
+ function compactFileList(paths, max = SUMMARY_MAX_FILE_NAMES) {
86
+ const seen = new Set();
87
+ const names = [];
88
+ for (const p of paths) {
89
+ const base = basenameOf(p);
90
+ if (seen.has(base))
91
+ continue;
92
+ seen.add(base);
93
+ names.push(base);
94
+ if (names.length >= max)
95
+ break;
96
+ }
97
+ return { count: paths.length, names };
98
+ }
99
+ /**
100
+ * Project a full AgentStatusDetail down to a compact AgentStatusSummary.
101
+ * Drops `prompt` entirely (caller knows what they queued), folds file lists
102
+ * to basenames + counts, caps `last_messages` to 3 × {@link SUMMARY_MESSAGE_MAX_CHARS}.
103
+ */
104
+ export function toAgentStatusSummary(detail) {
105
+ return {
106
+ agent_id: detail.agent_id,
107
+ name: detail.name ?? null,
108
+ agent_type: detail.agent_type,
109
+ status: detail.status,
110
+ duration: detail.duration,
111
+ tool_count: detail.tool_count,
112
+ has_errors: detail.has_errors,
113
+ pr_url: detail.pr_url ?? null,
114
+ files: {
115
+ modified: compactFileList(detail.files_modified),
116
+ created: compactFileList(detail.files_created),
117
+ deleted: compactFileList(detail.files_deleted),
118
+ read: { count: detail.files_read.length },
119
+ },
120
+ bash_commands: detail.bash_commands,
121
+ last_messages: detail.last_messages
122
+ .slice(-SUMMARY_MAX_MESSAGES)
123
+ .map((m) => trimMessage(m)),
124
+ cursor: detail.cursor,
125
+ };
126
+ }
127
+ /** Project a full TaskStatusResult down to the compact summary shape. */
128
+ export function toTaskStatusSummary(result) {
129
+ return {
130
+ task_name: result.task_name,
131
+ agents: result.agents.map(toAgentStatusSummary),
132
+ summary: result.summary,
133
+ cursor: result.cursor,
134
+ };
135
+ }
58
136
  /** Spawn a new teammate in a task and return its initial metadata. */
59
137
  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, profileName = null) {
60
138
  const defaultMode = manager.getDefaultMode();