@phnx-labs/agents-cli 1.20.44 → 1.20.45

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.
@@ -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
@@ -22,6 +22,7 @@ import { getAgentsDir as getSystemAgentsDir, getShimsDir } from '../state.js';
22
22
  import { AGENTS, getAccountInfo } from '../agents.js';
23
23
  import { resolveVersion, isVersionInstalled } from '../versions.js';
24
24
  import { sanitizeProcessEnv } from '../secrets/bundles.js';
25
+ import { recordRunName } from '../session/run-names.js';
25
26
  let lastMemoryWarnAt = 0;
26
27
  // On macOS, os.freemem() returns only the truly-free pool and ignores the
27
28
  // large inactive+purgeable cache the kernel will reclaim under pressure, so
@@ -1067,6 +1068,14 @@ export class AgentManager {
1067
1068
  throw new Error(`Failed to create agent directory: ${err.message}`);
1068
1069
  }
1069
1070
  this.agents.set(agentId, agent);
1071
+ // Seed the teammate's session label with its friendly team name, so the run
1072
+ // shows up as `<name>` in `agents sessions` and resolves by it — consistent
1073
+ // with `agents run --name`. For Claude the agent id IS the session id (passed
1074
+ // via --session-id in buildCommand); other agents don't expose a launch-time
1075
+ // id, so they're seeded once discovery captures one. Best-effort.
1076
+ if (agentType === 'claude' && name && !isCloudBacked) {
1077
+ recordRunName({ sessionId: agentId, name, agent: agentType, cwd: resolvedCwd ?? undefined });
1078
+ }
1070
1079
  if (isStaged) {
1071
1080
  await agent.saveMeta();
1072
1081
  debug(`Staged ${agentType} teammate '${name}' in team '${taskName}' (after: ${cleanAfter.join(', ')})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.44",
3
+ "version": "1.20.45",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",