@phnx-labs/agents-cli 1.20.73 → 1.20.76

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 (209) hide show
  1. package/CHANGELOG.md +989 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +31 -1
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/export.d.ts +11 -0
  15. package/dist/commands/export.js +215 -0
  16. package/dist/commands/feed.d.ts +7 -1
  17. package/dist/commands/feed.js +47 -4
  18. package/dist/commands/go.d.ts +9 -2
  19. package/dist/commands/go.js +16 -6
  20. package/dist/commands/harness.d.ts +14 -0
  21. package/dist/commands/harness.js +145 -0
  22. package/dist/commands/import.js +87 -9
  23. package/dist/commands/inspect.js +5 -2
  24. package/dist/commands/profiles.d.ts +20 -0
  25. package/dist/commands/profiles.js +68 -21
  26. package/dist/commands/repo.js +14 -0
  27. package/dist/commands/routines.js +15 -2
  28. package/dist/commands/secrets.js +44 -21
  29. package/dist/commands/sessions-browser.js +1 -1
  30. package/dist/commands/sessions-export.js +19 -5
  31. package/dist/commands/sessions-migrate.d.ts +29 -0
  32. package/dist/commands/sessions-migrate.js +596 -0
  33. package/dist/commands/sessions-picker.js +16 -1
  34. package/dist/commands/sessions.d.ts +36 -13
  35. package/dist/commands/sessions.js +210 -40
  36. package/dist/commands/setup-computer.js +2 -2
  37. package/dist/commands/setup.js +6 -1
  38. package/dist/commands/ssh.js +71 -35
  39. package/dist/commands/status.js +10 -2
  40. package/dist/commands/versions.js +51 -11
  41. package/dist/commands/view.d.ts +5 -0
  42. package/dist/commands/view.js +99 -31
  43. package/dist/commands/watchdog.d.ts +7 -2
  44. package/dist/commands/watchdog.js +73 -57
  45. package/dist/index.js +59 -7
  46. package/dist/lib/activity.d.ts +14 -4
  47. package/dist/lib/activity.js +17 -5
  48. package/dist/lib/actor.d.ts +50 -0
  49. package/dist/lib/actor.js +166 -0
  50. package/dist/lib/agent-spec/provider.js +2 -1
  51. package/dist/lib/agent-spec/resolve.js +19 -5
  52. package/dist/lib/agent-spec/types.d.ts +9 -1
  53. package/dist/lib/agents.d.ts +56 -0
  54. package/dist/lib/agents.js +133 -7
  55. package/dist/lib/cloud/codex.d.ts +2 -0
  56. package/dist/lib/cloud/codex.js +14 -3
  57. package/dist/lib/cloud/session-index.d.ts +32 -0
  58. package/dist/lib/cloud/session-index.js +58 -0
  59. package/dist/lib/cloud/store.d.ts +7 -0
  60. package/dist/lib/cloud/store.js +25 -0
  61. package/dist/lib/config-transfer.d.ts +16 -0
  62. package/dist/lib/config-transfer.js +60 -0
  63. package/dist/lib/daemon.js +18 -2
  64. package/dist/lib/devices/connect.d.ts +15 -1
  65. package/dist/lib/devices/connect.js +15 -1
  66. package/dist/lib/devices/fleet.d.ts +21 -0
  67. package/dist/lib/devices/fleet.js +26 -0
  68. package/dist/lib/devices/health-report.d.ts +38 -0
  69. package/dist/lib/devices/health-report.js +214 -0
  70. package/dist/lib/devices/health.js +4 -1
  71. package/dist/lib/devices/reachability.d.ts +31 -0
  72. package/dist/lib/devices/reachability.js +40 -0
  73. package/dist/lib/devices/registry.d.ts +33 -0
  74. package/dist/lib/devices/registry.js +37 -0
  75. package/dist/lib/devices/resolve-target.d.ts +15 -27
  76. package/dist/lib/devices/resolve-target.js +63 -102
  77. package/dist/lib/devices/sync.d.ts +18 -0
  78. package/dist/lib/devices/sync.js +23 -1
  79. package/dist/lib/devices/tailscale.d.ts +3 -0
  80. package/dist/lib/devices/tailscale.js +1 -0
  81. package/dist/lib/events.d.ts +1 -1
  82. package/dist/lib/exec.d.ts +66 -0
  83. package/dist/lib/exec.js +195 -6
  84. package/dist/lib/export.d.ts +72 -0
  85. package/dist/lib/export.js +269 -0
  86. package/dist/lib/feed-post.d.ts +63 -0
  87. package/dist/lib/feed-post.js +204 -0
  88. package/dist/lib/fleet/apply.d.ts +32 -2
  89. package/dist/lib/fleet/apply.js +97 -10
  90. package/dist/lib/fleet/types.d.ts +11 -0
  91. package/dist/lib/fs-walk.d.ts +13 -0
  92. package/dist/lib/fs-walk.js +16 -7
  93. package/dist/lib/hooks.d.ts +1 -0
  94. package/dist/lib/hooks.js +156 -0
  95. package/dist/lib/hosts/dispatch.d.ts +7 -0
  96. package/dist/lib/hosts/dispatch.js +2 -0
  97. package/dist/lib/hosts/passthrough.js +2 -0
  98. package/dist/lib/hosts/reconnect.d.ts +78 -0
  99. package/dist/lib/hosts/reconnect.js +127 -0
  100. package/dist/lib/hosts/registry.d.ts +75 -14
  101. package/dist/lib/hosts/registry.js +205 -30
  102. package/dist/lib/hosts/remote-cmd.js +1 -0
  103. package/dist/lib/hosts/run-target.d.ts +13 -5
  104. package/dist/lib/hosts/run-target.js +26 -10
  105. package/dist/lib/hosts/session-index.d.ts +18 -1
  106. package/dist/lib/hosts/session-index.js +37 -2
  107. package/dist/lib/hosts/session-marker.d.ts +33 -0
  108. package/dist/lib/hosts/session-marker.js +51 -0
  109. package/dist/lib/hosts/tasks.d.ts +8 -4
  110. package/dist/lib/import.d.ts +25 -0
  111. package/dist/lib/import.js +107 -2
  112. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  113. package/dist/lib/isolation-boundary-report.js +35 -0
  114. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  115. package/dist/lib/menubar/install-menubar.js +45 -1
  116. package/dist/lib/migrate.d.ts +16 -0
  117. package/dist/lib/migrate.js +36 -0
  118. package/dist/lib/models.d.ts +27 -0
  119. package/dist/lib/models.js +54 -1
  120. package/dist/lib/overdue.d.ts +7 -2
  121. package/dist/lib/overdue.js +18 -11
  122. package/dist/lib/picker.js +4 -1
  123. package/dist/lib/platform/process.d.ts +17 -0
  124. package/dist/lib/platform/process.js +70 -0
  125. package/dist/lib/profiles-presets.js +9 -7
  126. package/dist/lib/profiles.d.ts +31 -0
  127. package/dist/lib/profiles.js +70 -0
  128. package/dist/lib/project-resources.js +20 -5
  129. package/dist/lib/pty-server.d.ts +2 -10
  130. package/dist/lib/pty-server.js +4 -38
  131. package/dist/lib/registry.d.ts +1 -1
  132. package/dist/lib/registry.js +48 -8
  133. package/dist/lib/rotate.d.ts +18 -0
  134. package/dist/lib/rotate.js +28 -0
  135. package/dist/lib/routines.d.ts +26 -0
  136. package/dist/lib/routines.js +19 -2
  137. package/dist/lib/runner.js +104 -7
  138. package/dist/lib/secrets/agent.d.ts +66 -1
  139. package/dist/lib/secrets/agent.js +154 -90
  140. package/dist/lib/secrets/bundles.d.ts +34 -0
  141. package/dist/lib/secrets/bundles.js +179 -19
  142. package/dist/lib/secrets/index.d.ts +15 -1
  143. package/dist/lib/secrets/index.js +21 -3
  144. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  145. package/dist/lib/secrets/rc-hygiene.js +154 -0
  146. package/dist/lib/secrets/remote.d.ts +7 -4
  147. package/dist/lib/secrets/remote.js +7 -4
  148. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  149. package/dist/lib/secrets/sync-commands.js +21 -0
  150. package/dist/lib/self-update.d.ts +23 -2
  151. package/dist/lib/self-update.js +86 -5
  152. package/dist/lib/session/active.d.ts +114 -7
  153. package/dist/lib/session/active.js +323 -52
  154. package/dist/lib/session/cloud.js +2 -1
  155. package/dist/lib/session/db.d.ts +48 -0
  156. package/dist/lib/session/db.js +238 -14
  157. package/dist/lib/session/detached.d.ts +30 -0
  158. package/dist/lib/session/detached.js +92 -0
  159. package/dist/lib/session/discover.d.ts +465 -2
  160. package/dist/lib/session/discover.js +1648 -468
  161. package/dist/lib/session/fork.js +2 -1
  162. package/dist/lib/session/hook-sessions.d.ts +43 -0
  163. package/dist/lib/session/hook-sessions.js +135 -0
  164. package/dist/lib/session/linear.d.ts +6 -0
  165. package/dist/lib/session/linear.js +55 -0
  166. package/dist/lib/session/migrate-targets.d.ts +65 -0
  167. package/dist/lib/session/migrate-targets.js +94 -0
  168. package/dist/lib/session/migrations.d.ts +37 -0
  169. package/dist/lib/session/migrations.js +60 -0
  170. package/dist/lib/session/parse.d.ts +18 -0
  171. package/dist/lib/session/parse.js +130 -30
  172. package/dist/lib/session/pid-registry.d.ts +34 -2
  173. package/dist/lib/session/pid-registry.js +49 -2
  174. package/dist/lib/session/render.d.ts +7 -2
  175. package/dist/lib/session/render.js +22 -10
  176. package/dist/lib/session/short-id.d.ts +17 -0
  177. package/dist/lib/session/short-id.js +20 -0
  178. package/dist/lib/session/state.js +6 -2
  179. package/dist/lib/shims.d.ts +27 -1
  180. package/dist/lib/shims.js +88 -6
  181. package/dist/lib/sqlite.d.ts +3 -2
  182. package/dist/lib/sqlite.js +27 -4
  183. package/dist/lib/staleness/detectors/commands.js +1 -1
  184. package/dist/lib/staleness/detectors/workflows.js +13 -2
  185. package/dist/lib/staleness/writers/commands.js +7 -7
  186. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  187. package/dist/lib/startup/command-registry.d.ts +2 -0
  188. package/dist/lib/startup/command-registry.js +5 -0
  189. package/dist/lib/state.d.ts +1 -1
  190. package/dist/lib/state.js +15 -36
  191. package/dist/lib/teams/agents.d.ts +19 -10
  192. package/dist/lib/teams/agents.js +40 -39
  193. package/dist/lib/types.d.ts +48 -2
  194. package/dist/lib/uninstall.js +1 -45
  195. package/dist/lib/usage.d.ts +33 -1
  196. package/dist/lib/usage.js +172 -12
  197. package/dist/lib/versions.d.ts +22 -2
  198. package/dist/lib/versions.js +98 -10
  199. package/dist/lib/watchdog/log.d.ts +43 -0
  200. package/dist/lib/watchdog/log.js +69 -0
  201. package/dist/lib/watchdog/routine.d.ts +44 -0
  202. package/dist/lib/watchdog/routine.js +69 -0
  203. package/dist/lib/watchdog/runner.d.ts +51 -7
  204. package/dist/lib/watchdog/runner.js +239 -64
  205. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  206. package/dist/lib/watchdog/watchdog.js +31 -16
  207. package/dist/lib/workflows.d.ts +16 -0
  208. package/dist/lib/workflows.js +110 -1
  209. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import type { EventRecord } from './events.js';
