@phnx-labs/agents-cli 1.20.43 → 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.
Files changed (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -3
  3. package/dist/commands/exec.js +84 -13
  4. package/dist/commands/hosts.js +5 -4
  5. package/dist/commands/logs.d.ts +4 -0
  6. package/dist/commands/logs.js +19 -13
  7. package/dist/commands/routines.d.ts +6 -0
  8. package/dist/commands/routines.js +70 -12
  9. package/dist/commands/secrets.d.ts +18 -0
  10. package/dist/commands/secrets.js +105 -30
  11. package/dist/commands/sessions.d.ts +6 -5
  12. package/dist/commands/sessions.js +50 -22
  13. package/dist/commands/teams.js +104 -8
  14. package/dist/lib/daemon.js +34 -9
  15. package/dist/lib/exec.d.ts +8 -0
  16. package/dist/lib/exec.js +70 -6
  17. package/dist/lib/hosts/dispatch.d.ts +29 -0
  18. package/dist/lib/hosts/dispatch.js +46 -1
  19. package/dist/lib/hosts/logs.d.ts +14 -5
  20. package/dist/lib/hosts/logs.js +39 -13
  21. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  22. package/dist/lib/hosts/remote-cmd.js +27 -0
  23. package/dist/lib/hosts/session-index.d.ts +15 -0
  24. package/dist/lib/hosts/session-index.js +28 -2
  25. package/dist/lib/redact.js +1 -0
  26. package/dist/lib/rotate.d.ts +33 -0
  27. package/dist/lib/rotate.js +37 -0
  28. package/dist/lib/secrets/remote.d.ts +14 -0
  29. package/dist/lib/secrets/remote.js +18 -1
  30. package/dist/lib/session/active.d.ts +10 -0
  31. package/dist/lib/session/active.js +12 -1
  32. package/dist/lib/session/db.d.ts +16 -9
  33. package/dist/lib/session/db.js +66 -44
  34. package/dist/lib/session/discover.d.ts +4 -0
  35. package/dist/lib/session/discover.js +84 -13
  36. package/dist/lib/session/run-names.d.ts +9 -7
  37. package/dist/lib/session/run-names.js +9 -7
  38. package/dist/lib/session/state.d.ts +29 -3
  39. package/dist/lib/session/state.js +84 -5
  40. package/dist/lib/session/types.d.ts +19 -8
  41. package/dist/lib/shims.d.ts +1 -1
  42. package/dist/lib/shims.js +17 -3
  43. package/dist/lib/teams/agents.js +25 -7
  44. package/dist/lib/tmux/session.d.ts +40 -0
  45. package/dist/lib/tmux/session.js +92 -0
  46. package/dist/lib/versions.d.ts +54 -1
  47. package/dist/lib/versions.js +138 -1
  48. package/package.json +1 -1
@@ -22,7 +22,6 @@ export interface SessionRow {
22
22
  git_branch: string | null;
23
23
  topic: string | null;
24
24
  label: string | null;
25
- name: string | null;
26
25
  message_count: number | null;
27
26
  token_count: number | null;
28
27
  cost_usd: number | null;
@@ -130,15 +129,23 @@ export declare function upsertSessionsBatch(entries: Array<{
130
129
  */
131
130
  export declare function syncLabels(labelMap: Map<string, string | null>): number;
132
131
  /**
133
- * Sync `agents run --name` handles for a set of sessions, keyed by session id.
134
- * The name's source of truth lives outside the transcript (host task sidecars,
135
- * run-name sidecars written at launch), so like {@link syncLabels}it is
136
- * re-applied by id every scan rather than parsed per-file. Updates only
137
- * `sessions.name` (names resolve via a direct column tier in ftsSearch, not
138
- * FTS, so there's no session_text column to touch). Only writes when the value
139
- * differs; cheap to call every run. Returns the number of rows updated.
132
+ * Seed session labels from `agents run --name` handles, keyed by session id.
133
+ *
134
+ * `--name` is the universal launch-time way to set a session's label the same
135
+ * field an agent later refines with a generated title (`syncLabels`) or the user
136
+ * with `/rename`. The seed's source of truth lives outside the transcript (host
137
+ * task sidecars, run-name sidecars written at launch), so it is re-applied by id
138
+ * every scan rather than parsed per-file. It only fills a label that is still
139
+ * EMPTY — an agent-generated title always wins over the seed, so a Claude run's
140
+ * `--name` shows until Claude titles it, and a non-Claude run keeps its `--name`
141
+ * as the label. Writes both `sessions.label` and the FTS5 label column so a
142
+ * seeded name is fuzzy-searchable. Cheap to call every run; returns rows updated.
143
+ *
144
+ * Ordering matters: this runs AFTER the per-agent scans (which apply
145
+ * agent-generated titles via {@link syncLabels}), so it never overwrites a real
146
+ * title — it only backfills the gap the seed was meant to cover.
140
147
  */
141
- export declare function syncNames(nameMap: Map<string, string | null>): number;
148
+ export declare function seedLabelsFromNames(nameMap: Map<string, string | null>): number;
142
149
  /**
143
150
  * Sync topics (session titles) for a set of sessions, keyed by id. For agents
144
151
  * whose human-readable title lives in a side index that updates independently
@@ -13,7 +13,7 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
13
13
  const SESSIONS_DIR = getSessionsDir();
14
14
  const DB_PATH = getSessionsDbPath();
15
15
  /** Current schema version; bumped when migrations are added. */
16
- const SCHEMA_VERSION = 9;
16
+ const SCHEMA_VERSION = 10;
17
17
  /**
18
18
  * Canonicalize a file path for use as a scan_ledger key. The same physical
19
19
  * session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
@@ -52,7 +52,6 @@ CREATE TABLE IF NOT EXISTS sessions (
52
52
  git_branch TEXT,
53
53
  topic TEXT,
54
54
  label TEXT,
55
- name TEXT,
56
55
  message_count INTEGER,
57
56
  token_count INTEGER,
58
57
  cost_usd REAL,
@@ -198,6 +197,22 @@ function migrateSchema(db, fromVersion) {
198
197
  if (!cols.some(c => c.name === 'name'))
199
198
  db.exec(`ALTER TABLE sessions ADD COLUMN name TEXT`);
200
199
  }
200
+ if (fromVersion < 10) {
201
+ // v9 → v10: `name` and `label` unify into a single `label`. `--name` now
202
+ // SEEDS the label at launch (refined later by an agent-generated title)
203
+ // instead of living in a separate immutable `name` column. Fold any existing
204
+ // name into label where the label is empty, mirror it into the FTS row, then
205
+ // drop the redundant column. Seeds re-apply from the run-name sidecars every
206
+ // scan (seedLabelsFromNames), so no rescan is required.
207
+ const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
208
+ if (cols.some(c => c.name === 'name')) {
209
+ db.exec(`UPDATE sessions SET label = name
210
+ WHERE (label IS NULL OR label = '') AND name IS NOT NULL AND name != ''`);
211
+ db.exec(`UPDATE session_text SET label = COALESCE(
212
+ (SELECT label FROM sessions WHERE sessions.id = session_text.session_id), '')`);
213
+ db.exec(`ALTER TABLE sessions DROP COLUMN name`);
214
+ }
215
+ }
201
216
  }
202
217
  /** Open (or return the cached) sessions database, applying migrations as needed. */
203
218
  export function getDB() {
@@ -411,13 +426,13 @@ export function recordScans(entries) {
411
426
  const upsertSessionStmt = (db) => db.prepare(`
412
427
  INSERT INTO sessions (
413
428
  id, short_id, agent, version, account, timestamp, last_activity,
414
- project, cwd, git_branch, topic, label, name, message_count, token_count,
429
+ project, cwd, git_branch, topic, label, message_count, token_count,
415
430
  cost_usd, duration_ms,
416
431
  file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
417
432
  pr_url, pr_number, worktree_slug, ticket_id
418
433
  ) VALUES (
419
434
  @id, @short_id, @agent, @version, @account, @timestamp, @last_activity,
420
- @project, @cwd, @git_branch, @topic, @label, @name, @message_count, @token_count,
435
+ @project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
421
436
  @cost_usd, @duration_ms,
422
437
  @file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
423
438
  @pr_url, @pr_number, @worktree_slug, @ticket_id
@@ -481,7 +496,6 @@ export function upsertSession(meta, content, scan) {
481
496
  git_branch: meta.gitBranch ?? null,
482
497
  topic: meta.topic ?? null,
483
498
  label: meta.label ?? null,
484
- name: meta.name ?? null,
485
499
  message_count: meta.messageCount ?? null,
486
500
  token_count: meta.tokenCount ?? null,
487
501
  cost_usd: meta.costUsd ?? null,
@@ -570,7 +584,6 @@ export function upsertSessionsBatch(entries) {
570
584
  git_branch: meta.gitBranch ?? null,
571
585
  topic: meta.topic ?? null,
572
586
  label: meta.label ?? null,
573
- name: meta.name ?? null,
574
587
  message_count: meta.messageCount ?? null,
575
588
  token_count: meta.tokenCount ?? null,
576
589
  cost_usd: meta.costUsd ?? null,
@@ -639,15 +652,23 @@ export function syncLabels(labelMap) {
639
652
  return updates.length;
640
653
  }
641
654
  /**
642
- * Sync `agents run --name` handles for a set of sessions, keyed by session id.
643
- * The name's source of truth lives outside the transcript (host task sidecars,
644
- * run-name sidecars written at launch), so like {@link syncLabels}it is
645
- * re-applied by id every scan rather than parsed per-file. Updates only
646
- * `sessions.name` (names resolve via a direct column tier in ftsSearch, not
647
- * FTS, so there's no session_text column to touch). Only writes when the value
648
- * differs; cheap to call every run. Returns the number of rows updated.
655
+ * Seed session labels from `agents run --name` handles, keyed by session id.
656
+ *
657
+ * `--name` is the universal launch-time way to set a session's label the same
658
+ * field an agent later refines with a generated title (`syncLabels`) or the user
659
+ * with `/rename`. The seed's source of truth lives outside the transcript (host
660
+ * task sidecars, run-name sidecars written at launch), so it is re-applied by id
661
+ * every scan rather than parsed per-file. It only fills a label that is still
662
+ * EMPTY — an agent-generated title always wins over the seed, so a Claude run's
663
+ * `--name` shows until Claude titles it, and a non-Claude run keeps its `--name`
664
+ * as the label. Writes both `sessions.label` and the FTS5 label column so a
665
+ * seeded name is fuzzy-searchable. Cheap to call every run; returns rows updated.
666
+ *
667
+ * Ordering matters: this runs AFTER the per-agent scans (which apply
668
+ * agent-generated titles via {@link syncLabels}), so it never overwrites a real
669
+ * title — it only backfills the gap the seed was meant to cover.
649
670
  */
650
- export function syncNames(nameMap) {
671
+ export function seedLabelsFromNames(nameMap) {
651
672
  if (nameMap.size === 0)
652
673
  return 0;
653
674
  const db = getDB();
@@ -658,21 +679,25 @@ export function syncNames(nameMap) {
658
679
  const chunk = ids.slice(i, i + CHUNK);
659
680
  const placeholders = chunk.map(() => '?').join(',');
660
681
  const rows = db
661
- .prepare(`SELECT id, name FROM sessions WHERE id IN (${placeholders})`)
682
+ .prepare(`SELECT id, label FROM sessions WHERE id IN (${placeholders})`)
662
683
  .all(...chunk);
663
684
  for (const row of rows) {
664
- const live = nameMap.get(row.id) ?? null;
665
- if ((live ?? '') !== (row.name ?? '')) {
666
- updates.push({ id: row.id, name: live });
685
+ const seed = nameMap.get(row.id);
686
+ // Only fill an empty label; a real agent title (non-empty) always wins.
687
+ if (seed && !(row.label ?? '').trim()) {
688
+ updates.push({ id: row.id, label: seed });
667
689
  }
668
690
  }
669
691
  }
670
692
  if (updates.length === 0)
671
693
  return 0;
672
- const upd = db.prepare(`UPDATE sessions SET name = ? WHERE id = ?`);
694
+ const updSessions = db.prepare(`UPDATE sessions SET label = ? WHERE id = ?`);
695
+ const updFts = db.prepare(`UPDATE session_text SET label = ? WHERE session_id = ?`);
673
696
  const txn = db.transaction((items) => {
674
- for (const { id, name } of items)
675
- upd.run(name, id);
697
+ for (const { id, label } of items) {
698
+ updSessions.run(label, id);
699
+ updFts.run(label, id);
700
+ }
676
701
  });
677
702
  txn(updates);
678
703
  return updates.length;
@@ -739,7 +764,6 @@ function rowToMeta(row) {
739
764
  account: row.account ?? undefined,
740
765
  topic: row.topic ?? undefined,
741
766
  label: row.label ?? undefined,
742
- name: row.name ?? undefined,
743
767
  isTeamOrigin: row.is_team_origin === 1,
744
768
  prUrl: row.pr_url ?? undefined,
745
769
  prNumber: row.pr_number ?? undefined,
@@ -1009,33 +1033,31 @@ export function ftsSearch(input, limit = 200) {
1009
1033
  const seen = new Set();
1010
1034
  const hits = [];
1011
1035
  // Tier 1-3: handle-based matches, ordered by exactness. A session's handle is
1012
- // its /rename `label` OR its `agents run --name` handle both are user-chosen
1013
- // aliases and rank identically, so typing either the renamed title or the run
1014
- // name resolves the session ahead of any FTS content hit.
1036
+ // its `label` set by an agent title / `/rename`, or seeded at launch from
1037
+ // `agents run --name`. Typing it resolves the session ahead of any FTS content
1038
+ // hit.
1015
1039
  const labelRows = db.prepare(`
1016
- SELECT id, label, name FROM sessions
1017
- WHERE (label IS NOT NULL AND LOWER(label) LIKE ?)
1018
- OR (name IS NOT NULL AND LOWER(name) LIKE ?)
1019
- `).all(`%${lower}%`, `%${lower}%`);
1040
+ SELECT id, label FROM sessions
1041
+ WHERE label IS NOT NULL AND LOWER(label) LIKE ?
1042
+ `).all(`%${lower}%`);
1020
1043
  let hasExactLabelMatch = false;
1021
1044
  for (const row of labelRows) {
1022
- // Score against whichever handle matches best (exact > prefix > contains).
1045
+ // Score the label by match quality (exact > prefix > contains).
1023
1046
  let score = 0;
1024
- for (const handle of [row.label, row.name]) {
1025
- if (!handle)
1026
- continue;
1047
+ const handle = row.label;
1048
+ if (handle) {
1027
1049
  const h = handle.toLowerCase();
1028
- if (!h.includes(lower))
1029
- continue;
1030
- if (h === lower) {
1031
- score = Math.max(score, 1_000_000);
1032
- hasExactLabelMatch = true;
1033
- }
1034
- else if (h.startsWith(lower)) {
1035
- score = Math.max(score, 900_000);
1036
- }
1037
- else {
1038
- score = Math.max(score, 800_000);
1050
+ if (h.includes(lower)) {
1051
+ if (h === lower) {
1052
+ score = 1_000_000;
1053
+ hasExactLabelMatch = true;
1054
+ }
1055
+ else if (h.startsWith(lower)) {
1056
+ score = 900_000;
1057
+ }
1058
+ else {
1059
+ score = 800_000;
1060
+ }
1039
1061
  }
1040
1062
  }
1041
1063
  if (score === 0)
@@ -64,6 +64,10 @@ interface ClaudeSessionScan {
64
64
  prNumber?: number;
65
65
  worktreeSlug?: string;
66
66
  ticketId?: string;
67
+ /** Tracker refs the session CREATED (Linear create_issue / gh issue create). */
68
+ createdTickets?: string[];
69
+ /** Team name this session SPAWNED via `agents teams create/add` (not team-of-origin). */
70
+ spawnedTeam?: string;
67
71
  }
68
72
  /**
69
73
  * Discover sessions. Scans only files whose (mtime, size) have changed since
@@ -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;