@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import * as fs from 'fs';
|
|
|
9
9
|
import { truncate } from '../format.js';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
|
+
import { isSyntheticUserMessage } from './prompt.js';
|
|
12
13
|
/**
|
|
13
14
|
* Largest session file we will load into memory. Above this we throw a clean
|
|
14
15
|
* error instead of OOMing or hitting V8's ERR_STRING_TOO_LONG. Aligns with
|
|
@@ -156,9 +157,17 @@ export function parseSession(filePath, agent) {
|
|
|
156
157
|
}
|
|
157
158
|
// Chokepoint: every string field that originated in an untrusted session
|
|
158
159
|
// file gets stripped of terminal escapes here, so renderers downstream can
|
|
159
|
-
// safely splat values into chalk/console output.
|
|
160
|
-
|
|
160
|
+
// safely splat values into chalk/console output. Same pass flags
|
|
161
|
+
// harness-injected `role=user` scaffolding (Claude `<bash-input>`/`<bash-stdout>`
|
|
162
|
+
// from `!`-prefix runs, `<system-reminder>`, etc.) as `_synthetic` so turn
|
|
163
|
+
// slicing and `--include user` count only genuine user intent — one place,
|
|
164
|
+
// every harness, instead of per-consumer regex.
|
|
165
|
+
for (const e of events) {
|
|
161
166
|
sanitizeEvent(e);
|
|
167
|
+
if (e.type === 'message' && e.role === 'user' && isSyntheticUserMessage(e.content)) {
|
|
168
|
+
e._synthetic = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
162
171
|
return events;
|
|
163
172
|
}
|
|
164
173
|
/** Infer the agent type from a session file path using known directory conventions. */
|
|
@@ -1014,44 +1023,144 @@ export function parseAntigravity(dbPath) {
|
|
|
1014
1023
|
* Messages have role (user/assistant) and metadata.
|
|
1015
1024
|
* Parts contain the actual content: text, tool, reasoning, patch, step-start/finish.
|
|
1016
1025
|
*/
|
|
1026
|
+
/**
|
|
1027
|
+
* Parse a Grok (xAI CLI) session into normalized events.
|
|
1028
|
+
*
|
|
1029
|
+
* A Grok session dir holds several files; the conversation transcript is
|
|
1030
|
+
* `chat_history.jsonl`, one JSON object per line with a `type`:
|
|
1031
|
+
* - `system` — the system prompt (skipped; not conversational)
|
|
1032
|
+
* - `user` — `content` is an array of `{ type: 'text', text }` blocks
|
|
1033
|
+
* - `assistant` — `content` is a string, plus a `tool_calls[]` array of
|
|
1034
|
+
* `{ id, name, arguments }` (arguments is a JSON string)
|
|
1035
|
+
* - `reasoning` — chain-of-thought; text (when present) lives in `summary`
|
|
1036
|
+
* - `tool_result` — `{ tool_call_id, content }`, correlated back to the call
|
|
1037
|
+
*
|
|
1038
|
+
* The scanner records `summary.json` as the session's filePath (see
|
|
1039
|
+
* `readGrokMeta` in discover.ts), so resolve `chat_history.jsonl` from the same
|
|
1040
|
+
* dir; also accept being handed the transcript file directly. Per-line
|
|
1041
|
+
* timestamps aren't stored, so every event carries the session's `created_at`
|
|
1042
|
+
* (from summary.json), falling back to the transcript's mtime.
|
|
1043
|
+
*/
|
|
1017
1044
|
export function parseGrok(filePath) {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1045
|
+
const sessionDir = path.dirname(filePath);
|
|
1046
|
+
const historyPath = filePath.endsWith('chat_history.jsonl')
|
|
1047
|
+
? filePath
|
|
1048
|
+
: path.join(sessionDir, 'chat_history.jsonl');
|
|
1049
|
+
if (!fs.existsSync(historyPath))
|
|
1050
|
+
return [];
|
|
1051
|
+
let timestamp;
|
|
1021
1052
|
try {
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
content: summary.session_summary || 'Grok session',
|
|
1030
|
-
agent: 'grok',
|
|
1031
|
-
metadata: { sessionId: summary.id, cwd: summary.cwd },
|
|
1032
|
-
}];
|
|
1053
|
+
const summaryPath = filePath.endsWith('summary.json')
|
|
1054
|
+
? filePath
|
|
1055
|
+
: path.join(sessionDir, 'summary.json');
|
|
1056
|
+
if (fs.existsSync(summaryPath)) {
|
|
1057
|
+
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf-8'));
|
|
1058
|
+
if (typeof summary?.created_at === 'string')
|
|
1059
|
+
timestamp = summary.created_at;
|
|
1033
1060
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1061
|
+
}
|
|
1062
|
+
catch { /* fall back to mtime below */ }
|
|
1063
|
+
if (!timestamp) {
|
|
1064
|
+
let mtime = null;
|
|
1065
|
+
try {
|
|
1066
|
+
mtime = fs.statSync(historyPath).mtime;
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
mtime = null;
|
|
1070
|
+
}
|
|
1071
|
+
timestamp = (mtime ?? new Date()).toISOString();
|
|
1072
|
+
}
|
|
1073
|
+
const content = safeReadSessionFile(historyPath);
|
|
1074
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
1075
|
+
const events = [];
|
|
1076
|
+
const toolCallMap = new Map();
|
|
1077
|
+
// Grok text is either a plain string (assistant) or an array of typed blocks
|
|
1078
|
+
// (user: `{ type: 'text', text }`; reasoning summary: `{ text | summary_text }`).
|
|
1079
|
+
const extractText = (raw) => {
|
|
1080
|
+
if (typeof raw === 'string')
|
|
1081
|
+
return raw.trim();
|
|
1082
|
+
if (Array.isArray(raw)) {
|
|
1083
|
+
return raw
|
|
1084
|
+
.map((part) => typeof part?.text === 'string'
|
|
1085
|
+
? part.text
|
|
1086
|
+
: typeof part?.summary_text === 'string'
|
|
1087
|
+
? part.summary_text
|
|
1088
|
+
: '')
|
|
1089
|
+
.join('')
|
|
1090
|
+
.trim();
|
|
1091
|
+
}
|
|
1092
|
+
return '';
|
|
1093
|
+
};
|
|
1094
|
+
for (const line of lines) {
|
|
1095
|
+
let msg;
|
|
1096
|
+
try {
|
|
1097
|
+
msg = JSON.parse(line);
|
|
1098
|
+
}
|
|
1099
|
+
catch {
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const type = msg?.type;
|
|
1103
|
+
if (type === 'user') {
|
|
1104
|
+
const text = extractText(msg.content);
|
|
1105
|
+
if (text)
|
|
1106
|
+
events.push({ type: 'message', agent: 'grok', timestamp, role: 'user', content: text });
|
|
1107
|
+
}
|
|
1108
|
+
else if (type === 'assistant') {
|
|
1109
|
+
const text = extractText(msg.content);
|
|
1110
|
+
if (text)
|
|
1111
|
+
events.push({ type: 'message', agent: 'grok', timestamp, role: 'assistant', content: text });
|
|
1112
|
+
const calls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
1113
|
+
for (const call of calls) {
|
|
1114
|
+
const toolName = typeof call?.name === 'string' ? call.name : 'unknown';
|
|
1115
|
+
let args = {};
|
|
1116
|
+
if (call?.arguments && typeof call.arguments === 'object') {
|
|
1117
|
+
args = call.arguments;
|
|
1046
1118
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1119
|
+
else if (typeof call?.arguments === 'string') {
|
|
1120
|
+
try {
|
|
1121
|
+
args = JSON.parse(call.arguments);
|
|
1122
|
+
}
|
|
1123
|
+
catch {
|
|
1124
|
+
args = { _raw: call.arguments };
|
|
1125
|
+
}
|
|
1049
1126
|
}
|
|
1127
|
+
if (typeof call?.id === 'string')
|
|
1128
|
+
toolCallMap.set(call.id, toolName);
|
|
1129
|
+
events.push({
|
|
1130
|
+
type: 'tool_use',
|
|
1131
|
+
agent: 'grok',
|
|
1132
|
+
timestamp,
|
|
1133
|
+
tool: toolName,
|
|
1134
|
+
args,
|
|
1135
|
+
path: args.path || args.file_path || undefined,
|
|
1136
|
+
command: typeof args.command === 'string' ? args.command : undefined,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
else if (type === 'reasoning') {
|
|
1141
|
+
const text = extractText(msg.summary);
|
|
1142
|
+
if (text)
|
|
1143
|
+
events.push({ type: 'thinking', agent: 'grok', timestamp, content: text });
|
|
1144
|
+
}
|
|
1145
|
+
else if (type === 'tool_result') {
|
|
1146
|
+
const callId = typeof msg.tool_call_id === 'string' ? msg.tool_call_id : undefined;
|
|
1147
|
+
const toolName = (callId && toolCallMap.get(callId)) || 'unknown';
|
|
1148
|
+
const output = typeof msg.content === 'string' ? msg.content : extractText(msg.content);
|
|
1149
|
+
const isError = typeof output === 'string' && output.startsWith('Error:');
|
|
1150
|
+
events.push({
|
|
1151
|
+
type: isError ? 'error' : 'tool_result',
|
|
1152
|
+
agent: 'grok',
|
|
1153
|
+
timestamp,
|
|
1154
|
+
tool: toolName,
|
|
1155
|
+
success: !isError,
|
|
1156
|
+
output: output.length > 500 ? output.slice(0, 497) + '...' : output,
|
|
1050
1157
|
});
|
|
1158
|
+
if (callId)
|
|
1159
|
+
toolCallMap.delete(callId);
|
|
1051
1160
|
}
|
|
1161
|
+
// `system` lines are the system prompt — intentionally skipped.
|
|
1052
1162
|
}
|
|
1053
|
-
|
|
1054
|
-
return [];
|
|
1163
|
+
return events;
|
|
1055
1164
|
}
|
|
1056
1165
|
export function parseOpenCode(filePath) {
|
|
1057
1166
|
const [dbPath, sessionId] = filePath.split('#');
|
|
@@ -4,6 +4,23 @@ export interface PidSessionEntry {
|
|
|
4
4
|
/** The launch session id. Present for agents launched with a known id (Claude). */
|
|
5
5
|
sessionId?: string;
|
|
6
6
|
cwd?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The launch id minted by `ag run` and exported to the child as
|
|
9
|
+
* `AGENT_LAUNCH_ID`. The agent's SessionStart hook records the SAME id in its
|
|
10
|
+
* own state file (`terminals/sessions/<pid>.json`, `launch_id`), so the two
|
|
11
|
+
* records reconcile by this key even when the hook runs under a DIFFERENT pid
|
|
12
|
+
* (tmux pane leaf, cmd.exe wrapper) — see resolveHookSessionId in
|
|
13
|
+
* session/hook-sessions.ts. This
|
|
14
|
+
* is how a non-Claude launch (whose id we don't know at spawn) gets an exact
|
|
15
|
+
* session id at listing time instead of the newest-jsonl heuristic.
|
|
16
|
+
*/
|
|
17
|
+
launchId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* `AGENT_TERMINAL_ID` when the launch inherited one (a Factory VS Code tab).
|
|
20
|
+
* A secondary join key to the hook's `terminal_id`, for agents the extension
|
|
21
|
+
* spawned. Best-effort — undefined outside the extension.
|
|
22
|
+
*/
|
|
23
|
+
terminalId?: string;
|
|
7
24
|
/**
|
|
8
25
|
* `$TMUX_PANE` at launch — stored for diagnostics and possible future
|
|
9
26
|
* disambiguation. NOT currently consulted on read: the listing path keys
|
|
@@ -18,5 +35,20 @@ export declare function extractSessionIdArg(args: string[]): string | undefined;
|
|
|
18
35
|
export declare function writePidSessionEntry(entry: PidSessionEntry): void;
|
|
19
36
|
/** Look up a live pid's recorded session. Returns undefined if absent/corrupt. */
|
|
20
37
|
export declare function readPidSessionEntry(pid: number): PidSessionEntry | undefined;
|
|
21
|
-
/**
|
|
22
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Every recorded launch, one entry per live-or-dead pid. Used to index launches
|
|
40
|
+
* by their `tmuxPane` so the authoritative tmux source can attribute a pane it did
|
|
41
|
+
* NOT wrap (an agent bare-spawned into an existing pane) to its exact launch —
|
|
42
|
+
* the caller filters to live pids. Best-effort: unreadable/corrupt files are
|
|
43
|
+
* skipped, a missing dir yields `[]`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function listPidSessionEntries(): PidSessionEntry[];
|
|
46
|
+
/**
|
|
47
|
+
* Remove entries whose pid is no longer alive. Best-effort housekeeping.
|
|
48
|
+
*
|
|
49
|
+
* `isAlive` receives the entry's recorded `startedAtMs` so it can reject a pid
|
|
50
|
+
* that a newer process recycled (a "zombie" registry entry), not just one that
|
|
51
|
+
* no longer exists. An unreadable/corrupt entry falls back to the pid-only
|
|
52
|
+
* check.
|
|
53
|
+
*/
|
|
54
|
+
export declare function prunePidSessionRegistry(isAlive: (pid: number, startedAtMs?: number) => boolean): void;
|
|
@@ -81,7 +81,43 @@ export function readPidSessionEntry(pid) {
|
|
|
81
81
|
}
|
|
82
82
|
return undefined;
|
|
83
83
|
}
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Every recorded launch, one entry per live-or-dead pid. Used to index launches
|
|
86
|
+
* by their `tmuxPane` so the authoritative tmux source can attribute a pane it did
|
|
87
|
+
* NOT wrap (an agent bare-spawned into an existing pane) to its exact launch —
|
|
88
|
+
* the caller filters to live pids. Best-effort: unreadable/corrupt files are
|
|
89
|
+
* skipped, a missing dir yields `[]`.
|
|
90
|
+
*/
|
|
91
|
+
export function listPidSessionEntries() {
|
|
92
|
+
let files;
|
|
93
|
+
try {
|
|
94
|
+
files = fs.readdirSync(pidRegistryDir()).filter(f => f.endsWith('.json'));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const f of files) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(pidRegistryDir(), f), 'utf8'));
|
|
103
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.pid === 'number') {
|
|
104
|
+
out.push(parsed);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* raced with a writer/pruner, or corrupt — skip */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Remove entries whose pid is no longer alive. Best-effort housekeeping.
|
|
115
|
+
*
|
|
116
|
+
* `isAlive` receives the entry's recorded `startedAtMs` so it can reject a pid
|
|
117
|
+
* that a newer process recycled (a "zombie" registry entry), not just one that
|
|
118
|
+
* no longer exists. An unreadable/corrupt entry falls back to the pid-only
|
|
119
|
+
* check.
|
|
120
|
+
*/
|
|
85
121
|
export function prunePidSessionRegistry(isAlive) {
|
|
86
122
|
let files;
|
|
87
123
|
try {
|
|
@@ -92,7 +128,18 @@ export function prunePidSessionRegistry(isAlive) {
|
|
|
92
128
|
}
|
|
93
129
|
for (const f of files) {
|
|
94
130
|
const pid = Number(f.slice(0, -'.json'.length));
|
|
95
|
-
if (!Number.isInteger(pid)
|
|
131
|
+
if (!Number.isInteger(pid))
|
|
132
|
+
continue;
|
|
133
|
+
let startedAtMs;
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(pidRegistryDir(), f), 'utf8'));
|
|
136
|
+
if (typeof parsed?.startedAtMs === 'number')
|
|
137
|
+
startedAtMs = parsed.startedAtMs;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* unreadable/corrupt — fall back to the pid-only liveness check */
|
|
141
|
+
}
|
|
142
|
+
if (isAlive(pid, startedAtMs))
|
|
96
143
|
continue;
|
|
97
144
|
try {
|
|
98
145
|
fs.unlinkSync(path.join(pidRegistryDir(), f));
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* wrapper tags, team-spawn boilerplate) so that only the real user intent
|
|
6
6
|
* remains. Used by the session picker to show a human-readable topic line.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* True when a `role=user` message is harness-injected scaffolding, not a genuine
|
|
10
|
+
* user turn. Text-based so it is cross-harness by construction (the markers are
|
|
11
|
+
* the same wrappers across Claude/Codex/etc.); harnesses that already drop their
|
|
12
|
+
* own scaffolding at parse time simply never reach here.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isSyntheticUserMessage(raw: string | undefined): boolean;
|
|
8
15
|
export declare const HEADLESS_PLAN_MODE_PREFIX = "You are running in HEADLESS PLAN MODE.";
|
|
9
16
|
/** Strip framework noise from a raw session prompt, returning only meaningful user text. */
|
|
10
17
|
export declare function cleanSessionPrompt(raw: string): string;
|
|
@@ -26,6 +26,43 @@ const NOISE_LINE_PATTERNS = [
|
|
|
26
26
|
];
|
|
27
27
|
/** XML tag prefix used by local-command messages. */
|
|
28
28
|
const LOCAL_COMMAND_PREFIX = '<local-command-caveat>';
|
|
29
|
+
/**
|
|
30
|
+
* Patterns that mark a whole `role=user` message as harness-injected scaffolding
|
|
31
|
+
* rather than a genuine user turn: the `!`-prefix shell echo and its output,
|
|
32
|
+
* slash-command wrappers, injected reminders / task-notifications, the local
|
|
33
|
+
* caveat, the interrupt marker, skill bodies, and hook feedback. This is the
|
|
34
|
+
* single cross-harness list — `isSyntheticUserMessage` is consumed by the parser
|
|
35
|
+
* (`parseSession`) to flag such events `_synthetic`, so `--include user` and the
|
|
36
|
+
* `--first`/`--last` turn split see only real intent, and every consumer (the
|
|
37
|
+
* session picker, `session-recall`, external hooks) gets clean user turns without
|
|
38
|
+
* re-implementing the stripping. Extend this list, not each caller.
|
|
39
|
+
*/
|
|
40
|
+
const SYNTHETIC_USER_MESSAGE_PATTERNS = [
|
|
41
|
+
/^\s*<\/?(?:bash-input|bash-stdout|bash-stderr)>/i,
|
|
42
|
+
/^\s*<\/?(?:command-name|command-message|command-args|command-contents)>/i,
|
|
43
|
+
/^\s*<\/?(?:local-command-stdout|local-command-stderr|local-command-caveat)>/i,
|
|
44
|
+
/^\s*<\/?system-reminder>/i,
|
|
45
|
+
/^\s*<\/?task-notification>/i,
|
|
46
|
+
/^\s*<\/?user-prompt-submit-hook>/i,
|
|
47
|
+
/^\s*<\/?persisted-output>/i,
|
|
48
|
+
/^\s*<permissions instructions>/i,
|
|
49
|
+
/^\s*<collaboration_mode>/i,
|
|
50
|
+
/^\s*Caveat: The messages below were generated by the user while running/i,
|
|
51
|
+
/^\s*\[Request interrupted/i,
|
|
52
|
+
/^\s*Base directory for this skill:/i,
|
|
53
|
+
/^\s*[A-Za-z][A-Za-z-]* hook feedback:/,
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* True when a `role=user` message is harness-injected scaffolding, not a genuine
|
|
57
|
+
* user turn. Text-based so it is cross-harness by construction (the markers are
|
|
58
|
+
* the same wrappers across Claude/Codex/etc.); harnesses that already drop their
|
|
59
|
+
* own scaffolding at parse time simply never reach here.
|
|
60
|
+
*/
|
|
61
|
+
export function isSyntheticUserMessage(raw) {
|
|
62
|
+
if (!raw)
|
|
63
|
+
return false;
|
|
64
|
+
return SYNTHETIC_USER_MESSAGE_PATTERNS.some(pattern => pattern.test(raw));
|
|
65
|
+
}
|
|
29
66
|
// Prefix prepended to every Claude-in-plan-mode team spawn prompt.
|
|
30
67
|
// Ends at a blank line before the real user task.
|
|
31
68
|
export const HEADLESS_PLAN_MODE_PREFIX = 'You are running in HEADLESS PLAN MODE.';
|
|
@@ -37,6 +37,13 @@ export interface SessionProvenance {
|
|
|
37
37
|
mux?: MuxLocation;
|
|
38
38
|
/** Whether an existing rail can type back into this session (see module doc). */
|
|
39
39
|
reply: ReplyRail;
|
|
40
|
+
/** The initiating device, resolved at query time from ssh.clientIp against the
|
|
41
|
+
* device registry (ssh transport only). Answers "who launched this and from
|
|
42
|
+
* where" without scraping ps/who/tailscale. */
|
|
43
|
+
origin?: {
|
|
44
|
+
device: string;
|
|
45
|
+
user?: string;
|
|
46
|
+
};
|
|
40
47
|
}
|
|
41
48
|
/** Env vars that carry provenance. Kept small so the macOS `ps` scan stays cheap. */
|
|
42
49
|
export declare const PROVENANCE_ENV_KEYS: readonly ["SSH_CONNECTION", "SSH_TTY", "TMUX", "TMUX_PANE", "TERM_PROGRAM", "STY", "ITERM_SESSION_ID"];
|
|
@@ -12,10 +12,15 @@ import type { SessionEvent, SessionMeta } from './types.js';
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function relativeToCwd(absPath: string, cwd?: string): string;
|
|
14
14
|
/**
|
|
15
|
-
* Wrap label in an OSC 8 hyperlink when the terminal
|
|
16
|
-
* Degrades to plain label otherwise.
|
|
15
|
+
* Wrap a filesystem path label in an OSC 8 `file://` hyperlink when the terminal
|
|
16
|
+
* supports it. Degrades to the plain label otherwise.
|
|
17
17
|
*/
|
|
18
18
|
export declare function linkPath(absPath: string, label: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Wrap a label in an OSC 8 hyperlink to an arbitrary URL (a PR on GitHub, a Linear
|
|
21
|
+
* issue, …) when the terminal supports it. Degrades to the plain label otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export declare function linkUrl(url: string, label: string): string;
|
|
19
24
|
/**
|
|
20
25
|
* Unwrap wrapper prefixes to find the actual executable.
|
|
21
26
|
*/
|
|
@@ -13,6 +13,7 @@ import { cleanSessionPrompt, extractSessionTopic } from './prompt.js';
|
|
|
13
13
|
import { renderMarkdown } from '../markdown.js';
|
|
14
14
|
import { redactSecrets } from '../redact.js';
|
|
15
15
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from './digest.js';
|
|
16
|
+
import { extractTodoProgressFromEvents } from './state.js';
|
|
16
17
|
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
17
18
|
/**
|
|
18
19
|
* Return absPath relative to cwd; fall back to ~/… then absolute.
|
|
@@ -28,19 +29,31 @@ export function relativeToCwd(absPath, cwd) {
|
|
|
28
29
|
}
|
|
29
30
|
return absPath;
|
|
30
31
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/
|
|
35
|
-
export function linkPath(absPath, label) {
|
|
36
|
-
if (process.stdout.isTTY &&
|
|
32
|
+
/** Best-effort feature-detect for OSC 8 hyperlink support in the current TTY. */
|
|
33
|
+
function supportsHyperlinks() {
|
|
34
|
+
return Boolean(process.stdout.isTTY &&
|
|
37
35
|
(process.env.TERM_PROGRAM ||
|
|
38
36
|
process.env.WT_SESSION ||
|
|
39
37
|
process.env.KITTY_WINDOW_ID ||
|
|
40
|
-
process.env.WEZTERM_PANE))
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
process.env.WEZTERM_PANE));
|
|
39
|
+
}
|
|
40
|
+
/** Wrap `label` in an OSC 8 hyperlink to `target`. Callers gate on {@link supportsHyperlinks}. */
|
|
41
|
+
function osc8(target, label) {
|
|
42
|
+
return `\x1b]8;;${target}\x1b\\${label}\x1b]8;;\x1b\\`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Wrap a filesystem path label in an OSC 8 `file://` hyperlink when the terminal
|
|
46
|
+
* supports it. Degrades to the plain label otherwise.
|
|
47
|
+
*/
|
|
48
|
+
export function linkPath(absPath, label) {
|
|
49
|
+
return supportsHyperlinks() ? osc8(`file://${absPath}`, label) : label;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Wrap a label in an OSC 8 hyperlink to an arbitrary URL (a PR on GitHub, a Linear
|
|
53
|
+
* issue, …) when the terminal supports it. Degrades to the plain label otherwise.
|
|
54
|
+
*/
|
|
55
|
+
export function linkUrl(url, label) {
|
|
56
|
+
return supportsHyperlinks() ? osc8(url, label) : label;
|
|
44
57
|
}
|
|
45
58
|
// ── Command grouping ──────────────────────────────────────────────────────────
|
|
46
59
|
/**
|
|
@@ -516,7 +529,7 @@ export function renderSummary(events, cwd) {
|
|
|
516
529
|
// Commands with timestamps
|
|
517
530
|
const cmdList = [];
|
|
518
531
|
// Plan items
|
|
519
|
-
const todoItems = [];
|
|
532
|
+
const todoItems = extractTodoProgressFromEvents(events)?.items.map(item => item.content) ?? [];
|
|
520
533
|
let exitPlanContent = null;
|
|
521
534
|
let planFilePath = null;
|
|
522
535
|
// Subagent spawns
|
|
@@ -557,19 +570,6 @@ export function renderSummary(events, cwd) {
|
|
|
557
570
|
recentActivity.push({ kind: 'cmd', label: cmd, ts });
|
|
558
571
|
}
|
|
559
572
|
}
|
|
560
|
-
// Plan items: TodoWrite items + TaskCreate descriptions (project's task tracker)
|
|
561
|
-
if (tool === 'TodoWrite' && Array.isArray(args.todos)) {
|
|
562
|
-
for (const item of args.todos) {
|
|
563
|
-
const text = item.content || item.text || String(item);
|
|
564
|
-
if (text && !todoItems.includes(text))
|
|
565
|
-
todoItems.push(text);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (tool === 'TaskCreate' && (args.description || args.prompt)) {
|
|
569
|
-
const text = String(args.description || args.prompt || '').slice(0, 140);
|
|
570
|
-
if (text && !todoItems.includes(text))
|
|
571
|
-
todoItems.push(text);
|
|
572
|
-
}
|
|
573
573
|
if (tool === 'ExitPlanMode') {
|
|
574
574
|
exitPlanContent = args.result || args.plan || args.content || null;
|
|
575
575
|
}
|
|
@@ -784,6 +784,9 @@ export function parseRoleList(raw, flag) {
|
|
|
784
784
|
return parts;
|
|
785
785
|
}
|
|
786
786
|
function roleOfEvent(e) {
|
|
787
|
+
// Synthetic scaffolding still maps to 'user' here so `--exclude user` drops it
|
|
788
|
+
// like any other user-role event (pre-flag behavior). `--include user`'s
|
|
789
|
+
// "genuine intent only" carve-out lives in applyRoleFilter, not here.
|
|
787
790
|
if (e.type === 'message' && e.role === 'user')
|
|
788
791
|
return 'user';
|
|
789
792
|
if (e.type === 'message' && e.role === 'assistant')
|
|
@@ -805,7 +808,14 @@ function applyRoleFilter(events, opts) {
|
|
|
805
808
|
const set = new Set(opts.include);
|
|
806
809
|
return events.filter(e => {
|
|
807
810
|
const role = roleOfEvent(e);
|
|
808
|
-
|
|
811
|
+
if (role === null)
|
|
812
|
+
return false;
|
|
813
|
+
// `--include user` means genuine user intent, so drop harness-injected
|
|
814
|
+
// `_synthetic` scaffolding (bash-input, system-reminder) even though it
|
|
815
|
+
// carries role=user. `--exclude user` still drops it via roleOfEvent.
|
|
816
|
+
if (role === 'user' && e._synthetic)
|
|
817
|
+
return false;
|
|
818
|
+
return set.has(role);
|
|
809
819
|
});
|
|
810
820
|
}
|
|
811
821
|
if (opts.exclude && opts.exclude.length > 0) {
|
|
@@ -833,7 +843,11 @@ function applyTurnSlice(events, opts) {
|
|
|
833
843
|
if (!Number.isFinite(n) || n <= 0) {
|
|
834
844
|
throw new Error(`Turn count must be a positive integer, got ${n}`);
|
|
835
845
|
}
|
|
836
|
-
|
|
846
|
+
// A turn starts at a GENUINE user message — harness-injected `_synthetic`
|
|
847
|
+
// scaffolding (`<bash-input>`/`<bash-stdout>`, etc.) does not start a turn,
|
|
848
|
+
// so `--first N` / `--last N` count real asks, not the jump command that
|
|
849
|
+
// opened the session and its shell output.
|
|
850
|
+
const isTurnStart = (e) => e.type === 'message' && e.role === 'user' && !e._synthetic;
|
|
837
851
|
const turnStartIdx = [];
|
|
838
852
|
for (let i = 0; i < events.length; i++)
|
|
839
853
|
if (isTurnStart(events[i]))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical derivation of a session's 8-char `shortId`.
|
|
3
|
+
*
|
|
4
|
+
* Every session parser used to inline `id.slice(0, 8)` — some with a
|
|
5
|
+
* `.replace(prefix, '')` strip first (`session_`, `api-`, `ses_`). That strip is
|
|
6
|
+
* the bug: an id that is *only* its prefix (a bare `session_` directory, an id of
|
|
7
|
+
* exactly `api-`) strips to `''`, and `''.slice(0, 8)` is still `''`. An empty
|
|
8
|
+
* shortId then passes the `short_id TEXT NOT NULL` constraint (empty string is not
|
|
9
|
+
* NULL) yet matches nothing in the `short_id LIKE ?` picker lookups — a silently
|
|
10
|
+
* corrupt index row.
|
|
11
|
+
*
|
|
12
|
+
* This helper is the single source of truth: it strips the optional prefix, takes
|
|
13
|
+
* the first 8 chars, and — the whole point — guarantees a NON-EMPTY result by
|
|
14
|
+
* falling back to the unstripped id when the strip empties it, so a row always
|
|
15
|
+
* stays addressable by its shortId.
|
|
16
|
+
*/
|
|
17
|
+
export declare function deriveShortId(id: string, stripPrefix?: RegExp): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical derivation of a session's 8-char `shortId`.
|
|
3
|
+
*
|
|
4
|
+
* Every session parser used to inline `id.slice(0, 8)` — some with a
|
|
5
|
+
* `.replace(prefix, '')` strip first (`session_`, `api-`, `ses_`). That strip is
|
|
6
|
+
* the bug: an id that is *only* its prefix (a bare `session_` directory, an id of
|
|
7
|
+
* exactly `api-`) strips to `''`, and `''.slice(0, 8)` is still `''`. An empty
|
|
8
|
+
* shortId then passes the `short_id TEXT NOT NULL` constraint (empty string is not
|
|
9
|
+
* NULL) yet matches nothing in the `short_id LIKE ?` picker lookups — a silently
|
|
10
|
+
* corrupt index row.
|
|
11
|
+
*
|
|
12
|
+
* This helper is the single source of truth: it strips the optional prefix, takes
|
|
13
|
+
* the first 8 chars, and — the whole point — guarantees a NON-EMPTY result by
|
|
14
|
+
* falling back to the unstripped id when the strip empties it, so a row always
|
|
15
|
+
* stays addressable by its shortId.
|
|
16
|
+
*/
|
|
17
|
+
export function deriveShortId(id, stripPrefix) {
|
|
18
|
+
const stripped = stripPrefix ? id.replace(stripPrefix, '') : id;
|
|
19
|
+
return stripped.slice(0, 8) || id.slice(0, 8) || id;
|
|
20
|
+
}
|
|
@@ -126,6 +126,10 @@ export interface StateContext {
|
|
|
126
126
|
* list, so a session with no plan carries no `todos` field.
|
|
127
127
|
*/
|
|
128
128
|
export declare function extractTodoProgress(args?: Record<string, any>): TodoProgress | undefined;
|
|
129
|
+
/** Fold snapshot checklist tools and Claude TaskCreate/TaskUpdate event logs. */
|
|
130
|
+
export declare function extractTodoProgressFromEvents(events: SessionEvent[]): TodoProgress | undefined;
|
|
131
|
+
/** Derive a recency-ordered, de-duplicated list of directories touched by tools. */
|
|
132
|
+
export declare function extractRecentDirectoriesTouched(events: SessionEvent[], cwd?: string): string[] | undefined;
|
|
129
133
|
/** Detect a worktree from the session cwd, per the `.agents/worktrees/<slug>/` convention. */
|
|
130
134
|
export declare function detectWorktree(cwd?: string, branch?: string): DetectedWorktree | undefined;
|
|
131
135
|
/** Detect a tracker ticket from free text (prompt/topic) then a branch name. */
|