2
2
  /** Recognizable milestone events, ordered first in any activity lane. */
3
- export type MilestoneEvent = 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created';
3
+ export type MilestoneEvent = 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created'
4
+ /** Deliberate agent-authored progress post (`agents feed post`). */
5
+ | 'status.posted';
4
6
  /** Routine activity events, collapsed to counts by readers. */
5
7
  export type ActivityKind = 'file.edited';
6
8
  export type ActivityEventKind = MilestoneEvent | ActivityKind;
@@ -29,12 +31,20 @@ export interface ActivityEvent {
29
31
  cwd?: string;
30
32
  /** Agent that produced the event (claude, codex, ...). */
31
33
  agent?: string;
32
- /** Tool that triggered the event (Bash, Task, ExitPlanMode, ...). */
34
+ /** Tool that triggered the event (Bash, Task, ExitPlanMode, feed.post, ...). */
33
35
  tool?: string;
34
- /** One-line human summary (plan title, PR command, sub-agent role). */
36
+ /** One-line human summary (plan title, PR command, sub-agent role, status text). */
35
37
  detail?: string;
36
38
  /** Extracted URL when the event has one (e.g. the opened PR). */
37
39
  url?: string;
40
+ /** Auto-stamped process identity for deliberate posts (from pid registry / env). */
41
+ pid?: number;
42
+ /** Spawn-time join key (`AGENT_LAUNCH_ID`) when known. */
43
+ launchId?: string;
44
+ /** Factory terminal id when the launch inherited one. */
45
+ terminalId?: string;
46
+ /** `$TMUX_PANE` at launch when recorded. */
47
+ tmuxPane?: string;
38
48
  }
39
49
  /**
40
50
  * Append one event to a session's activity log. Primarily the Python hook
@@ -110,7 +120,7 @@ export declare function formatActivityLine(ev: ActivityEvent, opts?: {
110
120
  * Edit|MultiEdit) so read-only tools never pay the hook cost. Fail-open: any
111
121
  * error is swallowed so a logging hiccup never blocks a tool call.
112
122
  */
