@phnx-labs/agents-cli 1.20.76 → 1.20.78
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 +264 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +143 -9
- package/dist/commands/exec.js +87 -19
- package/dist/commands/repo.js +8 -0
- package/dist/commands/routines.js +275 -15
- package/dist/commands/secrets.js +65 -17
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +128 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-picker.d.ts +28 -1
- package/dist/commands/sessions-picker.js +222 -15
- package/dist/commands/sessions.d.ts +117 -4
- package/dist/commands/sessions.js +350 -62
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +48 -3
- package/dist/index.js +5 -6
- package/dist/lib/activity.d.ts +5 -4
- package/dist/lib/activity.js +450 -36
- package/dist/lib/agents.d.ts +0 -21
- package/dist/lib/agents.js +0 -37
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/cloud/session-index.js +2 -2
- package/dist/lib/daemon.d.ts +9 -32
- package/dist/lib/daemon.js +70 -84
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +16 -1
- package/dist/lib/devices/fleet.js +10 -2
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +27 -4
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/git.d.ts +13 -0
- package/dist/lib/git.js +102 -4
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hosts/dispatch.d.ts +9 -0
- package/dist/lib/hosts/dispatch.js +24 -4
- package/dist/lib/hosts/passthrough.js +7 -2
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +33 -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 +4 -0
- package/dist/lib/hosts/run-target.js +5 -1
- package/dist/lib/hosts/session-index.js +3 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +9 -0
- package/dist/lib/menubar/install-menubar.js +14 -0
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/overdue.d.ts +4 -5
- package/dist/lib/overdue.js +8 -41
- 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 +68 -0
- package/dist/lib/routines.js +74 -7
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +156 -28
- package/dist/lib/sandbox.js +0 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +11 -4
- package/dist/lib/secrets/agent.js +46 -24
- package/dist/lib/secrets/bundles.d.ts +21 -7
- package/dist/lib/secrets/bundles.js +82 -38
- package/dist/lib/secrets/index.d.ts +11 -2
- package/dist/lib/secrets/index.js +21 -9
- package/dist/lib/secrets/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +16 -2
- package/dist/lib/secrets/session-store.js +118 -15
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/session/active.d.ts +17 -0
- package/dist/lib/session/active.js +105 -7
- package/dist/lib/session/db.d.ts +43 -0
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +28 -3
- package/dist/lib/session/discover.js +110 -14
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/parse.js +11 -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.js +18 -16
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +94 -12
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/state.d.ts +7 -3
- package/dist/lib/state.js +20 -5
- package/dist/lib/types.d.ts +10 -0
- package/package.json +1 -1
|
@@ -99,31 +99,136 @@ export function toolHistogram(toolCounts, top = 8) {
|
|
|
99
99
|
.sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool))
|
|
100
100
|
.slice(0, top);
|
|
101
101
|
}
|
|
102
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Recognized test/build runners → the label we show.
|
|
104
|
+
*
|
|
105
|
+
* These match a *real invocation* of a runner, not any command that merely
|
|
106
|
+
* contains a runner token. In particular we must NOT fire on npm-script
|
|
107
|
+
* sub-targets like `bun test:setup`, `npm run test:watch`, or `pnpm test:ci` —
|
|
108
|
+
* those are setup/watch/CI-orchestration scripts, not a test run whose output we
|
|
109
|
+
* can scrape for a pass/fail verdict. A `test` token followed by `:something` is
|
|
110
|
+
* a distinct script name and is excluded via the `(?![:\w-])` guard.
|
|
111
|
+
*/
|
|
103
112
|
const TEST_RUNNERS = [
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{ re: /\
|
|
108
|
-
|
|
113
|
+
// Package-manager `test` script: `bun test`, `npm test`, `npm run test`,
|
|
114
|
+
// `yarn test`, `pnpm test`. The negative lookahead `(?![:\w-])` rejects
|
|
115
|
+
// `test:watch` / `test-ci` / `testfoo` — only the bare `test` target counts.
|
|
116
|
+
{ re: /\b(?:bun|npm|yarn|pnpm)\s+(?:run\s+)?test(?![:\w-])/, label: 'tests' },
|
|
117
|
+
// Direct binaries — `vitest`, `jest`, `mocha` (optionally via a runner). These
|
|
118
|
+
// are real invocations; a trailing subcommand like `vitest run` is fine.
|
|
119
|
+
{ re: /\b(?:vitest|jest|mocha)(?![:\w-])/, label: 'tests' },
|
|
120
|
+
{ re: /\bpytest(?![:\w-])/, label: 'pytest' },
|
|
121
|
+
{ re: /\bgo\s+test(?![:\w-])/, label: 'go test' },
|
|
122
|
+
{ re: /\bcargo\s+test(?![:\w-])/, label: 'cargo test' },
|
|
123
|
+
{ re: /\btsc(?:\s+--noEmit)?(?![:\w-])/, label: 'tsc' },
|
|
109
124
|
];
|
|
125
|
+
/**
|
|
126
|
+
* Anchored summary-line extractors, per runner family. Each returns a pass/fail
|
|
127
|
+
* verdict ONLY when it matches that runner's authoritative summary construct —
|
|
128
|
+
* never an arbitrary `\d+ pass`-shaped substring elsewhere in stdout. This is
|
|
129
|
+
* what stops `442 passwords generated`, `442 files`, or `442 passes/sec` from
|
|
130
|
+
* being reported as `442 pass`.
|
|
131
|
+
*/
|
|
132
|
+
function parseSummaryLine(output) {
|
|
133
|
+
const lines = output.split(/\r?\n/);
|
|
134
|
+
// vitest: ` Tests 1 failed | 2 passed (3)` (mixed) or ` Tests 2 passed (2)`
|
|
135
|
+
// (all green). Leading indent + the `Tests` / `Test Files` label + double
|
|
136
|
+
// space is the summary row. Prefer the ` Tests ` row (test count) over the
|
|
137
|
+
// ` Test Files ` row (file count).
|
|
138
|
+
for (const label of ['Tests', 'Test Files']) {
|
|
139
|
+
for (const line of lines) {
|
|
140
|
+
const row = new RegExp(`^\\s*${label}\\s{2,}(.+)$`).exec(line);
|
|
141
|
+
if (!row)
|
|
142
|
+
continue;
|
|
143
|
+
const seg = row[1];
|
|
144
|
+
const passed = /(\d+)\s+passed/.exec(seg);
|
|
145
|
+
const failed = /(\d+)\s+failed/.exec(seg);
|
|
146
|
+
// Only accept a vitest row that actually reports a pass/fail count.
|
|
147
|
+
if (passed || failed) {
|
|
148
|
+
return {
|
|
149
|
+
passed: passed ? +passed[1] : undefined,
|
|
150
|
+
failed: failed ? +failed[1] : 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// jest: `Tests: 1 failed, 2 passed, 3 total`. The `Tests:` label anchors
|
|
156
|
+
// it; order of failed/passed varies.
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
const row = /^\s*Tests:\s+(.+)$/.exec(line);
|
|
159
|
+
if (!row)
|
|
160
|
+
continue;
|
|
161
|
+
const seg = row[1];
|
|
162
|
+
const passed = /(\d+)\s+passed/.exec(seg);
|
|
163
|
+
const failed = /(\d+)\s+failed/.exec(seg);
|
|
164
|
+
if (passed || failed) {
|
|
165
|
+
return { passed: passed ? +passed[1] : undefined, failed: failed ? +failed[1] : 0 };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// pytest: the summary rule line `==== 3 passed, 1 failed in 0.12s ====`. The
|
|
169
|
+
// surrounding `=` rule and the ` in <dur>s` tail anchor it to the real footer,
|
|
170
|
+
// not a stray `N passed` in captured stdout.
|
|
171
|
+
for (const line of lines) {
|
|
172
|
+
if (!/={3,}.*\bin\s+[\d.]+s\b.*={3,}/.test(line) && !/^={3,}.*={3,}$/.test(line))
|
|
173
|
+
continue;
|
|
174
|
+
if (!/\bin\s+[\d.]+m?s\b/.test(line))
|
|
175
|
+
continue;
|
|
176
|
+
const passed = /(\d+)\s+passed/.exec(line);
|
|
177
|
+
const failed = /(\d+)\s+failed/.exec(line);
|
|
178
|
+
const errors = /(\d+)\s+error/.exec(line);
|
|
179
|
+
if (passed || failed || errors) {
|
|
180
|
+
const failCount = (failed ? +failed[1] : 0) + (errors ? +errors[1] : 0);
|
|
181
|
+
return { passed: passed ? +passed[1] : undefined, failed: failed || errors ? failCount : 0 };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// bun test: a summary block of ` N pass` / ` N fail` lines (each its own line,
|
|
185
|
+
// leading-indented), closed by `Ran N tests across M files.`. Require the
|
|
186
|
+
// `Ran … tests` sentinel to be present so we only read bun's real summary
|
|
187
|
+
// block — bun also prints inline `(pass)`/`(fail)` per-test lines we ignore.
|
|
188
|
+
if (/^Ran\s+\d+\s+tests?\b/m.test(output)) {
|
|
189
|
+
let passed;
|
|
190
|
+
let failed;
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
const p = /^\s*(\d+)\s+pass$/.exec(line);
|
|
193
|
+
const f = /^\s*(\d+)\s+fail$/.exec(line);
|
|
194
|
+
if (p)
|
|
195
|
+
passed = +p[1];
|
|
196
|
+
if (f)
|
|
197
|
+
failed = +f[1];
|
|
198
|
+
}
|
|
199
|
+
if (passed !== undefined || failed !== undefined) {
|
|
200
|
+
return { passed, failed: failed ?? 0 };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// mocha: ` 2 passing` / ` 1 failing` (leading-indented, own line). The
|
|
204
|
+
// `passing`/`failing` gerund is mocha-specific and won't match `442 passwords`.
|
|
205
|
+
{
|
|
206
|
+
let passed;
|
|
207
|
+
let failed;
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
const p = /^\s*(\d+)\s+passing\b/.exec(line);
|
|
210
|
+
const f = /^\s*(\d+)\s+failing\b/.exec(line);
|
|
211
|
+
if (p)
|
|
212
|
+
passed = +p[1];
|
|
213
|
+
if (f)
|
|
214
|
+
failed = +f[1];
|
|
215
|
+
}
|
|
216
|
+
if (passed !== undefined || failed !== undefined) {
|
|
217
|
+
return { passed, failed: failed ?? 0 };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
110
222
|
/** Parse pass/fail counts from common runner output. */
|
|
111
223
|
function parseTestOutput(runner, output) {
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
val = +m[1];
|
|
121
|
-
return val;
|
|
122
|
-
};
|
|
123
|
-
const passed = lastNum(/(\d+)\s+pass(?:ed)?/);
|
|
124
|
-
const failed = lastNum(/(\d+)\s+fail(?:ed|ures?)?/);
|
|
125
|
-
if (passed !== undefined || failed !== undefined) {
|
|
126
|
-
return { passed, failed, ok: true };
|
|
224
|
+
// Anchor to a recognized runner summary construct (vitest ` Tests `, jest
|
|
225
|
+
// `Tests:`, pytest `=== N passed in Xs ===`, bun's `N pass`/`N fail` block,
|
|
226
|
+
// mocha `N passing`) — never a blanket `\d+ pass`-shaped scrape over stdout.
|
|
227
|
+
// That blanket scrape used to report `442 passwords`, `442 files passed
|
|
228
|
+
// validation`, or a `442 passes/sec` benchmark as `442 pass`.
|
|
229
|
+
const summary = parseSummaryLine(output);
|
|
230
|
+
if (summary) {
|
|
231
|
+
return { ...summary, ok: true };
|
|
127
232
|
}
|
|
128
233
|
// tsc: no news is good news; "error TSxxxx" means failure.
|
|
129
234
|
if (runner === 'tsc') {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* the last run are re-parsed. All metadata is upserted into the sessions DB so
|
|
7
7
|
* subsequent queries are served entirely from the cache.
|
|
8
8
|
*/
|
|
9
|
-
import type { SessionAgentId, SessionMeta } from './types.js';
|
|
9
|
+
import type { SessionAgentId, SessionEvent, SessionMeta, TodoProgress } from './types.js';
|
|
10
|
+
export { machineForSessionFile } from './origin-machine.js';
|
|
10
11
|
import { type ScanStamp } from './db.js';
|
|
11
12
|
/** Options controlling which sessions to discover and how to report progress. */
|
|
12
13
|
export interface DiscoverOptions {
|
|
@@ -82,6 +83,8 @@ interface ClaudeSessionScan {
|
|
|
82
83
|
spawnedTeam?: string;
|
|
83
84
|
/** Plan markdown from the last ExitPlanMode tool call (Claude sessions only). */
|
|
84
85
|
plan?: string;
|
|
86
|
+
todos?: TodoProgress;
|
|
87
|
+
recentDirectoriesTouched?: string[];
|
|
85
88
|
}
|
|
86
89
|
/** Lightweight metadata extracted from a Codex JSONL file during incremental scan. */
|
|
87
90
|
interface CodexSessionScan {
|
|
@@ -105,6 +108,8 @@ interface CodexSessionScan {
|
|
|
105
108
|
ticketId?: string;
|
|
106
109
|
createdTickets?: string[];
|
|
107
110
|
spawnedTeam?: string;
|
|
111
|
+
todos?: TodoProgress;
|
|
112
|
+
recentDirectoriesTouched?: string[];
|
|
108
113
|
}
|
|
109
114
|
/**
|
|
110
115
|
* Discover sessions. Scans only files whose (mtime, size) have changed since
|
|
@@ -144,7 +149,6 @@ export declare function scanAgentsBounded<T>(items: readonly T[], run: (item: T)
|
|
|
144
149
|
* `agents add --isolated`, where the whole point was to keep the two apart.
|
|
145
150
|
*/
|
|
146
151
|
export declare function isManagedSessionFile(filePath: string): boolean;
|
|
147
|
-
export declare function machineForSessionFile(filePath: string, agent: string): string;
|
|
148
152
|
/**
|
|
149
153
|
* Count sessions in scope without running an incremental scan. Assumes the DB
|
|
150
154
|
* is already fresh (typically true because `discoverSessions` ran first this
|
|
@@ -190,6 +194,20 @@ export declare function _normalizeCwdForTest(cwd?: string): string;
|
|
|
190
194
|
* ids, whose charset is too permissive to distinguish from a search phrase.
|
|
191
195
|
*/
|
|
192
196
|
export declare function isCompleteSessionId(query: string): boolean;
|
|
197
|
+
/**
|
|
198
|
+
* Whether a query should be treated as a session id rather than a search phrase
|
|
199
|
+
* — the one canonical id-shaped test, shared by every session-id resolver.
|
|
200
|
+
*
|
|
201
|
+
* True for a complete id (`isCompleteSessionId`) AND for a bare hex short-id or
|
|
202
|
+
* prefix (`d3470b57`), which the complete-id check rejects. Any id-shaped query
|
|
203
|
+
* resolves by id ONLY (exact -> prefix -> `findSessionsById` index) and must
|
|
204
|
+
* never fall back to fuzzy content search: a short id like `d3470b57` otherwise
|
|
205
|
+
* surfaces every transcript that merely MENTIONS the string (a resume prompt
|
|
206
|
+
* echoes the parent id into the body of many later sessions). The hex test
|
|
207
|
+
* catches the bare short-id/prefix; `isCompleteSessionId` additionally catches
|
|
208
|
+
* the prefixed whole ids (`session_…`, `ses_…`) that the hex test rejects.
|
|
209
|
+
*/
|
|
210
|
+
export declare function looksLikeSessionId(query: string): boolean;
|
|
193
211
|
/**
|
|
194
212
|
* Resolve a session by full or short ID. Accepts a pre-loaded session list
|
|
195
213
|
* (fast path from discoverSessions) and falls back to a DB lookup for the
|
|
@@ -369,6 +387,8 @@ export interface ClaudeParseState {
|
|
|
369
387
|
pendingTicketTools: Set<string>;
|
|
370
388
|
spawnedTeam?: string;
|
|
371
389
|
plan?: string;
|
|
390
|
+
checklistEvents: SessionEvent[];
|
|
391
|
+
recentDirectoriesTouched: string[];
|
|
372
392
|
}
|
|
373
393
|
/** Zero-value accumulator for a fresh (from-byte-0) Claude parse. */
|
|
374
394
|
export declare function initClaudeParseState(): ClaudeParseState;
|
|
@@ -427,6 +447,8 @@ export interface ClaudeParserState {
|
|
|
427
447
|
spawnedTeam?: string;
|
|
428
448
|
ticketId?: string;
|
|
429
449
|
contentText?: string;
|
|
450
|
+
checklistEvents: SessionEvent[];
|
|
451
|
+
recentDirectoriesTouched: string[];
|
|
430
452
|
}
|
|
431
453
|
/**
|
|
432
454
|
* Snapshot a live {@link ClaudeParseState} into its serializable form at
|
|
@@ -500,6 +522,8 @@ export interface CodexParseState {
|
|
|
500
522
|
createdTickets: Set<string>;
|
|
501
523
|
pendingTicketTools: Set<string>;
|
|
502
524
|
spawnedTeam?: string;
|
|
525
|
+
checklistEvents: SessionEvent[];
|
|
526
|
+
recentDirectoriesTouched: string[];
|
|
503
527
|
}
|
|
504
528
|
/** Zero-value accumulator for a fresh (from-byte-0) Codex parse. */
|
|
505
529
|
export declare function initCodexParseState(): CodexParseState;
|
|
@@ -549,6 +573,8 @@ export interface CodexParserState {
|
|
|
549
573
|
spawnedTeam?: string;
|
|
550
574
|
ticketId?: string;
|
|
551
575
|
contentText?: string;
|
|
576
|
+
checklistEvents: SessionEvent[];
|
|
577
|
+
recentDirectoriesTouched: string[];
|
|
552
578
|
}
|
|
553
579
|
/**
|
|
554
580
|
* Snapshot a live {@link CodexParseState} into its serializable form at `offset`
|
|
@@ -641,4 +667,3 @@ export declare function readGrokMeta(filePath: string, currentVersion?: string):
|
|
|
641
667
|
} | null;
|
|
642
668
|
/** Parse a time filter string (relative like '7d' or ISO timestamp) into epoch milliseconds. */
|
|
643
669
|
export declare function parseTimeFilter(input: string): number;
|
|
644
|
-
export {};
|
|
@@ -24,12 +24,14 @@ import { SESSION_AGENTS } from './types.js';
|
|
|
24
24
|
import { deriveShortId } from './short-id.js';
|
|
25
25
|
import { extractSessionTopic } from './prompt.js';
|
|
26
26
|
import { parseAntigravity } from './parse.js';
|
|
27
|
-
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket } from './state.js';
|
|
27
|
+
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
28
28
|
import { costOfUsage } from '../pricing/index.js';
|
|
29
|
-
import {
|
|
29
|
+
import { machineForSessionFile } from './origin-machine.js';
|
|
30
|
+
export { machineForSessionFile } from './origin-machine.js';
|
|
30
31
|
import { mapBounded } from '../concurrency.js';
|
|
31
|
-
import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
|
|
32
|
+
import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, cacheLinearProject, } from './db.js';
|
|
32
33
|
import { buildRunNameMap } from './run-names.js';
|
|
34
|
+
import { resolveLinearApiKey } from '../auto-dispatch-linear.js';
|
|
33
35
|
const HOME = os.homedir();
|
|
34
36
|
// Versions can live under either repo: the user repo (current canonical
|
|
35
37
|
// location, ~/.agents/.history/versions/) or the system repo (legacy / npm-shipped,
|
|
@@ -99,10 +101,49 @@ export async function discoverSessions(options) {
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
const sessions = querySessions(buildQueryOptions(options, agents, { includeLimit: true }));
|
|
104
|
+
await resolveLinearProjects(sessions);
|
|
102
105
|
for (const s of sessions)
|
|
103
106
|
s.machine = machineForSessionFile(s.filePath, s.agent);
|
|
104
107
|
return scopeToManaged(sessions, agents, options);
|
|
105
108
|
}
|
|
109
|
+
const linearProjectCache = new Map();
|
|
110
|
+
async function resolveLinearProjects(sessions) {
|
|
111
|
+
const apiKey = resolveLinearApiKey();
|
|
112
|
+
if (!apiKey)
|
|
113
|
+
return;
|
|
114
|
+
await Promise.all(sessions.map(async (session) => {
|
|
115
|
+
if (!session.ticketId || session.linearProject)
|
|
116
|
+
return;
|
|
117
|
+
let project = linearProjectCache.get(session.ticketId);
|
|
118
|
+
if (project === undefined) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
signal: AbortSignal.timeout(3_000),
|
|
123
|
+
headers: { Authorization: apiKey, 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
query: `query($id:String!){ issue(id:$id){ project{ name url } } }`,
|
|
126
|
+
variables: { id: session.ticketId },
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
if (!response.ok)
|
|
130
|
+
throw new Error(String(response.status));
|
|
131
|
+
const body = await response.json();
|
|
132
|
+
const node = body.data?.issue?.project;
|
|
133
|
+
project = node?.name && node?.url ? { name: node.name, url: node.url } : null;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
project = null;
|
|
137
|
+
}
|
|
138
|
+
linearProjectCache.set(session.ticketId, project);
|
|
139
|
+
}
|
|
140
|
+
if (!project)
|
|
141
|
+
return;
|
|
142
|
+
session.linearProject = project.name;
|
|
143
|
+
session.linearProjectUrl = project.url;
|
|
144
|
+
cacheLinearProject(session.id, project.name, project.url);
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
106
147
|
/**
|
|
107
148
|
* Drop unmanaged rows for agents that HAVE managed versions.
|
|
108
149
|
*
|
|
@@ -183,7 +224,6 @@ function dispatchAgentScan(agent, onProgress) {
|
|
|
183
224
|
default: return Promise.resolve();
|
|
184
225
|
}
|
|
185
226
|
}
|
|
186
|
-
let _localMachineId;
|
|
187
227
|
/**
|
|
188
228
|
* The machine a discovered session originated on. Cross-machine sync mirrors a
|
|
189
229
|
* remote transcript to backups/<agent>/<machine>/<subdir>/… (see mirrorPath in
|
|
@@ -230,15 +270,6 @@ export function isManagedSessionFile(filePath) {
|
|
|
230
270
|
return !!realRoot && real.startsWith(realRoot + path.sep);
|
|
231
271
|
});
|
|
232
272
|
}
|
|
233
|
-
export function machineForSessionFile(filePath, agent) {
|
|
234
|
-
const base = path.join(getHistoryDir(), 'backups', agent) + path.sep;
|
|
235
|
-
if (filePath.startsWith(base)) {
|
|
236
|
-
const seg = filePath.slice(base.length).split(path.sep)[0];
|
|
237
|
-
if (seg)
|
|
238
|
-
return seg;
|
|
239
|
-
}
|
|
240
|
-
return (_localMachineId ??= machineId());
|
|
241
|
-
}
|
|
242
273
|
/**
|
|
243
274
|
* Count sessions in scope without running an incremental scan. Assumes the DB
|
|
244
275
|
* is already fresh (typically true because `discoverSessions` ran first this
|
|
@@ -348,6 +379,23 @@ export function isCompleteSessionId(query) {
|
|
|
348
379
|
const q = query.trim().toLowerCase();
|
|
349
380
|
return UUID_36.test(q.replace(SESSION_UUID_PREFIX, '')) || SES_ULID.test(q);
|
|
350
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Whether a query should be treated as a session id rather than a search phrase
|
|
384
|
+
* — the one canonical id-shaped test, shared by every session-id resolver.
|
|
385
|
+
*
|
|
386
|
+
* True for a complete id (`isCompleteSessionId`) AND for a bare hex short-id or
|
|
387
|
+
* prefix (`d3470b57`), which the complete-id check rejects. Any id-shaped query
|
|
388
|
+
* resolves by id ONLY (exact -> prefix -> `findSessionsById` index) and must
|
|
389
|
+
* never fall back to fuzzy content search: a short id like `d3470b57` otherwise
|
|
390
|
+
* surfaces every transcript that merely MENTIONS the string (a resume prompt
|
|
391
|
+
* echoes the parent id into the body of many later sessions). The hex test
|
|
392
|
+
* catches the bare short-id/prefix; `isCompleteSessionId` additionally catches
|
|
393
|
+
* the prefixed whole ids (`session_…`, `ses_…`) that the hex test rejects.
|
|
394
|
+
*/
|
|
395
|
+
export function looksLikeSessionId(query) {
|
|
396
|
+
const trimmed = query.trim();
|
|
397
|
+
return /^[0-9a-f-]{6,}$/i.test(trimmed) || isCompleteSessionId(trimmed);
|
|
398
|
+
}
|
|
351
399
|
/**
|
|
352
400
|
* Resolve a session by full or short ID. Accepts a pre-loaded session list
|
|
353
401
|
* (fast path from discoverSessions) and falls back to a DB lookup for the
|
|
@@ -968,6 +1016,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
968
1016
|
createdTickets: scan.createdTickets,
|
|
969
1017
|
spawnedTeam: scan.spawnedTeam,
|
|
970
1018
|
plan: scan.plan,
|
|
1019
|
+
todos: scan.todos,
|
|
1020
|
+
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
971
1021
|
};
|
|
972
1022
|
}
|
|
973
1023
|
else {
|
|
@@ -995,6 +1045,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
995
1045
|
createdTickets: scan.createdTickets,
|
|
996
1046
|
spawnedTeam: scan.spawnedTeam,
|
|
997
1047
|
plan: scan.plan,
|
|
1048
|
+
todos: scan.todos,
|
|
1049
|
+
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
998
1050
|
};
|
|
999
1051
|
}
|
|
1000
1052
|
return {
|
|
@@ -1288,6 +1340,8 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1288
1340
|
ticketId: scan.ticketId,
|
|
1289
1341
|
createdTickets: scan.createdTickets,
|
|
1290
1342
|
spawnedTeam: scan.spawnedTeam,
|
|
1343
|
+
todos: scan.todos,
|
|
1344
|
+
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
1291
1345
|
};
|
|
1292
1346
|
return {
|
|
1293
1347
|
meta,
|
|
@@ -2370,8 +2424,27 @@ export function initClaudeParseState() {
|
|
|
2370
2424
|
pendingTicketTools: new Set(),
|
|
2371
2425
|
spawnedTeam: undefined,
|
|
2372
2426
|
plan: undefined,
|
|
2427
|
+
checklistEvents: [],
|
|
2428
|
+
recentDirectoriesTouched: [],
|
|
2373
2429
|
};
|
|
2374
2430
|
}
|
|
2431
|
+
const CHECKLIST_TOOLS = new Set(['TodoWrite', 'todo_write', 'update_plan', 'TaskCreate', 'TaskUpdate']);
|
|
2432
|
+
const DIRECTORY_TOOLS = new Set(['Edit', 'Write', 'edit_file', 'write_file', 'create_file', 'edit', 'write', 'Bash', 'exec_command', 'run_shell_command', 'shell', 'Execute']);
|
|
2433
|
+
function foldDerivedToolState(state, event) {
|
|
2434
|
+
if (CHECKLIST_TOOLS.has(event.tool ?? ''))
|
|
2435
|
+
state.checklistEvents.push(event);
|
|
2436
|
+
if (!DIRECTORY_TOOLS.has(event.tool ?? ''))
|
|
2437
|
+
return;
|
|
2438
|
+
const next = extractRecentDirectoriesTouched([event], state.cwd);
|
|
2439
|
+
for (const dir of next ?? []) {
|
|
2440
|
+
const old = state.recentDirectoriesTouched.indexOf(dir);
|
|
2441
|
+
if (old >= 0)
|
|
2442
|
+
state.recentDirectoriesTouched.splice(old, 1);
|
|
2443
|
+
state.recentDirectoriesTouched.push(dir);
|
|
2444
|
+
}
|
|
2445
|
+
if (state.recentDirectoriesTouched.length > 10)
|
|
2446
|
+
state.recentDirectoriesTouched.splice(0, state.recentDirectoriesTouched.length - 10);
|
|
2447
|
+
}
|
|
2375
2448
|
/**
|
|
2376
2449
|
* Fold one parsed transcript line into the accumulator. This is the exact loop
|
|
2377
2450
|
* body {@link scanClaudeSession} used to run inline — extracted verbatim,
|
|
@@ -2407,6 +2480,10 @@ export function applyClaudeLine(state, parsed) {
|
|
|
2407
2480
|
if (p)
|
|
2408
2481
|
state.plan = b.input.plan;
|
|
2409
2482
|
}
|
|
2483
|
+
foldDerivedToolState(state, {
|
|
2484
|
+
type: 'tool_use', agent: 'claude', timestamp: parsed.timestamp || '', tool: b?.name, args: b?.input || {},
|
|
2485
|
+
path: b?.input?.file_path || b?.input?.path, command: b?.input?.command,
|
|
2486
|
+
});
|
|
2410
2487
|
}
|
|
2411
2488
|
}
|
|
2412
2489
|
if (state.pendingTicketTools.size > 0 && parsed.type === 'user' && Array.isArray(parsed.message?.content)) {
|
|
@@ -2558,6 +2635,8 @@ export function finalizeClaudeScan(state) {
|
|
|
2558
2635
|
createdTickets: state.createdTickets.size > 0 ? [...state.createdTickets] : undefined,
|
|
2559
2636
|
spawnedTeam: state.spawnedTeam,
|
|
2560
2637
|
plan: state.plan,
|
|
2638
|
+
todos: extractTodoProgressFromEvents(state.checklistEvents),
|
|
2639
|
+
recentDirectoriesTouched: state.recentDirectoriesTouched.length ? state.recentDirectoriesTouched : undefined,
|
|
2561
2640
|
};
|
|
2562
2641
|
}
|
|
2563
2642
|
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
@@ -2632,6 +2711,8 @@ export function serializeClaudeParserState(state, offset) {
|
|
|
2632
2711
|
// not be persisted.
|
|
2633
2712
|
ticketId: ticket?.id,
|
|
2634
2713
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
2714
|
+
checklistEvents: state.checklistEvents,
|
|
2715
|
+
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
2635
2716
|
};
|
|
2636
2717
|
}
|
|
2637
2718
|
/**
|
|
@@ -2684,6 +2765,8 @@ export function hydrateClaudeParseState(prior) {
|
|
|
2684
2765
|
pendingTicketTools: new Set(prior.pendingTicketTools),
|
|
2685
2766
|
spawnedTeam: prior.spawnedTeam,
|
|
2686
2767
|
plan: prior.plan,
|
|
2768
|
+
checklistEvents: prior.checklistEvents ?? [],
|
|
2769
|
+
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
2687
2770
|
};
|
|
2688
2771
|
}
|
|
2689
2772
|
/**
|
|
@@ -2883,6 +2966,8 @@ export function initCodexParseState() {
|
|
|
2883
2966
|
createdTickets: new Set(),
|
|
2884
2967
|
pendingTicketTools: new Set(),
|
|
2885
2968
|
spawnedTeam: undefined,
|
|
2969
|
+
checklistEvents: [],
|
|
2970
|
+
recentDirectoriesTouched: [],
|
|
2886
2971
|
};
|
|
2887
2972
|
}
|
|
2888
2973
|
/**
|
|
@@ -2898,11 +2983,16 @@ export function applyCodexLine(state, parsed) {
|
|
|
2898
2983
|
const p = parsed.payload || {};
|
|
2899
2984
|
if (p.type === 'function_call') {
|
|
2900
2985
|
let cmd = '';
|
|
2986
|
+
let args = {};
|
|
2901
2987
|
try {
|
|
2902
|
-
|
|
2988
|
+
args = typeof p.arguments === 'string' ? JSON.parse(p.arguments) : (p.arguments || {});
|
|
2903
2989
|
cmd = String(args.command || args.cmd || '');
|
|
2904
2990
|
}
|
|
2905
2991
|
catch { /* non-JSON args */ }
|
|
2992
|
+
foldDerivedToolState(state, {
|
|
2993
|
+
type: 'tool_use', agent: 'codex', timestamp: parsed.timestamp || '', tool: p.name, args,
|
|
2994
|
+
path: args.file_path || args.path, command: cmd || undefined,
|
|
2995
|
+
});
|
|
2906
2996
|
if (!state.prUrl && !state.sawPrCreate && isPrCreateCommand(cmd))
|
|
2907
2997
|
state.sawPrCreate = true;
|
|
2908
2998
|
if (!state.spawnedTeam) {
|
|
@@ -3024,6 +3114,8 @@ export function finalizeCodexScan(state) {
|
|
|
3024
3114
|
ticketId: ticket?.id,
|
|
3025
3115
|
createdTickets: state.createdTickets.size > 0 ? [...state.createdTickets] : undefined,
|
|
3026
3116
|
spawnedTeam: state.spawnedTeam,
|
|
3117
|
+
todos: extractTodoProgressFromEvents(state.checklistEvents),
|
|
3118
|
+
recentDirectoriesTouched: state.recentDirectoriesTouched.length ? state.recentDirectoriesTouched : undefined,
|
|
3027
3119
|
};
|
|
3028
3120
|
}
|
|
3029
3121
|
/** Stream a Codex JSONL file and extract scan-level metadata (session ID, cwd, topic, tokens). */
|
|
@@ -3085,6 +3177,8 @@ export function serializeCodexParserState(state, offset) {
|
|
|
3085
3177
|
// be persisted.
|
|
3086
3178
|
ticketId: ticket?.id,
|
|
3087
3179
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
3180
|
+
checklistEvents: state.checklistEvents,
|
|
3181
|
+
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
3088
3182
|
};
|
|
3089
3183
|
}
|
|
3090
3184
|
/**
|
|
@@ -3118,6 +3212,8 @@ export function hydrateCodexParseState(prior) {
|
|
|
3118
3212
|
createdTickets: new Set(prior.createdTickets),
|
|
3119
3213
|
pendingTicketTools: new Set(prior.pendingTicketTools),
|
|
3120
3214
|
spawnedTeam: prior.spawnedTeam,
|
|
3215
|
+
checklistEvents: prior.checklistEvents ?? [],
|
|
3216
|
+
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
3121
3217
|
};
|
|
3122
3218
|
}
|
|
3123
3219
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve which machine a session transcript originated on.
|
|
3
|
+
*
|
|
4
|
+
* Cross-machine sync mirrors a remote transcript to
|
|
5
|
+
* `backups/<agent>/<machine>/<subdir>/…`. Every other transcript is a live-home
|
|
6
|
+
* file on this box, so the origin is the local machine id.
|
|
7
|
+
*
|
|
8
|
+
* Kept in a leaf module (no session/db imports) so both discovery and the
|
|
9
|
+
* sessions DB upsert path can stamp `machine` without circular deps.
|
|
10
|
+
*/
|
|
11
|
+
/** Local machine id, cached for the process lifetime. */
|
|
12
|
+
export declare function localMachineId(): string;
|
|
13
|
+
/**
|
|
14
|
+
* The machine a discovered session originated on.
|
|
15
|
+
* When the path sits under the agent's backups root, the first segment below it
|
|
16
|
+
* is the origin machine id; otherwise it's the local machine.
|
|
17
|
+
*/
|
|
18
|
+
export declare function machineForSessionFile(filePath: string, agent: string): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve which machine a session transcript originated on.
|
|
3
|
+
*
|
|
4
|
+
* Cross-machine sync mirrors a remote transcript to
|
|
5
|
+
* `backups/<agent>/<machine>/<subdir>/…`. Every other transcript is a live-home
|
|
6
|
+
* file on this box, so the origin is the local machine id.
|
|
7
|
+
*
|
|
8
|
+
* Kept in a leaf module (no session/db imports) so both discovery and the
|
|
9
|
+
* sessions DB upsert path can stamp `machine` without circular deps.
|
|
10
|
+
*/
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { getHistoryDir } from '../state.js';
|
|
13
|
+
import { machineId } from '../machine-id.js';
|
|
14
|
+
let _localMachineId;
|
|
15
|
+
/** Local machine id, cached for the process lifetime. */
|
|
16
|
+
export function localMachineId() {
|
|
17
|
+
return (_localMachineId ??= machineId());
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The machine a discovered session originated on.
|
|
21
|
+
* When the path sits under the agent's backups root, the first segment below it
|
|
22
|
+
* is the origin machine id; otherwise it's the local machine.
|
|
23
|
+
*/
|
|
24
|
+
export function machineForSessionFile(filePath, agent) {
|
|
25
|
+
if (!filePath)
|
|
26
|
+
return localMachineId();
|
|
27
|
+
const base = path.join(getHistoryDir(), 'backups', agent) + path.sep;
|
|
28
|
+
if (filePath.startsWith(base)) {
|
|
29
|
+
const seg = filePath.slice(base.length).split(path.sep)[0];
|
|
30
|
+
if (seg)
|
|
31
|
+
return seg;
|
|
32
|
+
}
|
|
33
|
+
return localMachineId();
|
|
34
|
+
}
|
|
@@ -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. */
|
|
@@ -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.';
|