@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.
- package/CHANGELOG.md +18 -0
- package/README.md +4 -3
- package/dist/commands/exec.js +84 -13
- package/dist/commands/hosts.js +5 -4
- package/dist/commands/logs.d.ts +4 -0
- package/dist/commands/logs.js +19 -13
- package/dist/commands/routines.d.ts +6 -0
- package/dist/commands/routines.js +70 -12
- package/dist/commands/secrets.d.ts +18 -0
- package/dist/commands/secrets.js +105 -30
- package/dist/commands/sessions.d.ts +6 -5
- package/dist/commands/sessions.js +50 -22
- package/dist/commands/teams.js +104 -8
- package/dist/lib/daemon.js +34 -9
- package/dist/lib/exec.d.ts +8 -0
- package/dist/lib/exec.js +70 -6
- package/dist/lib/hosts/dispatch.d.ts +29 -0
- package/dist/lib/hosts/dispatch.js +46 -1
- package/dist/lib/hosts/logs.d.ts +14 -5
- package/dist/lib/hosts/logs.js +39 -13
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +27 -0
- package/dist/lib/hosts/session-index.d.ts +15 -0
- package/dist/lib/hosts/session-index.js +28 -2
- package/dist/lib/redact.js +1 -0
- package/dist/lib/rotate.d.ts +33 -0
- package/dist/lib/rotate.js +37 -0
- package/dist/lib/secrets/remote.d.ts +14 -0
- package/dist/lib/secrets/remote.js +18 -1
- package/dist/lib/session/active.d.ts +10 -0
- package/dist/lib/session/active.js +12 -1
- package/dist/lib/session/db.d.ts +16 -9
- package/dist/lib/session/db.js +66 -44
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +84 -13
- package/dist/lib/session/run-names.d.ts +9 -7
- package/dist/lib/session/run-names.js +9 -7
- package/dist/lib/session/state.d.ts +29 -3
- package/dist/lib/session/state.js +84 -5
- package/dist/lib/session/types.d.ts +19 -8
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +17 -3
- package/dist/lib/teams/agents.js +25 -7
- package/dist/lib/tmux/session.d.ts +40 -0
- package/dist/lib/tmux/session.js +92 -0
- package/dist/lib/versions.d.ts +54 -1
- package/dist/lib/versions.js +138 -1
- package/package.json +1 -1
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
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
|
|
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
|
package/dist/lib/session/db.js
CHANGED
|
@@ -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 =
|
|
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,
|
|
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, @
|
|
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
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
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
|
|
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,
|
|
682
|
+
.prepare(`SELECT id, label FROM sessions WHERE id IN (${placeholders})`)
|
|
662
683
|
.all(...chunk);
|
|
663
684
|
for (const row of rows) {
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
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
|
|
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,
|
|
675
|
-
|
|
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
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
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
|
|
1017
|
-
WHERE
|
|
1018
|
-
|
|
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
|
|
1045
|
+
// Score the label by match quality (exact > prefix > contains).
|
|
1023
1046
|
let score = 0;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
continue;
|
|
1047
|
+
const handle = row.label;
|
|
1048
|
+
if (handle) {
|
|
1027
1049
|
const h = handle.toLowerCase();
|
|
1028
|
-
if (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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,
|
|
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
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
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 (
|
|
1991
|
+
if (parsed.type === 'response_item') {
|
|
1939
1992
|
const p = parsed.payload || {};
|
|
1940
|
-
if (
|
|
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 (
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
30
|
-
* apply onto the index. Returns an empty
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
41
|
-
* apply onto the index. Returns an empty
|
|
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
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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;
|