113
- export declare const ACTIVITY_LOG_HOOK_SCRIPT = "#!/usr/bin/env python3\n\"\"\"Append agent-activity events for `agents feed` / `agents activity`.\n\nBound to PreToolUse (ExitPlanMode, Task) and PostToolUse (Bash, Write, Edit,\nMultiEdit). One append-only file per session; read-only tools never trigger it\nbecause the manifest matcher excludes them.\n\nSub-agent gate: when the payload carries `agent_type`, this is a Task/Agent\nsub-agent -- skip so only the top-level agent logs its own activity.\n\nFail-open: ANY error is swallowed so a logging hiccup never blocks a tool call.\n\"\"\"\nimport os\nimport re\nimport sys\nimport json\nimport shlex\nimport socket\nfrom datetime import datetime, timezone\n\nMAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log\nMILESTONE_EVENTS = {\n \"plan.created\", \"pr.opened\", \"pr.merged\", \"worktree.created\",\n \"worktree.removed\", \"commit.created\", \"pushed\", \"subagent.spawned\",\n \"artifact.created\",\n}\n\n# Deliverable file types + locations -- a Write here is a recognizable artifact\n# (an HTML plan, a PDF report, a rendered image), not a routine code edit.\nARTIFACT_EXTS = {\n \".html\", \".htm\", \".pdf\", \".png\", \".jpg\", \".jpeg\", \".gif\", \".svg\",\n \".webp\", \".mp4\", \".mov\", \".webm\", \".csv\", \".xlsx\", \".pptx\", \".docx\",\n}\nARTIFACT_DIR_HINTS = (\"/tmp/\", \"/downloads/\", \"/.agents/artifacts/\")\n\n\ndef is_artifact(file_path):\n low = (file_path or \"\").lower()\n if os.path.splitext(low)[1] in ARTIFACT_EXTS:\n return True\n return any(hint in low for hint in ARTIFACT_DIR_HINTS)\n\n\ndef first_line(text, limit=140):\n for raw in (text or \"\").splitlines():\n s = raw.strip().lstrip(\"#\").strip()\n if s:\n return s[:limit]\n return \"\"\n\n\ndef _subcommand(tokens, tool):\n \"\"\"First non-flag token after `tool`, i.e. its subcommand (skips -C <path>,\n -c <cfg>, and other leading flags). None if `tool` isn't the invoked command.\"\"\"\n try:\n i = tokens.index(tool) + 1\n except ValueError:\n return None, []\n while i < len(tokens):\n t = tokens[i]\n if t in (\"-C\", \"-c\", \"--git-dir\", \"--work-tree\"):\n i += 2 # flag that consumes the next token\n continue\n if t.startswith(\"-\"):\n i += 1\n continue\n return t, tokens[i + 1:]\n return None, []\n\n\ndef classify_bash(command):\n \"\"\"Return the milestone event for a git/gh command, else None. Tokenizes so a\n path like `git diff -- src/commit.ts` is not mistaken for a commit.\"\"\"\n try:\n tokens = shlex.split(command or \"\")\n except Exception:\n tokens = (command or \"\").split()\n\n verb, rest = _subcommand(tokens, \"git\")\n if verb == \"worktree\":\n sub = rest[0] if rest else \"\"\n if sub == \"add\":\n return \"worktree.created\"\n if sub == \"remove\":\n return \"worktree.removed\"\n elif verb == \"commit\":\n return \"commit.created\"\n elif verb == \"push\":\n return \"pushed\"\n\n verb, rest = _subcommand(tokens, \"gh\")\n if verb == \"pr\" and rest:\n if rest[0] == \"create\":\n return \"pr.opened\"\n if rest[0] == \"merge\":\n return \"pr.merged\"\n return None\n\n\ndef extract_url(tool_response):\n \"\"\"Pull the first https URL out of a Bash tool response (stdout).\"\"\"\n text = \"\"\n if isinstance(tool_response, dict):\n text = str(tool_response.get(\"stdout\") or tool_response.get(\"output\") or \"\")\n elif isinstance(tool_response, str):\n text = tool_response\n m = re.search(r\"https?://\\S+\", text)\n return m.group(0).rstrip(\").,\") if m else None\n\n\ndef build_event(payload, hook_event):\n tool_name = payload.get(\"tool_name\", \"\")\n tool_input = payload.get(\"tool_input\", {}) or {}\n tool_response = payload.get(\"tool_response\", {})\n\n event = None\n detail = None\n url = None\n\n if hook_event == \"PreToolUse\":\n if tool_name == \"ExitPlanMode\":\n event = \"plan.created\"\n detail = first_line(tool_input.get(\"plan\", \"\")) or \"plan presented\"\n elif tool_name == \"Task\":\n event = \"subagent.spawned\"\n role = tool_input.get(\"subagent_type\") or \"agent\"\n desc = tool_input.get(\"description\") or tool_input.get(\"prompt\") or \"\"\n detail = (role + \": \" + first_line(desc)).strip(\": \").strip()\n elif hook_event == \"PostToolUse\":\n if tool_name == \"Bash\":\n event = classify_bash(tool_input.get(\"command\", \"\"))\n if event:\n detail = first_line(tool_input.get(\"command\", \"\"))\n url = extract_url(tool_response)\n elif tool_name in (\"Write\", \"Edit\", \"MultiEdit\"):\n fp = tool_input.get(\"file_path\") or tool_input.get(\"path\") or \"\"\n # A freshly-written deliverable is a milestone; edits and code\n # writes stay routine and collapse to a count.\n if tool_name == \"Write\" and is_artifact(fp):\n event = \"artifact.created\"\n else:\n event = \"file.edited\"\n detail = os.path.basename(fp) if fp else tool_name\n\n if not event:\n return None, tool_name\n\n tier = \"milestone\" if event in MILESTONE_EVENTS else \"activity\"\n record = {\n \"v\": 1,\n \"ts\": datetime.now(timezone.utc).isoformat(),\n \"event\": event,\n \"tier\": tier,\n }\n if detail:\n record[\"detail\"] = detail\n if url:\n record[\"url\"] = url\n record[\"tool\"] = tool_name\n return record, tool_name\n\n\ndef main():\n raw = sys.stdin.read()\n try:\n payload = json.loads(raw) if raw.strip() else {}\n except Exception:\n return\n\n # Sub-agent gate -- only the top-level agent logs.\n if payload.get(\"agent_type\"):\n return\n\n session_id = payload.get(\"session_id\", \"\")\n if not session_id:\n return\n\n hook_event = payload.get(\"hook_event_name\", \"\")\n record, tool_name = build_event(payload, hook_event)\n if not record:\n return\n\n safe_session = re.sub(r\"[^A-Za-z0-9._-]\", \"-\", session_id) or \"unknown\"\n home = os.environ.get(\"HOME\") or os.path.expanduser(\"~\")\n activity_dir = os.path.join(home, \".agents\", \".history\", \"activity\")\n target = os.path.join(activity_dir, safe_session + \".jsonl\")\n\n # Identity (mirrors 10-feed-publish.py).\n mailbox_id = os.path.basename(\n os.environ.get(\"AGENTS_MAILBOX_DIR\", \"\").rstrip(\"/\")\n ) or session_id\n hostname = os.environ.get(\"AGENTS_SYNC_MACHINE_ID\") or socket.gethostname()\n host = re.sub(r\"[^a-z0-9_-]\", \"-\", hostname.split(\".\")[0].strip().lower()) or \"unknown\"\n\n record[\"sessionId\"] = session_id\n record[\"mailboxId\"] = mailbox_id\n record[\"host\"] = host\n record[\"runtime\"] = os.environ.get(\"AGENTS_RUNTIME\", \"headless\")\n cwd = payload.get(\"cwd\") or os.environ.get(\"AGENTS_CWD\")\n if cwd:\n record[\"cwd\"] = cwd\n agent = os.environ.get(\"AGENTS_AGENT_NAME\") or \"claude\"\n record[\"agent\"] = agent\n\n try:\n os.makedirs(activity_dir, exist_ok=True)\n # Cap growth: once over the size limit, keep only milestones.\n if record[\"tier\"] != \"milestone\":\n try:\n if os.path.getsize(target) > MAX_LOG_BYTES:\n return\n except OSError:\n pass\n with open(target, \"a\") as f:\n f.write(json.dumps(record) + \"\\n\")\n except Exception:\n pass # fail open\n\n\nif __name__ == \"__main__\":\n try:\n main()\n except Exception:\n pass # fail open\n";
123
+ export declare const ACTIVITY_LOG_HOOK_SCRIPT = "#!/usr/bin/env python3\n\"\"\"Append agent-activity events for `agents feed` / `agents activity`.\n\nBound to PreToolUse (ExitPlanMode, Task) and PostToolUse (Bash, Write, Edit,\nMultiEdit). One append-only file per session; read-only tools never trigger it\nbecause the manifest matcher excludes them.\n\nSub-agent gate: when the payload carries `agent_type`, this is a Task/Agent\nsub-agent -- skip so only the top-level agent logs its own activity.\n\nFail-open: ANY error is swallowed so a logging hiccup never blocks a tool call.\n\"\"\"\nimport os\nimport re\nimport sys\nimport json\nimport shlex\nimport socket\nfrom datetime import datetime, timezone\n\nMAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log\nMILESTONE_EVENTS = {\n \"plan.created\", \"pr.opened\", \"pr.merged\", \"worktree.created\",\n \"worktree.removed\", \"commit.created\", \"pushed\", \"subagent.spawned\",\n \"artifact.created\", \"status.posted\",\n}\n\n# Deliverable file types + locations -- a Write here is a recognizable artifact\n# (an HTML plan, a PDF report, a rendered image), not a routine code edit.\nARTIFACT_EXTS = {\n \".html\", \".htm\", \".pdf\", \".png\", \".jpg\", \".jpeg\", \".gif\", \".svg\",\n \".webp\", \".mp4\", \".mov\", \".webm\", \".csv\", \".xlsx\", \".pptx\", \".docx\",\n}\nARTIFACT_DIR_HINTS = (\"/tmp/\", \"/downloads/\", \"/.agents/artifacts/\")\n\n\ndef is_artifact(file_path):\n low = (file_path or \"\").lower()\n if os.path.splitext(low)[1] in ARTIFACT_EXTS:\n return True\n return any(hint in low for hint in ARTIFACT_DIR_HINTS)\n\n\ndef first_line(text, limit=140):\n for raw in (text or \"\").splitlines():\n s = raw.strip().lstrip(\"#\").strip()\n if s:\n return s[:limit]\n return \"\"\n\n\ndef _subcommand(tokens, tool):\n \"\"\"First non-flag token after `tool`, i.e. its subcommand (skips -C <path>,\n -c <cfg>, and other leading flags). None if `tool` isn't the invoked command.\"\"\"\n try:\n i = tokens.index(tool) + 1\n except ValueError:\n return None, []\n while i < len(tokens):\n t = tokens[i]\n if t in (\"-C\", \"-c\", \"--git-dir\", \"--work-tree\"):\n i += 2 # flag that consumes the next token\n continue\n if t.startswith(\"-\"):\n i += 1\n continue\n return t, tokens[i + 1:]\n return None, []\n\n\ndef classify_bash(command):\n \"\"\"Return the milestone event for a git/gh command, else None. Tokenizes so a\n path like `git diff -- src/commit.ts` is not mistaken for a commit.\"\"\"\n try:\n tokens = shlex.split(command or \"\")\n except Exception:\n tokens = (command or \"\").split()\n\n verb, rest = _subcommand(tokens, \"git\")\n if verb == \"worktree\":\n sub = rest[0] if rest else \"\"\n if sub == \"add\":\n return \"worktree.created\"\n if sub == \"remove\":\n return \"worktree.removed\"\n elif verb == \"commit\":\n return \"commit.created\"\n elif verb == \"push\":\n return \"pushed\"\n\n verb, rest = _subcommand(tokens, \"gh\")\n if verb == \"pr\" and rest:\n if rest[0] == \"create\":\n return \"pr.opened\"\n if rest[0] == \"merge\":\n return \"pr.merged\"\n return None\n\n\ndef extract_url(tool_response):\n \"\"\"Pull the first https URL out of a Bash tool response (stdout).\"\"\"\n text = \"\"\n if isinstance(tool_response, dict):\n text = str(tool_response.get(\"stdout\") or tool_response.get(\"output\") or \"\")\n elif isinstance(tool_response, str):\n text = tool_response\n m = re.search(r\"https?://\\S+\", text)\n return m.group(0).rstrip(\").,\") if m else None\n\n\ndef build_event(payload, hook_event):\n tool_name = payload.get(\"tool_name\", \"\")\n tool_input = payload.get(\"tool_input\", {}) or {}\n tool_response = payload.get(\"tool_response\", {})\n\n event = None\n detail = None\n url = None\n\n if hook_event == \"PreToolUse\":\n if tool_name == \"ExitPlanMode\":\n event = \"plan.created\"\n detail = first_line(tool_input.get(\"plan\", \"\")) or \"plan presented\"\n elif tool_name == \"Task\":\n event = \"subagent.spawned\"\n role = tool_input.get(\"subagent_type\") or \"agent\"\n desc = tool_input.get(\"description\") or tool_input.get(\"prompt\") or \"\"\n detail = (role + \": \" + first_line(desc)).strip(\": \").strip()\n elif hook_event == \"PostToolUse\":\n if tool_name == \"Bash\":\n event = classify_bash(tool_input.get(\"command\", \"\"))\n if event:\n detail = first_line(tool_input.get(\"command\", \"\"))\n url = extract_url(tool_response)\n elif tool_name in (\"Write\", \"Edit\", \"MultiEdit\"):\n fp = tool_input.get(\"file_path\") or tool_input.get(\"path\") or \"\"\n # A freshly-written deliverable is a milestone; edits and code\n # writes stay routine and collapse to a count.\n if tool_name == \"Write\" and is_artifact(fp):\n event = \"artifact.created\"\n else:\n event = \"file.edited\"\n detail = os.path.basename(fp) if fp else tool_name\n\n if not event:\n return None, tool_name\n\n tier = \"milestone\" if event in MILESTONE_EVENTS else \"activity\"\n record = {\n \"v\": 1,\n \"ts\": datetime.now(timezone.utc).isoformat(),\n \"event\": event,\n \"tier\": tier,\n }\n if detail:\n record[\"detail\"] = detail\n if url:\n record[\"url\"] = url\n record[\"tool\"] = tool_name\n return record, tool_name\n\n\ndef main():\n raw = sys.stdin.read()\n try:\n payload = json.loads(raw) if raw.strip() else {}\n except Exception:\n return\n\n # Sub-agent gate -- only the top-level agent logs.\n if payload.get(\"agent_type\"):\n return\n\n session_id = payload.get(\"session_id\", \"\")\n if not session_id:\n return\n\n hook_event = payload.get(\"hook_event_name\", \"\")\n record, tool_name = build_event(payload, hook_event)\n if not record:\n return\n\n safe_session = re.sub(r\"[^A-Za-z0-9._-]\", \"-\", session_id) or \"unknown\"\n home = os.environ.get(\"HOME\") or os.path.expanduser(\"~\")\n activity_dir = os.path.join(home, \".agents\", \".history\", \"activity\")\n target = os.path.join(activity_dir, safe_session + \".jsonl\")\n\n # Identity (mirrors 10-feed-publish.py).\n mailbox_id = os.path.basename(\n os.environ.get(\"AGENTS_MAILBOX_DIR\", \"\").rstrip(\"/\")\n ) or session_id\n hostname = os.environ.get(\"AGENTS_SYNC_MACHINE_ID\") or socket.gethostname()\n host = re.sub(r\"[^a-z0-9_-]\", \"-\", hostname.split(\".\")[0].strip().lower()) or \"unknown\"\n\n record[\"sessionId\"] = session_id\n record[\"mailboxId\"] = mailbox_id\n record[\"host\"] = host\n record[\"runtime\"] = os.environ.get(\"AGENTS_RUNTIME\", \"headless\")\n cwd = payload.get(\"cwd\") or os.environ.get(\"AGENTS_CWD\")\n if cwd:\n record[\"cwd\"] = cwd\n agent = os.environ.get(\"AGENTS_AGENT_NAME\") or \"claude\"\n record[\"agent\"] = agent\n\n try:\n os.makedirs(activity_dir, exist_ok=True)\n # Cap growth: once over the size limit, keep only milestones.\n if record[\"tier\"] != \"milestone\":\n try:\n if os.path.getsize(target) > MAX_LOG_BYTES:\n return\n except OSError:\n pass\n with open(target, \"a\") as f:\n f.write(json.dumps(record) + \"\\n\")\n except Exception:\n pass # fail open\n\n\nif __name__ == \"__main__\":\n try:\n main()\n except Exception:\n pass # fail open\n";
114
124
  /** Hook manifest entries (agents.yaml shape) for the activity-log hook. */
