@phnx-labs/agents-cli 1.20.44 → 1.20.46

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 (47) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/dist/commands/exec.js +54 -11
  3. package/dist/commands/secrets.d.ts +18 -0
  4. package/dist/commands/secrets.js +105 -30
  5. package/dist/commands/teams.js +61 -3
  6. package/dist/index.js +14 -119
  7. package/dist/lib/daemon.js +9 -6
  8. package/dist/lib/hosts/dispatch.d.ts +29 -0
  9. package/dist/lib/hosts/dispatch.js +46 -1
  10. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  11. package/dist/lib/hosts/remote-cmd.js +27 -0
  12. package/dist/lib/hosts/session-index.d.ts +15 -0
  13. package/dist/lib/hosts/session-index.js +28 -2
  14. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  15. package/dist/lib/rotate.d.ts +33 -0
  16. package/dist/lib/rotate.js +37 -0
  17. package/dist/lib/secrets/remote.d.ts +14 -0
  18. package/dist/lib/secrets/remote.js +18 -1
  19. package/dist/lib/self-heal/checks/path.d.ts +2 -0
  20. package/dist/lib/self-heal/checks/path.js +30 -0
  21. package/dist/lib/self-heal/checks/resources.d.ts +2 -0
  22. package/dist/lib/self-heal/checks/resources.js +36 -0
  23. package/dist/lib/self-heal/checks/shadowing.d.ts +2 -0
  24. package/dist/lib/self-heal/checks/shadowing.js +48 -0
  25. package/dist/lib/self-heal/checks/shims.d.ts +2 -0
  26. package/dist/lib/self-heal/checks/shims.js +35 -0
  27. package/dist/lib/self-heal/registry.d.ts +22 -0
  28. package/dist/lib/self-heal/registry.js +66 -0
  29. package/dist/lib/self-heal/types.d.ts +41 -0
  30. package/dist/lib/self-heal/types.js +21 -0
  31. package/dist/lib/session/active.d.ts +4 -0
  32. package/dist/lib/session/active.js +2 -0
  33. package/dist/lib/session/db.d.ts +16 -9
  34. package/dist/lib/session/db.js +66 -44
  35. package/dist/lib/session/discover.d.ts +4 -0
  36. package/dist/lib/session/discover.js +84 -13
  37. package/dist/lib/session/run-names.d.ts +9 -7
  38. package/dist/lib/session/run-names.js +9 -7
  39. package/dist/lib/session/state.d.ts +29 -3
  40. package/dist/lib/session/state.js +84 -5
  41. package/dist/lib/session/types.d.ts +19 -8
  42. package/dist/lib/shim-heal.d.ts +23 -0
  43. package/dist/lib/shim-heal.js +109 -0
  44. package/dist/lib/shims.d.ts +6 -0
  45. package/dist/lib/shims.js +1 -1
  46. package/dist/lib/teams/agents.js +9 -0
  47. package/package.json +1 -1
@@ -21,11 +21,11 @@ import { getConfigSymlinkVersion } from '../shims.js';
21
21
  import { SESSION_AGENTS } from './types.js';
22
22
  import { extractSessionTopic } from './prompt.js';
23
23
  import { parseAntigravity } from './parse.js';
24
- import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand } from './state.js';
24
+ import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket } from './state.js';
25
25
  import { costOfUsage } from '../pricing/index.js';
26
26
  import { machineId } from './sync/config.js';
27
27
  import { mapBounded } from '../concurrency.js';
28
- import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, syncNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
28
+ import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
29
29
  import { buildRunNameMap } from './run-names.js';
30
30
  const HOME = os.homedir();
31
31
  // Versions can live under either repo: the user repo (current canonical
@@ -64,9 +64,11 @@ export async function discoverSessions(options) {
64
64
  // reads to behavioral EDR (CrowdStrike Falcon) as a ransomware-style bulk
65
65
  // file-enumeration sweep. Same dirs, same results — just not all at once.
66
66
  await scanAgentsBounded(agents, agent => dispatchAgentScan(agent, onProgress));
67
- // Apply `agents run --name` handles onto the freshly-scanned rows by id —
68
- // the same idempotent, re-applied-every-scan pattern as /rename labels.
69
- syncNames(buildRunNameMap());
67
+ // Seed labels from `agents run --name` handles onto the freshly-scanned
68
+ // rows by id. Runs AFTER the per-agent scans (which applied agent-generated
69
+ // titles via syncLabels), so a real title always wins and the seed only
70
+ // backfills sessions that would otherwise be unnamed.
71
+ seedLabelsFromNames(buildRunNameMap());
70
72
  }
