@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.
- package/CHANGELOG.md +12 -1
- package/dist/commands/exec.js +54 -11
- package/dist/commands/secrets.d.ts +18 -0
- package/dist/commands/secrets.js +105 -30
- package/dist/commands/teams.js +61 -3
- package/dist/index.js +14 -119
- package/dist/lib/daemon.js +9 -6
- package/dist/lib/hosts/dispatch.d.ts +29 -0
- package/dist/lib/hosts/dispatch.js +46 -1
- 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/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -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/self-heal/checks/path.d.ts +2 -0
- package/dist/lib/self-heal/checks/path.js +30 -0
- package/dist/lib/self-heal/checks/resources.d.ts +2 -0
- package/dist/lib/self-heal/checks/resources.js +36 -0
- package/dist/lib/self-heal/checks/shadowing.d.ts +2 -0
- package/dist/lib/self-heal/checks/shadowing.js +48 -0
- package/dist/lib/self-heal/checks/shims.d.ts +2 -0
- package/dist/lib/self-heal/checks/shims.js +35 -0
- package/dist/lib/self-heal/registry.d.ts +22 -0
- package/dist/lib/self-heal/registry.js +66 -0
- package/dist/lib/self-heal/types.d.ts +41 -0
- package/dist/lib/self-heal/types.js +21 -0
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/active.js +2 -0
- 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/shim-heal.d.ts +23 -0
- package/dist/lib/shim-heal.js +109 -0
- package/dist/lib/shims.d.ts +6 -0
- package/dist/lib/shims.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- 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,
|
|
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;
|
|
@@ -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
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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 {
|
|
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
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* `agents
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -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
|
}
|