115
125
  export declare const ACTIVITY_HOOK_DEFINITIONS: Record<string, Record<string, unknown>>;
116
126
  /**
@@ -34,6 +34,7 @@ export const MILESTONE_EVENTS = [
34
34
  'pushed',
35
35
  'subagent.spawned',
36
36
  'artifact.created',
37
+ 'status.posted',
37
38
  ];
38
39
  const MILESTONE_SET = new Set(MILESTONE_EVENTS);
39
40
  export function tierForEvent(event) {
@@ -82,6 +83,10 @@ function parseLine(line) {
82
83
  tool: parsed.tool,
83
84
  detail: parsed.detail,
84
85
  url: parsed.url,
86
+ pid: typeof parsed.pid === 'number' ? parsed.pid : undefined,
87
+ launchId: parsed.launchId,
88
+ terminalId: parsed.terminalId,
89
+ tmuxPane: parsed.tmuxPane,
85
90
  };
86
91
  }
87
92
  catch {
@@ -193,11 +198,11 @@ export function activityEventToRecord(ev) {
193
198
  hostname: ev.host,
194
199
  platform: process.platform,
195
200
  arch: process.arch,
196
- pid: 0,
201
+ pid: ev.pid ?? 0,
197
202
  ppid: 0,
198
203
  event: ev.event,
199
204
  level: 'info',
200
- caller: 'hook',
205
+ caller: ev.tool === 'feed.post' ? 'agent' : 'hook',
201
206
  session: ev.sessionId,
202
207
  osUser: ev.agent ?? 'agent',
203
208
  transport: 'local',
@@ -210,6 +215,9 @@ export function activityEventToRecord(ev) {
210
215
  detail: ev.detail,
211
216
  url: ev.url,
212
217
  tier: ev.tier,
218
+ ...(ev.launchId ? { launchId: ev.launchId } : {}),
219
+ ...(ev.terminalId ? { terminalId: ev.terminalId } : {}),
220
+ ...(ev.tmuxPane ? { tmuxPane: ev.tmuxPane } : {}),
213
221
  };
214
222
  }
215
223
  /** Read recent activity across sessions as unified {@link EventRecord}s. */