71
73
  finally {
72
74
  releaseScan(process.pid);
@@ -481,6 +483,8 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
481
483
  prNumber: scan.prNumber,
482
484
  worktreeSlug: scan.worktreeSlug,
483
485
  ticketId: scan.ticketId,
486
+ createdTickets: scan.createdTickets,
487
+ spawnedTeam: scan.spawnedTeam,
484
488
  };
485
489
  }
486
490
  else {
@@ -504,6 +508,8 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
504
508
  prNumber: scan.prNumber,
505
509
  worktreeSlug: scan.worktreeSlug,
506
510
  ticketId: scan.ticketId,
511
+ createdTickets: scan.createdTickets,
512
+ spawnedTeam: scan.spawnedTeam,
507
513
  };
508
514
  }
509
515
  return { meta, content: scan.contentText || '' };
@@ -715,6 +721,8 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion) {
715
721
  prNumber: scan.prNumber,
716
722
  worktreeSlug: scan.worktreeSlug,
717
723
  ticketId: scan.ticketId,
724
+ createdTickets: scan.createdTickets,
725
+ spawnedTeam: scan.spawnedTeam,
718
726
  };
719
727
  return { meta, content: scan.contentText || '' };
720
728
  }
@@ -1756,6 +1764,12 @@ export async function scanClaudeSession(filePath) {
1756
1764
  let sawPrCreate = false;
1757
1765
  let prUrl;
1758
1766
  let prNumber;
1767
+ // Artifacts the session PRODUCED: tracker refs it created and any team it spawned.
1768
+ // Ticket creation spans two events — a create_issue tool_use, then the tool_result
1769
+ // carrying the new id — so we hold the pending tool_use ids until their result lands.
1770
+ const createdTickets = new Set();
1771
+ const pendingTicketTools = new Set();
1772
+ let spawnedTeam;
1759
1773
  try {
1760
1774
  for await (const line of rl) {
1761
1775
  if (!line.trim())
@@ -1772,6 +1786,39 @@ export async function scanClaudeSession(filePath) {
1772
1786
  if (!entrypoint && typeof parsed.entrypoint === 'string') {
1773
1787
  entrypoint = parsed.entrypoint;
1774
1788
  }
1789
+ // Produced-artifact signals, structurally (independent of the PR gate below):
1790
+ // - a Bash `agents teams create/add` command → the team it spawned
1791
+ // - a Linear create_issue / `gh issue create` tool_use → its result carries
1792
+ // the new ticket ref, read from the matching tool_result.
1793
+ if (parsed.type === 'assistant' && Array.isArray(parsed.message?.content)) {
1794
+ for (const b of parsed.message.content) {
1795
+ if (b?.type !== 'tool_use')
1796
+ continue;
1797
+ if (!spawnedTeam && typeof b?.input?.command === 'string') {
1798
+ const team = detectSpawnedTeam(b.input.command);
1799
+ if (team)
1800
+ spawnedTeam = team;
1801
+ }
1802
+ if (typeof b?.id === 'string' && isTicketCreateTool(b?.name, b?.input?.command)) {
1803
+ pendingTicketTools.add(b.id);
1804
+ }
1805
+ }
1806
+ }
1807
+ if (pendingTicketTools.size > 0 && parsed.type === 'user' && Array.isArray(parsed.message?.content)) {
1808
+ for (const b of parsed.message.content) {
1809
+ if (b?.type !== 'tool_result' || typeof b?.tool_use_id !== 'string')
1810
+ continue;
1811
+ if (!pendingTicketTools.has(b.tool_use_id))
1812
+ continue;
1813
+ pendingTicketTools.delete(b.tool_use_id);
1814
+ const text = typeof b.content === 'string'
1815
+ ? b.content
1816
+ : Array.isArray(b.content) ? b.content.map((c) => c?.text || '').join('\n') : '';
1817
+ const t = extractCreatedTicket(text);
1818
+ if (t)
1819
+ createdTickets.add(t);
1820
+ }
1821
+ }
1775
1822
  // PR signal, structurally: a Bash tool_use whose command is `gh pr create`
1776
1823
  // marks intent; the pull URL is then read from a tool_result's output.
1777
1824
  if (!prUrl) {
@@ -1900,6 +1947,8 @@ export async function scanClaudeSession(filePath) {
1900
1947
  prNumber,
1901
1948
  worktreeSlug: worktree?.slug,
1902
1949
  ticketId: ticket?.id,
1950
+ createdTickets: createdTickets.size > 0 ? [...createdTickets] : undefined,
1951
+ spawnedTeam,
1903
1952
  };
1904
1953
  }
1905
1954
  /** Stream a Codex JSONL file and extract scan-level metadata (session ID, cwd, topic, tokens). */
@@ -1922,6 +1971,10 @@ async function scanCodexSession(filePath) {
1922
1971
  let sawPrCreate = false;
1923
1972
  let prUrl;
1924
1973
  let prNumber;
1974
+ // Produced artifacts (mirror of the Claude scan): created tracker refs + spawned team.
1975
+ const createdTickets = new Set();
1976
+ const pendingTicketTools = new Set();
1977
+ let spawnedTeam;
1925
1978
  try {
1926
1979
  for await (const line of rl) {
1927
1980
  if (!line.trim())
@@ -1935,23 +1988,39 @@ async function scanCodexSession(filePath) {
1935
1988
  }
1936
1989
  // PR signal, structurally: a Codex `function_call` whose command is
1937
1990
  // `gh pr create`, then the pull URL from a `function_call_output`.
1938
- if (!prUrl && parsed.type === 'response_item') {
1991
+ if (parsed.type === 'response_item') {
1939
1992
  const p = parsed.payload || {};
1940
- if (!sawPrCreate && p.type === 'function_call') {
1993
+ if (p.type === 'function_call') {
1941
1994
  let cmd = '';
1942
1995
  try {
1943
1996
  const args = typeof p.arguments === 'string' ? JSON.parse(p.arguments) : (p.arguments || {});
1944
1997
  cmd = String(args.command || args.cmd || '');
1945
1998
  }
1946
1999
  catch { /* non-JSON args */ }
1947
- if (isPrCreateCommand(cmd))
2000
+ if (!prUrl && !sawPrCreate && isPrCreateCommand(cmd))
1948
2001
  sawPrCreate = true;
2002
+ if (!spawnedTeam) {
2003
+ const team = detectSpawnedTeam(cmd);
2004
+ if (team)
2005
+ spawnedTeam = team;
2006
+ }
2007
+ if (typeof p.call_id === 'string' && isTicketCreateTool(p.name, cmd)) {
2008
+ pendingTicketTools.add(p.call_id);
2009
+ }
1949
2010
  }
1950
- if (sawPrCreate && p.type === 'function_call_output') {
1951
- const pr = extractPrUrl(String(p.output || ''));
1952
- if (pr) {
1953
- prUrl = pr.url;
1954
- prNumber = pr.number;
2011
+ if (p.type === 'function_call_output') {
2012
+ if (!prUrl && sawPrCreate) {
2013
+ const pr = extractPrUrl(String(p.output || ''));
2014
+ if (pr) {
2015
+ prUrl = pr.url;
2016
+ prNumber = pr.number;
2017
+ }
2018
+ }
2019
+ if (typeof p.call_id === 'string' && pendingTicketTools.has(p.call_id)) {
2020
+ pendingTicketTools.delete(p.call_id);
2021
+ const t = extractCreatedTicket(String(p.output || ''));
2022
+ if (t)
2023
+ createdTickets.add(t);
1955
2024
  }
1956
2025
  }
1957
2026
  }
@@ -2044,6 +2113,8 @@ async function scanCodexSession(filePath) {
2044
2113
  prNumber,
2045
2114
  worktreeSlug: worktree?.slug,
2046
2115
  ticketId: ticket?.id,
2116
+ createdTickets: createdTickets.size > 0 ? [...createdTickets] : undefined,
2117
+ spawnedTeam,
2047
2118
  };
2048
2119
  }
2049
2120
  /** Resolve the working directory for an OpenClaw agent from its workspace config. */
@@ -3,11 +3,12 @@
3
3
  * session id of the run it named.
4
4
  *
5
5
  * `agents run` records `<sessionId>.json` here at launch whenever both a name
6
- * and a session id are known up front (Claude pre-mints its id — see
7
- * spawnAgent). The session-discovery pass reads these sidecars and applies the
8
- * names onto the SQLite index by id (via syncNames), the same idempotent,
9
- * re-applied-every-scan pattern as Claude `/rename` labels. Names therefore
10
- * survive transcript rescans without being parsed out of the transcript itself.
6
+ * and a session id are known up front (Claude pre-mints its id — see spawnAgent;
7
+ * a teams teammate's Claude session id is its agent id). The session-discovery
8
+ * pass reads these sidecars and SEEDS the session label by id (via
9
+ * seedLabelsFromNames) the launch handle becomes the label, refined later by an
10
+ * agent-generated title. Idempotent and re-applied every scan, so seeds survive
11
+ * transcript rescans without being parsed out of the transcript itself.
11
12
  *
12
13
  * Mirrors the host-task sidecar convention (`~/.agents/.cache/hosts/<id>.json`),
13
14
  * one small JSON per run under `~/.agents/.cache/run-names/`.
@@ -26,7 +27,8 @@ export declare function runNamesDir(): string;
26
27
  */
27
28
  export declare function recordRunName(rec: Omit<RunNameRecord, 'ts'>): void;
28
29
  /**
29
- * Build the sessionId → name map from every run-name sidecar, for syncNames to
30
- * apply onto the index. Returns an empty map when the dir doesn't exist yet.
30
+ * Build the sessionId → name map from every run-name sidecar, for
31
+ * seedLabelsFromNames to apply onto the index as label seeds. Returns an empty
32
+ * map when the dir doesn't exist yet.
31
33
  */
32
34
  export declare function buildRunNameMap(): Map<string, string | null>;
@@ -3,11 +3,12 @@
3
3
  * session id of the run it named.
4
4
  *
5
5
  * `agents run` records `<sessionId>.json` here at launch whenever both a name
6
- * and a session id are known up front (Claude pre-mints its id — see
7
- * spawnAgent). The session-discovery pass reads these sidecars and applies the
8
- * names onto the SQLite index by id (via syncNames), the same idempotent,
9
- * re-applied-every-scan pattern as Claude `/rename` labels. Names therefore
10
- * survive transcript rescans without being parsed out of the transcript itself.
6
+ * and a session id are known up front (Claude pre-mints its id — see spawnAgent;
7
+ * a teams teammate's Claude session id is its agent id). The session-discovery
8
+ * pass reads these sidecars and SEEDS the session label by id (via
9
+ * seedLabelsFromNames) the launch handle becomes the label, refined later by an
10
+ * agent-generated title. Idempotent and re-applied every scan, so seeds survive
11
+ * transcript rescans without being parsed out of the transcript itself.
11
12
  *
12
13
  * Mirrors the host-task sidecar convention (`~/.agents/.cache/hosts/<id>.json`),
13
14
  * one small JSON per run under `~/.agents/.cache/run-names/`.
@@ -37,8 +38,9 @@ export function recordRunName(rec) {
37
38
  }
38
39
  }
39
40
  /**
40
- * Build the sessionId → name map from every run-name sidecar, for syncNames to
41
- * apply onto the index. Returns an empty map when the dir doesn't exist yet.
41
+ * Build the sessionId → name map from every run-name sidecar, for
42
+ * seedLabelsFromNames to apply onto the index as label seeds. Returns an empty
43
+ * map when the dir doesn't exist yet.
42
44
  */
43
45
  export function buildRunNameMap() {
44
46
  const map = new Map();
@@ -44,6 +44,10 @@ export interface SessionState {
44
44
  pr?: DetectedPr;
45
45
  worktree?: DetectedWorktree;
46
46
  ticket?: DetectedTicket;
47
+ /** Tracker refs this session CREATED (Linear create_issue / gh issue create). */
48
+ createdTickets?: string[];
49
+ /** Team name this session SPAWNED via `agents teams create/add`. */
50
+ spawnedTeam?: string;
47
51
  }
48
52
  export interface StateContext {
49
53
  /** Session file mtime; drives running-vs-stale. */
@@ -63,6 +67,24 @@ export declare function detectTicket(text?: string, branch?: string): DetectedTi
63
67
  export declare function extractPrUrl(output?: string): DetectedPr | undefined;
64
68
  /** True when a Bash/exec command string is a `gh pr create`. */
65
69
  export declare function isPrCreateCommand(command?: string): boolean;
70
+ /**
71
+ * The team a session SPAWNED, from an `agents teams create/add <name>` command.
72
+ * Returns the team name, or undefined if the command isn't a team spawn. Note this
73
+ * is the opposite of `isTeamOrigin` (which marks sessions spawned BY a team).
74
+ */
75
+ export declare function detectSpawnedTeam(command?: string): string | undefined;
76
+ /**
77
+ * True when a tool_use call CREATES a tracker ticket — a Linear MCP `create_issue`
78
+ * tool, or a Bash `gh issue create`. The created id is then read from the matching
79
+ * tool_result via {@link extractCreatedTicket}.
80
+ */
81
+ export declare function isTicketCreateTool(name?: string, command?: string): boolean;
82
+ /**
83
+ * Pull a created ticket ref out of a create-issue tool_result. Linear returns a
84
+ * key like `RUSH-1234`; `gh issue create` returns the issue URL, from which we
85
+ * take `#<number>`. Returns undefined when neither shape is present.
86
+ */
87
+ export declare function extractCreatedTicket(text?: string): string | undefined;
66
88
  /**
67
89
  * Infer live activity + a preview from a chronological event slice. `pr` /
68
90
  * `ticket` / `worktree` are attached by `inferSessionState`; this focuses on the
@@ -70,13 +92,17 @@ export declare function isPrCreateCommand(command?: string): boolean;
70
92
  */
71
93
  export declare function inferActivity(events: SessionEvent[], ctx?: StateContext): SessionState;
72
94
  /**
73
- * Scan an event slice for the durable signals (PR opened, ticket) that aren't
74
- * about the cwd. Correlates each `gh pr create` with the nearest following
75
- * tool_result URL; keeps the last PR found.
95
+ * Scan an event slice for the durable signals that aren't about the cwd: the PR
96
+ * opened, the injected ticket, plus the artifacts the session PRODUCED tracker
97
+ * refs it created and any team it spawned. Each `gh pr create` / create-issue tool
98
+ * call is correlated with the nearest following tool_result; the team name comes
99
+ * straight off the `agents teams create/add` command.
76
100
  */
77
101
  export declare function detectDurableSignals(events: SessionEvent[]): {
78
102
  pr?: DetectedPr;
79
103
  ticket?: DetectedTicket;
104
+ createdTickets?: string[];
105
+ spawnedTeam?: string;
80
106
  };
81
107
  /** Full inference: activity + preview + durable signals + worktree/ticket from ctx. */
82
108
  export declare function inferSessionState(events: SessionEvent[], ctx?: StateContext): SessionState;
@@ -37,6 +37,16 @@ const PR_URL_RE = /https:\/\/github\.com\/[^\s"'()<>]+\/pull\/(\d+)/;
37
37
  const WORKTREE_RE = /\/\.agents\/worktrees\/([^/]+)/;
38
38
  /** gh invocations that create/open a PR. */
39
39
  const GH_PR_CREATE_RE = /\bgh\s+pr\s+(?:create|new)\b/;
40
+ /** gh invocation that opens an issue — the created number is read from its result. */
41
+ const GH_ISSUE_CREATE_RE = /\bgh\s+issue\s+create\b/;
42
+ /** A created GitHub issue URL (…/issues/123) in tool-result output. */
43
+ const GH_ISSUE_URL_RE = /https:\/\/github\.com\/[^\s"'()<>]+\/issues\/(\d+)/;
44
+ /**
45
+ * `agents teams create <name>` / `agents teams add <team> …` (also the `ag` alias).
46
+ * The team NAME is the first bareword after the sub-verb, skipping any flags. This
47
+ * is the structural signal that a session SPAWNED a team (vs. was spawned by one).
48
+ */
49
+ const TEAMS_SPAWN_RE = /\bag(?:ents)?\s+teams?\s+(?:create|add)\s+(?:--?[a-z][\w-]*(?:[= ]\S+)?\s+)*([A-Za-z0-9][\w-]*)/;
40
50
  /** Collapse to a single trimmed line for a one-row preview cell. */
41
51
  function oneLine(s) {
42
52
  return s.replace(/\s+/g, ' ').trim();
@@ -80,6 +90,46 @@ export function extractPrUrl(output) {
80
90
  export function isPrCreateCommand(command) {
81
91
  return !!command && GH_PR_CREATE_RE.test(command);
82
92
  }
93
+ /**
94
+ * The team a session SPAWNED, from an `agents teams create/add <name>` command.
95
+ * Returns the team name, or undefined if the command isn't a team spawn. Note this
96
+ * is the opposite of `isTeamOrigin` (which marks sessions spawned BY a team).
97
+ */
98
+ export function detectSpawnedTeam(command) {
99
+ if (!command)
100
+ return undefined;
101
+ const m = command.match(TEAMS_SPAWN_RE);
102
+ return m ? m[1] : undefined;
103
+ }
104
+ /**
105
+ * True when a tool_use call CREATES a tracker ticket — a Linear MCP `create_issue`
106
+ * tool, or a Bash `gh issue create`. The created id is then read from the matching
107
+ * tool_result via {@link extractCreatedTicket}.
108
+ */
109
+ export function isTicketCreateTool(name, command) {
110
+ if (typeof name === 'string' && /linear/i.test(name) && /create[_-]?issue/i.test(name))
111
+ return true;
112
+ // Any shell tool (Bash / shell / local_shell) running `gh issue create`.
113
+ if (!!command && GH_ISSUE_CREATE_RE.test(command))
114
+ return true;
115
+ return false;
116
+ }
117
+ /**
118
+ * Pull a created ticket ref out of a create-issue tool_result. Linear returns a
119
+ * key like `RUSH-1234`; `gh issue create` returns the issue URL, from which we
120
+ * take `#<number>`. Returns undefined when neither shape is present.
121
+ */
122
+ export function extractCreatedTicket(text) {
123
+ if (!text)
124
+ return undefined;
125
+ const lin = text.match(TICKET_RE);
126
+ if (lin && !TICKET_DENYLIST.has(lin[1].split('-')[0]))
127
+ return lin[1];
128
+ const gh = text.match(GH_ISSUE_URL_RE);
129
+ if (gh)
130
+ return `#${gh[1]}`;
131
+ return undefined;
132
+ }
83
133
  /** Does an assistant message read as a question directed at the user? */
84
134
  function looksLikeQuestion(text) {
85
135
  const t = text.trim();
@@ -181,14 +231,19 @@ export function inferActivity(events, ctx = {}) {
181
231
  return base;
182
232
  }
183
233
  /**
184
- * Scan an event slice for the durable signals (PR opened, ticket) that aren't
185
- * about the cwd. Correlates each `gh pr create` with the nearest following
186
- * tool_result URL; keeps the last PR found.
234
+ * Scan an event slice for the durable signals that aren't about the cwd: the PR
235
+ * opened, the injected ticket, plus the artifacts the session PRODUCED tracker
236
+ * refs it created and any team it spawned. Each `gh pr create` / create-issue tool
237
+ * call is correlated with the nearest following tool_result; the team name comes
238
+ * straight off the `agents teams create/add` command.
187
239
  */
188
240
  export function detectDurableSignals(events) {
189
241
  let pr;
190
242
  let sawPrCreate = false;
191
243
  let ticket;
244
+ let sawTicketCreate = false;
245
+ let spawnedTeam;
246
+ const createdTickets = new Set();
192
247
  for (const e of events) {
193
248
  // Structural PR signal: a real `gh pr create` tool call, then the pull URL
194
249
  // from a following tool_result — never a bare URL mentioned in prose.
@@ -201,21 +256,45 @@ export function detectDurableSignals(events) {
201
256
  sawPrCreate = false;
202
257
  }
203
258
  }
259
+ // Produced artifacts: a team spawn is read off the command; a created ticket
260
+ // is a create-issue tool call whose following tool_result carries the new ref.
261
+ if (e.type === 'tool_use') {
262
+ if (!spawnedTeam) {
263
+ const team = detectSpawnedTeam(e.command);
264
+ if (team)
265
+ spawnedTeam = team;
266
+ }
267
+ if (isTicketCreateTool(e.tool, e.command))
268
+ sawTicketCreate = true;
269
+ }
270
+ if (sawTicketCreate && e.type === 'tool_result') {
271
+ const t = extractCreatedTicket(e.output);
272
+ if (t)
273
+ createdTickets.add(t);
274
+ sawTicketCreate = false;
275
+ }
204
276
  if (!ticket && e.type === 'message' && e.role === 'user') {
205
277
  ticket = detectTicket(e.content);
206
278
  }
207
279
  }
208
- return { pr, ticket };
280
+ return {
281
+ pr,
282
+ ticket,
283
+ createdTickets: createdTickets.size > 0 ? [...createdTickets] : undefined,
284
+ spawnedTeam,
285
+ };
209
286
  }
210
287
  /** Full inference: activity + preview + durable signals + worktree/ticket from ctx. */
211
288
  export function inferSessionState(events, ctx = {}) {
212
289
  const state = inferActivity(events, ctx);
213
- const { pr, ticket } = detectDurableSignals(events);
290
+ const { pr, ticket, createdTickets, spawnedTeam } = detectDurableSignals(events);
214
291
  const worktree = detectWorktree(ctx.cwd, ctx.gitBranch);
215
292
  return {
216
293
  ...state,
217
294
  pr: pr ?? state.pr,
218
295
  worktree: worktree ?? state.worktree,
219
296
  ticket: ticket ?? detectTicket(undefined, ctx.gitBranch) ?? state.ticket,
297
+ createdTickets,
298
+ spawnedTeam,
220
299
  };
221
300
  }
@@ -65,16 +65,16 @@ export interface SessionMeta {
65
65
  version?: string;
66
66
  account?: string;
67
67
  topic?: string;
68
- /** Custom name the user gave the session (e.g. Claude Code /rename). */
69
- label?: string;
70
68
  /**
71
- * Durable launch handle from `agents run --name <slug>` an alias chosen at
72
- * launch (not derived from the session id), used to resolve the run in
73
- * `agents sessions <name>`. Distinct from `label` (post-hoc /rename): a run's
74
- * name is immutable; both are searchable. Absent for runs launched without
75
- * `--name`.
69
+ * The session's human-readable name one field, several sources with a plain
70
+ * priority: an agent-generated title / Claude `/rename` wins; else the launch
71
+ * handle seeded from `agents run --name <slug>` (interactive, headless, remote
72
+ * host, or a teams teammate); else it stays unset and the listing falls back to
73
+ * `topic`. Searchable via `agents sessions <label>`. (Before v10 the launch
74
+ * handle lived in a separate immutable `name` column; `--name` now just seeds
75
+ * this label.)
76
76
  */
77
- name?: string;
77
+ label?: string;
78
78
  /** Set when this session was spawned by `agents teams`. */
79
79
  teamOrigin?: TeamOrigin;
80
80
  /** Durable state signals extracted at scan time by the session-state engine. */
@@ -86,6 +86,17 @@ export interface SessionMeta {
86
86
  worktreeSlug?: string;
87
87
  /** Tracker ticket ref (e.g. RUSH-1234) from the prompt or branch. */
88
88
  ticketId?: string;
89
+ /**
90
+ * Tracker refs the session CREATED during its run — Linear `create_issue` MCP
91
+ * calls or `gh issue create` shell commands — read from the tool result. Distinct
92
+ * from `ticketId` (the injected/worked-on ticket from the prompt or branch).
93
+ */
94
+ createdTickets?: string[];
95
+ /**
96
+ * Team name this session SPAWNED via `agents teams create/add`. The inverse of
97
+ * `isTeamOrigin` (which marks sessions spawned BY a team).
98
+ */
99
+ spawnedTeam?: string;
89
100
  /**
90
101
  * True when the session was spawned programmatically (SDK entrypoint) rather
91
102
  * than by a human at the Claude CLI. Captured at scan time from the JSONL
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Heal the shim/shadow/PATH conditions silently, then return the notice lines to
3
+ * print ONCE for whatever is left (a real-binary shadow we won't move; a PATH entry
4
+ * that was just added / needs a reload), or null when there's nothing new to say.
5
+ */
6
+ export declare function healShimsInteractive(): Promise<string[] | null>;
7
+ type PathNoticeState = 'ok' | 'added' | 'reload';
8
+ /**
9
+ * A stable signature of the conditions worth surfacing: the sorted set of
10
+ * real-binary shadows plus the PATH notice state. Empty string = nothing to say.
11
+ */
12
+ export declare function computeShimNoticeSignature(input: {
13
+ shadowNotes: string[];
14
+ pathState: PathNoticeState;
15
+ }): string;
16
+ /**
17
+ * Whether to surface the notice for the current condition. Returns false (stay
18
+ * quiet) when the exact same signature was already surfaced, or when there's
19
+ * nothing to say. On true it records the signature so the next shell with the same
20
+ * state is suppressed. An empty signature clears the marker.
21
+ */
22
+ export declare function shouldSurfaceShimNotice(signature: string): boolean;
23
+ export {};
@@ -0,0 +1,109 @@
1
+ // Interactive shim-heal for the CLI startup path + the persistent notice-state that
2
+ // replaces the old per-PPID sentinel.
3
+ //
4
+ // The actual repair (regenerating shims, adopting symlink launchers, adding the
5
+ // shims dir to PATH) lives in the unified self-heal registry — this module just
6
+ // drives the shim-relevant checks SILENTLY on a normal `agents` invocation and then
7
+ // decides whether to print a one-time notice about anything left. The old flow
8
+ // re-ran its whole detect-and-nag on every new terminal (its sentinel was keyed to
9
+ // process.ppid); the persistent signature here means an unresolved condition is
10
+ // surfaced once, not on every shell.
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { getRuntimeStateDir } from './state.js';
14
+ /**
15
+ * Heal the shim/shadow/PATH conditions silently, then return the notice lines to
16
+ * print ONCE for whatever is left (a real-binary shadow we won't move; a PATH entry
17
+ * that was just added / needs a reload), or null when there's nothing new to say.
18
+ */
19
+ export async function healShimsInteractive() {
20
+ const { runSelfHeal } = await import('./self-heal/registry.js');
21
+ const report = await runSelfHeal({ checks: ['shims', 'shadowing', 'path'], mode: 'safe' });
22
+ const shadowNotes = [];
23
+ let pathAdded = null;
24
+ let pathReload = null;
25
+ for (const c of report.checks) {
26
+ if (!c.result)
27
+ continue;
28
+ if (c.id === 'shadowing')
29
+ shadowNotes.push(...c.result.needsAttention);
30
+ if (c.id === 'path') {
31
+ for (const f of c.result.fixed)
32
+ pathAdded = f; // "added shims to PATH (~/.zshrc)"
33
+ for (const a of c.result.needsAttention)
34
+ pathReload = a; // "...not loaded — open a new terminal"
35
+ }
36
+ }
37
+ const pathState = pathAdded ? 'added' : pathReload ? 'reload' : 'ok';
38
+ const signature = computeShimNoticeSignature({ shadowNotes, pathState });
39
+ if (!shouldSurfaceShimNotice(signature))
40
+ return null;
41
+ const lines = [];
42
+ if (pathAdded) {
43
+ lines.push(pathAdded);
44
+ lines.push('Open a new terminal (or source your shell rc) to pick it up.');
45
+ }
46
+ else if (pathReload) {
47
+ lines.push(pathReload);
48
+ }
49
+ if (shadowNotes.length > 0) {
50
+ lines.push('These agent commands run a native binary instead of the version-managed shim:');
51
+ for (const note of shadowNotes)
52
+ lines.push(` ${note}`);
53
+ lines.push("It's a real binary (not a symlink), so agents-cli won't move it — reorder PATH or remove it to hand it over.");
54
+ }
55
+ return lines.length > 0 ? lines : null;
56
+ }
57
+ function noticeStatePath() {
58
+ return path.join(getRuntimeStateDir(), 'shim-notice.json');
59
+ }
60
+ /**
61
+ * A stable signature of the conditions worth surfacing: the sorted set of
62
+ * real-binary shadows plus the PATH notice state. Empty string = nothing to say.
63
+ */
64
+ export function computeShimNoticeSignature(input) {
65
+ const shadows = [...input.shadowNotes].sort().join(',');
66
+ const parts = [];
67
+ if (shadows)
68
+ parts.push(`shadow:${shadows}`);
69
+ if (input.pathState !== 'ok')
70
+ parts.push(`path:${input.pathState}`);
71
+ return parts.join('|');
72
+ }
73
+ function readLastNoticeSignature() {
74
+ try {
75
+ const parsed = JSON.parse(fs.readFileSync(noticeStatePath(), 'utf-8'));
76
+ return typeof parsed.signature === 'string' ? parsed.signature : null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function writeLastNoticeSignature(signature) {
83
+ try {
84
+ fs.mkdirSync(getRuntimeStateDir(), { recursive: true });
85
+ fs.writeFileSync(noticeStatePath(), JSON.stringify({ signature }));
86
+ }
87
+ catch {
88
+ /* best-effort: never block a command on the marker */
89
+ }
90
+ }
91
+ /**
92
+ * Whether to surface the notice for the current condition. Returns false (stay
93
+ * quiet) when the exact same signature was already surfaced, or when there's
94
+ * nothing to say. On true it records the signature so the next shell with the same
95
+ * state is suppressed. An empty signature clears the marker.
96
+ */
97
+ export function shouldSurfaceShimNotice(signature) {
98
+ if (!signature) {
99
+ try {
100
+ fs.rmSync(noticeStatePath(), { force: true });
101
+ }
102
+ catch { /* best-effort */ }
103
+ return false;
104
+ }
105
+ if (readLastNoticeSignature() === signature)
106
+ return false;
107
+ writeLastNoticeSignature(signature);
108
+ return true;
109
+ }
@@ -278,6 +278,12 @@ export declare function getConfigSymlinkVersion(agent: AgentId): string | null;
278
278
  */
279
279
  export declare function onDiskShimFile(cliCommand: string, platform: NodeJS.Platform): string;
280
280
  export declare function shimExists(agent: AgentId): boolean;
281
+ /**
282
+ * True if the on-disk shim's schema version matches `SHIM_SCHEMA_VERSION`.
283
+ * False means either the shim is missing, is pre-v2 (no marker), or is an
284
+ * older version that needs regeneration.
285
+ */
286
+ export declare function isShimCurrent(agent: AgentId): boolean;
281
287
  /**
282
288
  * Regenerate the shim if it's missing or outdated. Returns a status describing
283
289
  * what happened — callers can surface a one-line notice to the user ("Updated
package/dist/lib/shims.js CHANGED
@@ -1606,7 +1606,7 @@ function readShimSchemaVersion(agent) {
1606
1606
  * False means either the shim is missing, is pre-v2 (no marker), or is an
1607
1607
  * older version that needs regeneration.
1608
1608
  */
1609
- function isShimCurrent(agent) {
1609
+ export function isShimCurrent(agent) {
1610
1610
  const version = readShimSchemaVersion(agent);
1611
1611
  return version === SHIM_SCHEMA_VERSION;
1612
1612
  }