@@ -230,6 +238,7 @@ export const EVENT_STYLE = {
230
238
  'pushed': { glyph: '↥', color: chalk.yellow, label: 'pushed' },
231
239
  'subagent.spawned': { glyph: '⑂', color: chalk.magenta, label: 'sub-agent spawned' },
232
240
  'artifact.created': { glyph: '▤', color: chalk.cyan, label: 'artifact' },
241
+ 'status.posted': { glyph: '▸', color: chalk.white, label: 'status' },
233
242
  'file.edited': { glyph: '·', color: chalk.gray, label: 'file edited' },
234
243
  };
235
244
  export function styleForEvent(event) {
@@ -240,10 +249,13 @@ export function formatActivityLine(ev, opts = {}) {
240
249
  const s = styleForEvent(ev.event);
241
250
  const host = opts.showHost && ev.host && ev.host !== 'unknown' ? chalk.gray(`[${ev.host}] `) : '';
242
251
  const label = s.color(`${s.glyph} ${s.label}`);
243
- const detail = ev.detail ? ` ${truncate(ev.detail, 60)}` : '';
252
+ // Status posts are the message; allow a longer snippet than tool-derived detail.
253
+ const detailLimit = ev.event === 'status.posted' ? 100 : 60;
254
+ const detail = ev.detail ? ` ${truncate(ev.detail, detailLimit)}` : '';
244
255
  const url = ev.url ? chalk.gray(` ${ev.url}`) : '';
256
+ const agent = ev.event === 'status.posted' && ev.agent ? chalk.gray(` · ${ev.agent}`) : '';
245
257
  const when = chalk.gray(relTime(ev.ts).padStart(7));
246
- return ` ${when} ${host}${label}${detail}${url}`;
258
+ return ` ${when} ${host}${label}${detail}${agent}${url}`;
247
259
  }
248
260
  // ---------------------------------------------------------------------------
249
261
  // Hook installation
@@ -281,7 +293,7 @@ MAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log
281
293
  MILESTONE_EVENTS = {
282
294
  "plan.created", "pr.opened", "pr.merged", "worktree.created",
283
295
  "worktree.removed", "commit.created", "pushed", "subagent.spawned",
284
- "artifact.created",
296
+ "artifact.created", "status.posted",
285
297
  }
286
298
 
287
299
  # Deliverable file types + locations -- a Write here is a recognizable artifact
@@ -0,0 +1,50 @@
1
+ import type { ActorConfig } from './types.js';
2
+ export type ActorKind = 'human' | 'agent';
3
+ export interface ResolvedActor {
4
+ /**
5
+ * Stable id for the responsible entity: the tailnet login (usually an email)
6
+ * for a resolved human, or `UNRESOLVED@<host>` when it can't be determined.
7
+ */
8
+ id: string;
9
+ kind: ActorKind;
10
+ /** Human-readable name, for git author + display. */
11
+ name?: string;
12
+ /** Email, for git author + as a durable key. */
13
+ email?: string;
14
+ /** GitHub handle, when the actors map records one. */
15
+ github?: string;
16
+ }
17
+ /** Result of `tailscale whois --json <ip>` we care about. */
18
+ export interface WhoisIdentity {
19
+ login?: string;
20
+ displayName?: string;
21
+ }
22
+ /**
23
+ * Map a resolved tailnet identity (+ the actors map) to a ResolvedActor. Pure --
24
+ * the impure whois/config reads happen in computeActor -- so the enrich/override
25
+ * path is fully testable. No login (local, or an unnameable peer) yields the
26
+ * honest `UNRESOLVED@<host>`; a login without a config entry still credits git
27
+ * from the tailnet DisplayName + login email.
28
+ */
29
+ export declare function actorFromIdentity(who: WhoisIdentity | undefined, host: string, actors: Record<string, ActorConfig>): ResolvedActor;
30
+ /**
31
+ * Compute the actor for a given environment. Pure with respect to `env` (the
32
+ * only impurity is the `tailscale whois` / config read on the fresh-SSH path),
33
+ * so tests can drive every branch by passing an env explicitly.
34
+ */
35
+ export declare function computeActor(env?: NodeJS.ProcessEnv): ResolvedActor;
36
+ /**
37
+ * Resolve the actor for the current process, cached for the process lifetime
38
+ * (the SSH `whois` shell-out runs at most once).
39
+ */
40
+ export declare function resolveActor(): ResolvedActor;
41
+ /** Clear the per-process cache. For tests, and for env changes within a run. */
42
+ export declare function resetActorCache(): void;
43
+ /**
44
+ * The env an actor propagates to child processes. Always carries the actor id +
45
+ * kind (so children inherit and don't re-resolve). For a resolved human with a
46
+ * real name and email, it also carries `GIT_AUTHOR_*` / `GIT_COMMITTER_*` so the
47
+ * agent's own commits are credited to the person, not the shared account. An
48
+ * unresolved actor sets no git identity -- git keeps its ambient config.
49
+ */
50
+ export declare function actorEnv(actor: ResolvedActor): Record<string, string>;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Actor provenance -- who initiated a run.
3
+ *
4
+ * One shared account means every session, commit, and event otherwise shows up
5
+ * as the same person. `resolveActor()` answers "which human is behind this run"
6
+ * by looking at how the process was reached:
7
+ *
8
+ * - Over SSH (the shared-fleet case): `tailscale whois` the SSH client IP to
9
+ * the connecting tailnet identity -- a real name + login email.
10
+ * - Locally (non-SSH): we can't honestly say who is at the box, so the id is
11
+ * `UNRESOLVED@<host>` and no personal git identity is claimed.
12
+ * - Inherited: a child spawn trusts the `AGENTS_ACTOR*` env its parent
13
+ * stamped rather than re-resolving, so the whole spawn tree shares one actor.
14
+ *
15
+ * The resolved actor rides the child-process env (`actorEnv()`, merged into
16
+ * `buildExecEnv`). When the actor is a resolved human, that env also carries
17
+ * `GIT_AUTHOR_*` / `GIT_COMMITTER_*`, so the agent's own `git commit` is credited
18
+ * to the person instead of the shared account.
19
+ *
20
+ * The optional `actors:` map in agents.yaml enriches or overrides a resolved
21
+ * identity (pin a work email, add a github handle, mark an entry as an agent).
22
+ */
23
+ import { spawnSync } from 'child_process';
24
+ import { machineId } from './machine-id.js';
25
+ import { parseSshConnection } from './session/provenance.js';
26
+ import { readMeta } from './state.js';
27
+ /** Hard cap on the whois shell-out — it runs on the spawn hot path. */
28
+ const WHOIS_TIMEOUT_MS = 2000;
29
+ /**
30
+ * Resolve the tailnet identity behind an IP via `tailscale whois`. Returns
31
+ * undefined when tailscale is absent, the peer is unknown, the call fails, or it
32
+ * exceeds WHOIS_TIMEOUT_MS -- every one of those falls back to an unresolved
33
+ * actor, never an error and never a hang (a wedged tailscaled is timed out, not
34
+ * waited on, since this sits in buildExecEnv on every agent spawn).
35
+ */
36
+ function tailscaleWhois(ip) {
37
+ try {
38
+ const res = spawnSync('tailscale', ['whois', '--json', ip], {
39
+ encoding: 'utf-8',
40
+ windowsHide: true,
41
+ timeout: WHOIS_TIMEOUT_MS,
42
+ });
43
+ // A timeout leaves status null (child killed by SIGTERM) -- the status guard
44
+ // below treats it as an unresolved actor, same as any other failure.
45
+ if (res.status !== 0 || !res.stdout)
46
+ return undefined;
47
+ const data = JSON.parse(res.stdout);
48
+ const up = data.UserProfile;
49
+ if (!up)
50
+ return undefined;
51
+ return { login: up.LoginName, displayName: up.DisplayName };
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ /** Read the actors map from config, tolerant of a missing/unreadable config. */
58
+ function readActors() {
59
+ try {
60
+ return readMeta().actors ?? {};
61
+ }
62
+ catch {
63
+ return {};
64
+ }
65
+ }
66
+ /**
67
+ * Find the actors-map entry for a resolved tailnet login. Matches on an entry's
68
+ * explicit `login`, its map key, or its `email` (all case-insensitive).
69
+ */
70
+ function findActorConfig(login, actors) {
71
+ const needle = login.toLowerCase();
72
+ for (const [key, cfg] of Object.entries(actors)) {
73
+ const candidates = [cfg.login, key, cfg.email].filter((v) => !!v);
74
+ if (candidates.some((c) => c.toLowerCase() === needle))
75
+ return cfg;
76
+ }
77
+ return undefined;
78
+ }
79
+ /**
80
+ * Map a resolved tailnet identity (+ the actors map) to a ResolvedActor. Pure --
81
+ * the impure whois/config reads happen in computeActor -- so the enrich/override
82
+ * path is fully testable. No login (local, or an unnameable peer) yields the
83
+ * honest `UNRESOLVED@<host>`; a login without a config entry still credits git
84
+ * from the tailnet DisplayName + login email.
85
+ */
86
+ export function actorFromIdentity(who, host, actors) {
87
+ const login = who?.login;
88
+ if (!login) {
89
+ return { id: `UNRESOLVED@${host}`, kind: 'human' };
90
+ }
91
+ const cfg = findActorConfig(login, actors);
92
+ const emailFromLogin = login.includes('@') ? login : undefined;
93
+ return {
94
+ id: login,
95
+ kind: cfg?.kind ?? 'human',
96
+ name: cfg?.name ?? who?.displayName,
97
+ email: cfg?.email ?? emailFromLogin,
98
+ github: cfg?.github,
99
+ };
100
+ }
101
+ /** Reconstruct an actor an ancestor process already resolved into the env. */
102
+ function inheritedActor(env) {
103
+ const id = env.AGENTS_ACTOR;
104
+ if (!id)
105
+ return undefined;
106
+ return {
107
+ id,
108
+ kind: env.AGENTS_ACTOR_KIND === 'agent' ? 'agent' : 'human',
109
+ name: env.AGENTS_ACTOR_NAME || undefined,
110
+ email: env.AGENTS_ACTOR_EMAIL || undefined,
111
+ github: env.AGENTS_ACTOR_GITHUB || undefined,
112
+ };
113
+ }
114
+ /**
115
+ * Compute the actor for a given environment. Pure with respect to `env` (the
116
+ * only impurity is the `tailscale whois` / config read on the fresh-SSH path),
117
+ * so tests can drive every branch by passing an env explicitly.
118
+ */
119
+ export function computeActor(env = process.env) {
120
+ const inherited = inheritedActor(env);
121
+ if (inherited)
122
+ return inherited;
123
+ const ssh = env.SSH_CONNECTION ? parseSshConnection(env.SSH_CONNECTION) : undefined;
124
+ const who = ssh?.clientIp ? tailscaleWhois(ssh.clientIp) : undefined;
125
+ return actorFromIdentity(who, machineId(), readActors());
126
+ }
127
+ let cached;
128
+ /**
129
+ * Resolve the actor for the current process, cached for the process lifetime
130
+ * (the SSH `whois` shell-out runs at most once).
131
+ */
132
+ export function resolveActor() {
133
+ if (!cached)
134
+ cached = computeActor(process.env);
135
+ return cached;
136
+ }
137
+ /** Clear the per-process cache. For tests, and for env changes within a run. */
138
+ export function resetActorCache() {
139
+ cached = undefined;
140
+ }
141
+ /**
142
+ * The env an actor propagates to child processes. Always carries the actor id +
143
+ * kind (so children inherit and don't re-resolve). For a resolved human with a
144
+ * real name and email, it also carries `GIT_AUTHOR_*` / `GIT_COMMITTER_*` so the
145
+ * agent's own commits are credited to the person, not the shared account. An
146
+ * unresolved actor sets no git identity -- git keeps its ambient config.
147
+ */
148
+ export function actorEnv(actor) {
149
+ const env = {
150
+ AGENTS_ACTOR: actor.id,
151
+ AGENTS_ACTOR_KIND: actor.kind,
152
+ };
153
+ if (actor.name)
154
+ env.AGENTS_ACTOR_NAME = actor.name;
155
+ if (actor.email)
156
+ env.AGENTS_ACTOR_EMAIL = actor.email;
157
+ if (actor.github)
158
+ env.AGENTS_ACTOR_GITHUB = actor.github;
159
+ if (actor.kind === 'human' && actor.name && actor.email) {
160
+ env.GIT_AUTHOR_NAME = actor.name;
161
+ env.GIT_AUTHOR_EMAIL = actor.email;
162
+ env.GIT_COMMITTER_NAME = actor.name;
163
+ env.GIT_COMMITTER_EMAIL = actor.email;
164
+ }
165
+ return env;
166
+ }
@@ -1,9 +1,10 @@
1
1
  // The production VersionProvider: the only fs/meta-coupled file in the engine.
2
2
  // Everything else in this module is pure and testable without it.
3
- import { listInstalledVersions, getGlobalDefault, getProjectVersion, isVersionInstalled, } from '../versions.js';
3
+ import { listInstalledVersions, getGlobalDefault, getIsolatedDefault, getProjectVersion, isVersionInstalled, } from '../versions.js';
4
4
  export const defaultVersionProvider = {
5
5
  listInstalled: (agent) => listInstalledVersions(agent),
6
6
  getProjectVersion: (agent, cwd) => getProjectVersion(agent, cwd),
7
7
  getGlobalDefault: (agent) => getGlobalDefault(agent),
8
+ getIsolatedDefault: (agent) => getIsolatedDefault(agent),
8
9
  isInstalled: (agent, version) => isVersionInstalled(agent, version),
9
10
  };
@@ -66,6 +66,15 @@ export function resolveAgentTargets(spec, provider, opts = {}) {
66
66
  push(agent, glob, 'global-default');
67
67
  continue;
68
68
  }
69
+ // An isolated-only agent never has a global default — that is the point — so
70
+ // without this step `--agents codex` threw "No default version set" at a user
71
+ // who had explicitly run `agents use codex@<v>`. resolveVersion already had
72
+ // this fallback; the two resolvers had drifted apart.
73
+ const iso = provider.getIsolatedDefault(agent);
74
+ if (iso) {
75
+ push(agent, iso, 'isolated-default');
76
+ continue;
77
+ }
69
78
  const installed = provider.listInstalled(agent);
70
79
  if (installed.length === 0) {
71
80
  push(agent, null, 'none');
@@ -81,13 +90,17 @@ export function resolveAgentTargets(spec, provider, opts = {}) {
81
90
  }
82
91
  throw new AgentSpecError(`No default version set for ${name}. Specify one (${agent}@<version>) or set it: agents use ${agent}@<version>.`, 'no-default', agent, installed);
83
92
  }
84
- // ----- @pinned / @default: the configured global default -----
93
+ // ----- @pinned / @default: the configured default (global, else isolated) -----
85
94
  if (qualifier === 'pinned' || qualifier === 'default') {
86
- const def = provider.getGlobalDefault(agent);
95
+ const glob = provider.getGlobalDefault(agent);
96
+ // Report which kind it was: an isolated default owns none of the launcher /
97
+ // shim / config-symlink machinery a global default does, and callers that log
98
+ // the source shouldn't claim otherwise.
99
+ const def = glob ?? provider.getIsolatedDefault(agent);
87
100
  if (!def) {
88
101
  throw new AgentSpecError(`No default version set for ${name}. Run: agents use ${agent}@<version>`, 'no-default', agent, provider.listInstalled(agent));
89
102
  }
90
- push(agent, def, 'global-default(@pinned)');
103
+ push(agent, def, glob ? 'global-default(@pinned)' : 'isolated-default');
91
104
  continue;
92
105
  }
93
106
  // ----- @all: every installed version -----
@@ -168,7 +181,8 @@ export function resolveListFilter(agent, qualifier, provider, opts = {}) {
168
181
  const q = qualifier?.trim();
169
182
  if (!q || q === 'any')
170
183
  return undefined;
171
- if (q === 'default' || q === 'pinned')
172
- return provider.getGlobalDefault(agent) ?? undefined;
184
+ if (q === 'default' || q === 'pinned') {
185
+ return provider.getGlobalDefault(agent) ?? provider.getIsolatedDefault(agent) ?? undefined;
186
+ }
173
187
  return resolveSingleAgentTarget(`${agent}@${q}`, provider, { ...opts, availableAgents: [agent] }).version;
174
188
  }
@@ -1,6 +1,6 @@
1
1
  import type { AgentId } from '../types.js';
2
2
  /** How a resolved version was arrived at — for provenance, display, and error rendering. */
3
- export type VersionSource = 'explicit' | 'project-pin' | 'global-default' | 'global-default(@pinned)' | 'sole-installed' | 'newest-installed' | 'alias-latest' | 'alias-oldest' | 'none';
3
+ export type VersionSource = 'explicit' | 'project-pin' | 'global-default' | 'isolated-default' | 'global-default(@pinned)' | 'sole-installed' | 'newest-installed' | 'alias-latest' | 'alias-oldest' | 'none';
4
4
  export interface AgentTarget {
5
5
  agent: AgentId;
6
6
  /** Resolved exact version, or null when the agent has no installed versions yet. */
@@ -32,6 +32,14 @@ export interface VersionProvider {
32
32
  getProjectVersion(agent: AgentId, cwd: string): string | null;
33
33
  /** The configured global default version, or null. */
34
34
  getGlobalDefault(agent: AgentId): string | null;
35
+ /**
36
+ * The preferred isolated copy. Kept separate from getGlobalDefault on purpose: a
37
+ * global default owns the launcher, the bare shim and the real ~/.<agent> config
38
+ * symlink, and an isolated version must never acquire any of those. Folding the
39
+ * two together here would hand an isolated version back to every caller that
40
+ * reasonably assumes a global default means "the one that owns the launcher".
41
+ */
42
+ getIsolatedDefault(agent: AgentId): string | null;
35
43
  /** Whether an exact version is installed. */
36
44
  isInstalled(agent: AgentId, version: string): boolean;
37
45
  }
@@ -71,6 +71,17 @@ export declare function getCliPath(agentId: AgentId): Promise<string | null>;
71
71
  * back to a plain PATH lookup.
72
72
  */
73
73
  export declare function getCliState(agentId: AgentId): Promise<CliState>;
74
+ /**
75
+ * Resolve the agent's OWN install — the one agents-cli does not manage — by plain
76
+ * PATH lookup, ignoring the version dirs entirely.
77
+ *
78
+ * Callers that specifically mean "the user's own globally-installed CLI" must use
79
+ * this rather than `getCliState`, whose managed fast path reports an installed
80
+ * version (any version dir, in readdir order) and would therefore hand back an
81
+ * isolated copy — a copy that is deliberately unreachable from PATH — labelled as
82
+ * the global install.
83
+ */
84
+ export declare function getUnmanagedCliState(agentId: AgentId): Promise<CliState>;
74
85
  /** Resolve CLI state for all registered agents in parallel. */
75
86
  export declare function getAllCliStates(): Promise<Partial<Record<AgentId, CliState>>>;
76
87
  /** Info about an existing unmanaged agent installation. */
@@ -230,6 +241,51 @@ export declare function antigravityOsKeyringProbe(platform?: NodeJS.Platform): {
230
241
  } | null;
231
242
  /** @internal test hook — clear the per-process keyring probe cache. */
232
243
  export declare function __resetAntigravityKeychainCacheForTest(): void;
244
+ /**
245
+ * Whether a Claude version home's credential file is present but carries no
246
+ * token — the "must have a real credential" floor (see
247
+ * `isValidOpenCodeCredential`) applied to claude.
248
+ *
249
+ * A FAILED OAuth refresh leaves exactly this state behind: Claude Code rewrites
250
+ * `.claude/.credentials.json` with `accessToken: ""`, `refreshToken: ""` and
251
+ * `expiresAt: 0`, keeping only the descriptive fields (`subscriptionType`,
252
+ * `rateLimitTier`, `refreshTokenExpiresAt`). Everything we derive from
253
+ * `.claude.json` — email, plan — still looks healthy, so the install reported
254
+ * `signedIn: true`, `agents view` drew usage bars for it, and balanced rotation
255
+ * (whose `authValid` is just "email present") kept picking it — every pick dying
256
+ * at spawn on "OAuth session expired and could not be refreshed".
257
+ *
258
+ * Only decidable off macOS: there the login Keychain is the canonical store and
259
+ * this file is not authoritative, and probing the Keychain would raise an
260
+ * authorization sheet per installed version on every `agents run` — the reason
261
+ * rotation stopped calling `isClaudeAuthValid` at all. Off macOS the file IS the
262
+ * only store, so a token-less file is proof of signed-out. `platform` is a
263
+ * parameter so both branches are testable on any host.
264
+ *
265
+ * Sync, no Keychain, no network — safe on the `agents run` hot path.
266
+ */
267
+ export declare function isClaudeCredentialFileBlank(base: string, platform?: NodeJS.Platform): boolean;
268
+ /**
269
+ * Whether a Claude version home holds its OWN on-disk credential — a non-blank
270
+ * `.claude/.credentials.json` (the store Claude Code writes on login off macOS)
271
+ * or a `.claude/.oauth_token` file (the keychain-less shim fallback, shims.ts).
272
+ *
273
+ * RUSH-1979: the routines daemon injects one ambient `CLAUDE_CODE_OAUTH_TOKEN`
274
+ * (RUSH-1759) so a token-less default account still authenticates. But Claude —
275
+ * and the Linux shim's own `-z CLAUDE_CODE_OAUTH_TOKEN` guard — both prefer that
276
+ * env var over a pinned account's config dir, so when balanced rotation pins a
277
+ * specific account's `CLAUDE_CONFIG_DIR` the ambient token shadows it: the pool
278
+ * is inert and every fire 401s on the one stale token. A routine spawn drops the
279
+ * ambient token when the pinned account has its own credential (this returns
280
+ * true) so the account authenticates itself; when it does not (the RUSH-1759
281
+ * default), the injected token is kept.
282
+ *
283
+ * Off-darwin only, mirroring {@link isClaudeCredentialFileBlank}: on macOS the
284
+ * login Keychain is the canonical store and probing it raises an auth sheet, and
285
+ * there the daemon injects no token to shadow (broker-only, usually absent), so
286
+ * returning false changes nothing. Sync, no Keychain, no network.
287
+ */
288
+ export declare function claudeHomeHasOwnCredential(base: string, platform?: NodeJS.Platform): boolean;
233
289
  export declare function getAccountInfo(agentId: AgentId, home?: string): Promise<AccountInfo>;
234
290
  /**
235
291
  * Determine when the agent was last used by checking session file mtimes,