@phnx-labs/agents-cli 1.20.76 → 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 +162 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +58 -16
- package/dist/commands/routines.js +271 -13
- package/dist/commands/secrets.js +59 -17
- package/dist/commands/sessions-browser.js +11 -1
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +168 -15
- package/dist/commands/sessions.d.ts +23 -4
- package/dist/commands/sessions.js +128 -38
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +39 -2
- package/dist/index.js +2 -1
- package/dist/lib/activity.d.ts +5 -4
- package/dist/lib/activity.js +450 -36
- package/dist/lib/cloud/session-index.js +2 -2
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +99 -12
- package/dist/lib/devices/fleet.d.ts +16 -1
- package/dist/lib/devices/fleet.js +10 -2
- 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/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 +31 -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/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 +150 -25
- 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 +9 -4
- package/dist/lib/secrets/agent.js +30 -20
- package/dist/lib/secrets/bundles.d.ts +20 -6
- package/dist/lib/secrets/bundles.js +70 -34
- package/dist/lib/secrets/index.d.ts +11 -2
- package/dist/lib/secrets/index.js +21 -9
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -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 +10 -0
- package/dist/lib/session/active.js +67 -4
- package/dist/lib/session/db.d.ts +6 -0
- package/dist/lib/session/db.js +83 -6
- package/dist/lib/session/discover.d.ts +13 -1
- package/dist/lib/session/discover.js +91 -3
- 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 +4 -0
- package/dist/lib/session/state.js +93 -11
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -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
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -40,6 +40,10 @@ export interface SessionRow {
|
|
|
40
40
|
worktree_slug: string | null;
|
|
41
41
|
ticket_id: string | null;
|
|
42
42
|
plan: string | null;
|
|
43
|
+
todos: string | null;
|
|
44
|
+
recent_directories_touched: string | null;
|
|
45
|
+
linear_project: string | null;
|
|
46
|
+
linear_project_url: string | null;
|
|
43
47
|
}
|
|
44
48
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
45
49
|
export interface ScanStamp {
|
|
@@ -215,6 +219,8 @@ export declare function isSessionActivityFresh(row: {
|
|
|
215
219
|
timestamp: string;
|
|
216
220
|
file_mtime_ms: number | null;
|
|
217
221
|
}, maxAgeMs: number, nowMs: number): boolean;
|
|
222
|
+
/** Persist a lazily resolved Linear project without reparsing the transcript. */
|
|
223
|
+
export declare function cacheLinearProject(sessionId: string, project: string, projectUrl: string): void;
|
|
218
224
|
/**
|
|
219
225
|
* Newest indexed session file for an agent working in `cwd`. Lets the live
|
|
220
226
|
* `--active` scanner locate a Codex transcript (whose files are date-partitioned,
|
package/dist/lib/session/db.js
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
|
+
import { parseSession } from './parse.js';
|
|
13
|
+
import { extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
12
14
|
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
13
15
|
const SESSIONS_DIR = getSessionsDir();
|
|
14
16
|
const DB_PATH = getSessionsDbPath();
|
|
15
17
|
/** Current schema version; bumped when migrations are added. */
|
|
16
|
-
const SCHEMA_VERSION =
|
|
18
|
+
const SCHEMA_VERSION = 17;
|
|
17
19
|
/**
|
|
18
20
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
19
21
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -69,7 +71,11 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
69
71
|
pr_number INTEGER,
|
|
70
72
|
worktree_slug TEXT,
|
|
71
73
|
ticket_id TEXT,
|
|
72
|
-
plan TEXT
|
|
74
|
+
plan TEXT,
|
|
75
|
+
todos TEXT,
|
|
76
|
+
recent_directories_touched TEXT,
|
|
77
|
+
linear_project TEXT,
|
|
78
|
+
linear_project_url TEXT
|
|
73
79
|
);
|
|
74
80
|
CREATE INDEX IF NOT EXISTS idx_sessions_timestamp ON sessions(timestamp DESC);
|
|
75
81
|
CREATE INDEX IF NOT EXISTS idx_sessions_cwd ON sessions(cwd);
|
|
@@ -316,6 +322,18 @@ function migrateSchema(db, fromVersion) {
|
|
|
316
322
|
// primary key — so every corrupt row becomes addressable. No rescan needed.
|
|
317
323
|
db.exec(`UPDATE sessions SET short_id = substr(id, 1, 8) WHERE short_id IS NULL OR short_id = ''`);
|
|
318
324
|
}
|
|
325
|
+
if (fromVersion < 17) {
|
|
326
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
327
|
+
if (!cols.some(c => c.name === 'todos'))
|
|
328
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN todos TEXT`);
|
|
329
|
+
if (!cols.some(c => c.name === 'recent_directories_touched'))
|
|
330
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN recent_directories_touched TEXT`);
|
|
331
|
+
if (!cols.some(c => c.name === 'linear_project'))
|
|
332
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN linear_project TEXT`);
|
|
333
|
+
if (!cols.some(c => c.name === 'linear_project_url'))
|
|
334
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN linear_project_url TEXT`);
|
|
335
|
+
db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
|
|
336
|
+
}
|
|
319
337
|
}
|
|
320
338
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
321
339
|
export function getDB() {
|
|
@@ -656,14 +674,16 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
656
674
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
657
675
|
output_tokens, cost_usd, duration_ms,
|
|
658
676
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
659
|
-
pr_url, pr_number, worktree_slug, ticket_id, plan
|
|
677
|
+
pr_url, pr_number, worktree_slug, ticket_id, plan, todos,
|
|
678
|
+
recent_directories_touched, linear_project, linear_project_url
|
|
660
679
|
) VALUES (
|
|
661
680
|
@id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
|
|
662
681
|
@version, @account, @timestamp, @last_activity,
|
|
663
682
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
664
683
|
@output_tokens, @cost_usd, @duration_ms,
|
|
665
684
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
666
|
-
@pr_url, @pr_number, @worktree_slug, @ticket_id, @plan
|
|
685
|
+
@pr_url, @pr_number, @worktree_slug, @ticket_id, @plan, @todos,
|
|
686
|
+
@recent_directories_touched, @linear_project, @linear_project_url
|
|
667
687
|
)
|
|
668
688
|
ON CONFLICT(id) DO UPDATE SET
|
|
669
689
|
short_id = excluded.short_id,
|
|
@@ -702,8 +722,34 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
702
722
|
pr_number = excluded.pr_number,
|
|
703
723
|
worktree_slug = excluded.worktree_slug,
|
|
704
724
|
ticket_id = excluded.ticket_id,
|
|
705
|
-
plan = excluded.plan
|
|
725
|
+
plan = excluded.plan,
|
|
726
|
+
todos = excluded.todos,
|
|
727
|
+
recent_directories_touched = excluded.recent_directories_touched,
|
|
728
|
+
linear_project = CASE
|
|
729
|
+
WHEN excluded.ticket_id IS NOT sessions.ticket_id THEN excluded.linear_project
|
|
730
|
+
ELSE COALESCE(excluded.linear_project, sessions.linear_project)
|
|
731
|
+
END,
|
|
732
|
+
linear_project_url = CASE
|
|
733
|
+
WHEN excluded.ticket_id IS NOT sessions.ticket_id THEN excluded.linear_project_url
|
|
734
|
+
ELSE COALESCE(excluded.linear_project_url, sessions.linear_project_url)
|
|
735
|
+
END
|
|
706
736
|
`);
|
|
737
|
+
function enrichCachedSessionMeta(meta) {
|
|
738
|
+
if (!meta.filePath)
|
|
739
|
+
return meta;
|
|
740
|
+
try {
|
|
741
|
+
const events = parseSession(meta.filePath, meta.agent);
|
|
742
|
+
return {
|
|
743
|
+
...meta,
|
|
744
|
+
todos: extractTodoProgressFromEvents(events),
|
|
745
|
+
recentDirectoriesTouched: extractRecentDirectoriesTouched(events, meta.cwd),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
// Synthetic/cloud rows can intentionally name a transcript that is not local.
|
|
750
|
+
return meta;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
707
753
|
const deleteTextStmt = (db) => db.prepare(`DELETE FROM session_text WHERE session_id = ?`);
|
|
708
754
|
const insertTextStmt = (db) => db.prepare(`INSERT INTO session_text (session_id, label, topic, project, content) VALUES (?, ?, ?, ?, ?)`);
|
|
709
755
|
// Read back the label the upsert actually stored (which may be the preserved
|
|
@@ -737,6 +783,7 @@ function storedFtsLabel(readLabel, id) {
|
|
|
737
783
|
* `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
|
|
738
784
|
*/
|
|
739
785
|
export function upsertSession(meta, content, scan) {
|
|
786
|
+
meta = enrichCachedSessionMeta(meta);
|
|
740
787
|
const db = getDB();
|
|
741
788
|
const { upsert, delText, insText, readLabel } = stmts(db);
|
|
742
789
|
const row = {
|
|
@@ -770,6 +817,10 @@ export function upsertSession(meta, content, scan) {
|
|
|
770
817
|
worktree_slug: meta.worktreeSlug ?? null,
|
|
771
818
|
ticket_id: meta.ticketId ?? null,
|
|
772
819
|
plan: meta.plan ?? null,
|
|
820
|
+
todos: meta.todos ? JSON.stringify(meta.todos) : null,
|
|
821
|
+
recent_directories_touched: meta.recentDirectoriesTouched ? JSON.stringify(meta.recentDirectoriesTouched) : null,
|
|
822
|
+
linear_project: meta.linearProject ?? null,
|
|
823
|
+
linear_project_url: meta.linearProjectUrl ?? null,
|
|
773
824
|
};
|
|
774
825
|
const txn = db.transaction(() => {
|
|
775
826
|
upsert.run(row);
|
|
@@ -812,6 +863,9 @@ export function upsertSessionsBatch(entries) {
|
|
|
812
863
|
const byPath = new Map(entries
|
|
813
864
|
.filter(e => e.scan && e.meta.filePath)
|
|
814
865
|
.map(e => [canonicalLedgerKey(e.meta.filePath), e]));
|
|
866
|
+
const enrichedEntries = entries.map(entry => entry.meta.agent === 'claude' || entry.meta.agent === 'codex'
|
|
867
|
+
? entry
|
|
868
|
+
: { ...entry, meta: enrichCachedSessionMeta(entry.meta) });
|
|
815
869
|
const txn = db.transaction((items) => {
|
|
816
870
|
// Re-read the ledger now that we hold the write lock. Any file committed
|
|
817
871
|
// by a concurrent process since our pre-scan is visible here.
|
|
@@ -873,6 +927,10 @@ export function upsertSessionsBatch(entries) {
|
|
|
873
927
|
worktree_slug: meta.worktreeSlug ?? null,
|
|
874
928
|
ticket_id: meta.ticketId ?? null,
|
|
875
929
|
plan: meta.plan ?? null,
|
|
930
|
+
todos: meta.todos ? JSON.stringify(meta.todos) : null,
|
|
931
|
+
recent_directories_touched: meta.recentDirectoriesTouched ? JSON.stringify(meta.recentDirectoriesTouched) : null,
|
|
932
|
+
linear_project: meta.linearProject ?? null,
|
|
933
|
+
linear_project_url: meta.linearProjectUrl ?? null,
|
|
876
934
|
});
|
|
877
935
|
delText.run(meta.id);
|
|
878
936
|
insText.run(meta.id,
|
|
@@ -890,7 +948,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
890
948
|
}
|
|
891
949
|
}
|
|
892
950
|
});
|
|
893
|
-
txn(
|
|
951
|
+
txn(enrichedEntries);
|
|
894
952
|
}
|
|
895
953
|
/**
|
|
896
954
|
* Sync labels for a set of sessions. For each id in the map, if the stored
|
|
@@ -1053,8 +1111,22 @@ function rowToMeta(row) {
|
|
|
1053
1111
|
worktreeSlug: row.worktree_slug ?? undefined,
|
|
1054
1112
|
ticketId: row.ticket_id ?? undefined,
|
|
1055
1113
|
plan: row.plan ?? undefined,
|
|
1114
|
+
todos: parseJsonColumn(row.todos),
|
|
1115
|
+
recentDirectoriesTouched: parseJsonColumn(row.recent_directories_touched),
|
|
1116
|
+
linearProject: row.linear_project ?? undefined,
|
|
1117
|
+
linearProjectUrl: row.linear_project_url ?? undefined,
|
|
1056
1118
|
};
|
|
1057
1119
|
}
|
|
1120
|
+
function parseJsonColumn(value) {
|
|
1121
|
+
if (!value)
|
|
1122
|
+
return undefined;
|
|
1123
|
+
try {
|
|
1124
|
+
return JSON.parse(value);
|
|
1125
|
+
}
|
|
1126
|
+
catch {
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1058
1130
|
/**
|
|
1059
1131
|
* The recency signal used to sort and label the listing: last-message time when
|
|
1060
1132
|
* a parser computed it (`meta.lastActivity` from `lastTsMs`), else the file's
|
|
@@ -1075,6 +1147,11 @@ export function isSessionActivityFresh(row, maxAgeMs, nowMs) {
|
|
|
1075
1147
|
const activityMs = Number.isFinite(parsedActivityMs) ? parsedActivityMs : row.file_mtime_ms ?? undefined;
|
|
1076
1148
|
return activityMs != null && nowMs - activityMs <= maxAgeMs;
|
|
1077
1149
|
}
|
|
1150
|
+
/** Persist a lazily resolved Linear project without reparsing the transcript. */
|
|
1151
|
+
export function cacheLinearProject(sessionId, project, projectUrl) {
|
|
1152
|
+
getDB().prepare(`UPDATE sessions SET linear_project = ?, linear_project_url = ? WHERE id = ?`)
|
|
1153
|
+
.run(project, projectUrl, sessionId);
|
|
1154
|
+
}
|
|
1078
1155
|
/**
|
|
1079
1156
|
* Newest indexed session file for an agent working in `cwd`. Lets the live
|
|
1080
1157
|
* `--active` scanner locate a Codex transcript (whose files are date-partitioned,
|
|
@@ -6,7 +6,7 @@
|
|
|
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
10
|
import { type ScanStamp } from './db.js';
|
|
11
11
|
/** Options controlling which sessions to discover and how to report progress. */
|
|
12
12
|
export interface DiscoverOptions {
|
|
@@ -82,6 +82,8 @@ interface ClaudeSessionScan {
|
|
|
82
82
|
spawnedTeam?: string;
|
|
83
83
|
/** Plan markdown from the last ExitPlanMode tool call (Claude sessions only). */
|
|
84
84
|
plan?: string;
|
|
85
|
+
todos?: TodoProgress;
|
|
86
|
+
recentDirectoriesTouched?: string[];
|
|
85
87
|
}
|
|
86
88
|
/** Lightweight metadata extracted from a Codex JSONL file during incremental scan. */
|
|
87
89
|
interface CodexSessionScan {
|
|
@@ -105,6 +107,8 @@ interface CodexSessionScan {
|
|
|
105
107
|
ticketId?: string;
|
|
106
108
|
createdTickets?: string[];
|
|
107
109
|
spawnedTeam?: string;
|
|
110
|
+
todos?: TodoProgress;
|
|
111
|
+
recentDirectoriesTouched?: string[];
|
|
108
112
|
}
|
|
109
113
|
/**
|
|
110
114
|
* Discover sessions. Scans only files whose (mtime, size) have changed since
|
|
@@ -369,6 +373,8 @@ export interface ClaudeParseState {
|
|
|
369
373
|
pendingTicketTools: Set<string>;
|
|
370
374
|
spawnedTeam?: string;
|
|
371
375
|
plan?: string;
|
|
376
|
+
checklistEvents: SessionEvent[];
|
|
377
|
+
recentDirectoriesTouched: string[];
|
|
372
378
|
}
|
|
373
379
|
/** Zero-value accumulator for a fresh (from-byte-0) Claude parse. */
|
|
374
380
|
export declare function initClaudeParseState(): ClaudeParseState;
|
|
@@ -427,6 +433,8 @@ export interface ClaudeParserState {
|
|
|
427
433
|
spawnedTeam?: string;
|
|
428
434
|
ticketId?: string;
|
|
429
435
|
contentText?: string;
|
|
436
|
+
checklistEvents: SessionEvent[];
|
|
437
|
+
recentDirectoriesTouched: string[];
|
|
430
438
|
}
|
|
431
439
|
/**
|
|
432
440
|
* Snapshot a live {@link ClaudeParseState} into its serializable form at
|
|
@@ -500,6 +508,8 @@ export interface CodexParseState {
|
|
|
500
508
|
createdTickets: Set<string>;
|
|
501
509
|
pendingTicketTools: Set<string>;
|
|
502
510
|
spawnedTeam?: string;
|
|
511
|
+
checklistEvents: SessionEvent[];
|
|
512
|
+
recentDirectoriesTouched: string[];
|
|
503
513
|
}
|
|
504
514
|
/** Zero-value accumulator for a fresh (from-byte-0) Codex parse. */
|
|
505
515
|
export declare function initCodexParseState(): CodexParseState;
|
|
@@ -549,6 +559,8 @@ export interface CodexParserState {
|
|
|
549
559
|
spawnedTeam?: string;
|
|
550
560
|
ticketId?: string;
|
|
551
561
|
contentText?: string;
|
|
562
|
+
checklistEvents: SessionEvent[];
|
|
563
|
+
recentDirectoriesTouched: string[];
|
|
552
564
|
}
|
|
553
565
|
/**
|
|
554
566
|
* Snapshot a live {@link CodexParseState} into its serializable form at `offset`
|
|
@@ -24,12 +24,13 @@ 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
29
|
import { machineId } from './sync/config.js';
|
|
30
30
|
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';
|
|
31
|
+
import { getDB, getScanStampByPath, getScanStampsForPaths, getParserStatesForPaths, getDirLedgerForPaths, recordDirScans, recordScans, syncLabels, seedLabelsFromNames, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, cacheLinearProject, } from './db.js';
|
|
32
32
|
import { buildRunNameMap } from './run-names.js';
|
|
33
|
+
import { resolveLinearApiKey } from '../auto-dispatch-linear.js';
|
|
33
34
|
const HOME = os.homedir();
|
|
34
35
|
// Versions can live under either repo: the user repo (current canonical
|
|
35
36
|
// location, ~/.agents/.history/versions/) or the system repo (legacy / npm-shipped,
|
|
@@ -99,10 +100,49 @@ export async function discoverSessions(options) {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
const sessions = querySessions(buildQueryOptions(options, agents, { includeLimit: true }));
|
|
103
|
+
await resolveLinearProjects(sessions);
|
|
102
104
|
for (const s of sessions)
|
|
103
105
|
s.machine = machineForSessionFile(s.filePath, s.agent);
|
|
104
106
|
return scopeToManaged(sessions, agents, options);
|
|
105
107
|
}
|
|
108
|
+
const linearProjectCache = new Map();
|
|
109
|
+
async function resolveLinearProjects(sessions) {
|
|
110
|
+
const apiKey = resolveLinearApiKey();
|
|
111
|
+
if (!apiKey)
|
|
112
|
+
return;
|
|
113
|
+
await Promise.all(sessions.map(async (session) => {
|
|
114
|
+
if (!session.ticketId || session.linearProject)
|
|
115
|
+
return;
|
|
116
|
+
let project = linearProjectCache.get(session.ticketId);
|
|
117
|
+
if (project === undefined) {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch('https://api.linear.app/graphql', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
signal: AbortSignal.timeout(3_000),
|
|
122
|
+
headers: { Authorization: apiKey, 'Content-Type': 'application/json' },
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
query: `query($id:String!){ issue(id:$id){ project{ name url } } }`,
|
|
125
|
+
variables: { id: session.ticketId },
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok)
|
|
129
|
+
throw new Error(String(response.status));
|
|
130
|
+
const body = await response.json();
|
|
131
|
+
const node = body.data?.issue?.project;
|
|
132
|
+
project = node?.name && node?.url ? { name: node.name, url: node.url } : null;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
project = null;
|
|
136
|
+
}
|
|
137
|
+
linearProjectCache.set(session.ticketId, project);
|
|
138
|
+
}
|
|
139
|
+
if (!project)
|
|
140
|
+
return;
|
|
141
|
+
session.linearProject = project.name;
|
|
142
|
+
session.linearProjectUrl = project.url;
|
|
143
|
+
cacheLinearProject(session.id, project.name, project.url);
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
106
146
|
/**
|
|
107
147
|
* Drop unmanaged rows for agents that HAVE managed versions.
|
|
108
148
|
*
|
|
@@ -968,6 +1008,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
968
1008
|
createdTickets: scan.createdTickets,
|
|
969
1009
|
spawnedTeam: scan.spawnedTeam,
|
|
970
1010
|
plan: scan.plan,
|
|
1011
|
+
todos: scan.todos,
|
|
1012
|
+
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
971
1013
|
};
|
|
972
1014
|
}
|
|
973
1015
|
else {
|
|
@@ -995,6 +1037,8 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, account,
|
|
|
995
1037
|
createdTickets: scan.createdTickets,
|
|
996
1038
|
spawnedTeam: scan.spawnedTeam,
|
|
997
1039
|
plan: scan.plan,
|
|
1040
|
+
todos: scan.todos,
|
|
1041
|
+
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
998
1042
|
};
|
|
999
1043
|
}
|
|
1000
1044
|
return {
|
|
@@ -1288,6 +1332,8 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1288
1332
|
ticketId: scan.ticketId,
|
|
1289
1333
|
createdTickets: scan.createdTickets,
|
|
1290
1334
|
spawnedTeam: scan.spawnedTeam,
|
|
1335
|
+
todos: scan.todos,
|
|
1336
|
+
recentDirectoriesTouched: scan.recentDirectoriesTouched,
|
|
1291
1337
|
};
|
|
1292
1338
|
return {
|
|
1293
1339
|
meta,
|
|
@@ -2370,8 +2416,27 @@ export function initClaudeParseState() {
|
|
|
2370
2416
|
pendingTicketTools: new Set(),
|
|
2371
2417
|
spawnedTeam: undefined,
|
|
2372
2418
|
plan: undefined,
|
|
2419
|
+
checklistEvents: [],
|
|
2420
|
+
recentDirectoriesTouched: [],
|
|
2373
2421
|
};
|
|
2374
2422
|
}
|
|
2423
|
+
const CHECKLIST_TOOLS = new Set(['TodoWrite', 'todo_write', 'update_plan', 'TaskCreate', 'TaskUpdate']);
|
|
2424
|
+
const DIRECTORY_TOOLS = new Set(['Edit', 'Write', 'edit_file', 'write_file', 'create_file', 'edit', 'write', 'Bash', 'exec_command', 'run_shell_command', 'shell', 'Execute']);
|
|
2425
|
+
function foldDerivedToolState(state, event) {
|
|
2426
|
+
if (CHECKLIST_TOOLS.has(event.tool ?? ''))
|
|
2427
|
+
state.checklistEvents.push(event);
|
|
2428
|
+
if (!DIRECTORY_TOOLS.has(event.tool ?? ''))
|
|
2429
|
+
return;
|
|
2430
|
+
const next = extractRecentDirectoriesTouched([event], state.cwd);
|
|
2431
|
+
for (const dir of next ?? []) {
|
|
2432
|
+
const old = state.recentDirectoriesTouched.indexOf(dir);
|
|
2433
|
+
if (old >= 0)
|
|
2434
|
+
state.recentDirectoriesTouched.splice(old, 1);
|
|
2435
|
+
state.recentDirectoriesTouched.push(dir);
|
|
2436
|
+
}
|
|
2437
|
+
if (state.recentDirectoriesTouched.length > 10)
|
|
2438
|
+
state.recentDirectoriesTouched.splice(0, state.recentDirectoriesTouched.length - 10);
|
|
2439
|
+
}
|
|
2375
2440
|
/**
|
|
2376
2441
|
* Fold one parsed transcript line into the accumulator. This is the exact loop
|
|
2377
2442
|
* body {@link scanClaudeSession} used to run inline — extracted verbatim,
|
|
@@ -2407,6 +2472,10 @@ export function applyClaudeLine(state, parsed) {
|
|
|
2407
2472
|
if (p)
|
|
2408
2473
|
state.plan = b.input.plan;
|
|
2409
2474
|
}
|
|
2475
|
+
foldDerivedToolState(state, {
|
|
2476
|
+
type: 'tool_use', agent: 'claude', timestamp: parsed.timestamp || '', tool: b?.name, args: b?.input || {},
|
|
2477
|
+
path: b?.input?.file_path || b?.input?.path, command: b?.input?.command,
|
|
2478
|
+
});
|
|
2410
2479
|
}
|
|
2411
2480
|
}
|
|
2412
2481
|
if (state.pendingTicketTools.size > 0 && parsed.type === 'user' && Array.isArray(parsed.message?.content)) {
|
|
@@ -2558,6 +2627,8 @@ export function finalizeClaudeScan(state) {
|
|
|
2558
2627
|
createdTickets: state.createdTickets.size > 0 ? [...state.createdTickets] : undefined,
|
|
2559
2628
|
spawnedTeam: state.spawnedTeam,
|
|
2560
2629
|
plan: state.plan,
|
|
2630
|
+
todos: extractTodoProgressFromEvents(state.checklistEvents),
|
|
2631
|
+
recentDirectoriesTouched: state.recentDirectoriesTouched.length ? state.recentDirectoriesTouched : undefined,
|
|
2561
2632
|
};
|
|
2562
2633
|
}
|
|
2563
2634
|
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
@@ -2632,6 +2703,8 @@ export function serializeClaudeParserState(state, offset) {
|
|
|
2632
2703
|
// not be persisted.
|
|
2633
2704
|
ticketId: ticket?.id,
|
|
2634
2705
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
2706
|
+
checklistEvents: state.checklistEvents,
|
|
2707
|
+
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
2635
2708
|
};
|
|
2636
2709
|
}
|
|
2637
2710
|
/**
|
|
@@ -2684,6 +2757,8 @@ export function hydrateClaudeParseState(prior) {
|
|
|
2684
2757
|
pendingTicketTools: new Set(prior.pendingTicketTools),
|
|
2685
2758
|
spawnedTeam: prior.spawnedTeam,
|
|
2686
2759
|
plan: prior.plan,
|
|
2760
|
+
checklistEvents: prior.checklistEvents ?? [],
|
|
2761
|
+
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
2687
2762
|
};
|
|
2688
2763
|
}
|
|
2689
2764
|
/**
|
|
@@ -2883,6 +2958,8 @@ export function initCodexParseState() {
|
|
|
2883
2958
|
createdTickets: new Set(),
|
|
2884
2959
|
pendingTicketTools: new Set(),
|
|
2885
2960
|
spawnedTeam: undefined,
|
|
2961
|
+
checklistEvents: [],
|
|
2962
|
+
recentDirectoriesTouched: [],
|
|
2886
2963
|
};
|
|
2887
2964
|
}
|
|
2888
2965
|
/**
|
|
@@ -2898,11 +2975,16 @@ export function applyCodexLine(state, parsed) {
|
|
|
2898
2975
|
const p = parsed.payload || {};
|
|
2899
2976
|
if (p.type === 'function_call') {
|
|
2900
2977
|
let cmd = '';
|
|
2978
|
+
let args = {};
|
|
2901
2979
|
try {
|
|
2902
|
-
|
|
2980
|
+
args = typeof p.arguments === 'string' ? JSON.parse(p.arguments) : (p.arguments || {});
|
|
2903
2981
|
cmd = String(args.command || args.cmd || '');
|
|
2904
2982
|
}
|
|
2905
2983
|
catch { /* non-JSON args */ }
|
|
2984
|
+
foldDerivedToolState(state, {
|
|
2985
|
+
type: 'tool_use', agent: 'codex', timestamp: parsed.timestamp || '', tool: p.name, args,
|
|
2986
|
+
path: args.file_path || args.path, command: cmd || undefined,
|
|
2987
|
+
});
|
|
2906
2988
|
if (!state.prUrl && !state.sawPrCreate && isPrCreateCommand(cmd))
|
|
2907
2989
|
state.sawPrCreate = true;
|
|
2908
2990
|
if (!state.spawnedTeam) {
|
|
@@ -3024,6 +3106,8 @@ export function finalizeCodexScan(state) {
|
|
|
3024
3106
|
ticketId: ticket?.id,
|
|
3025
3107
|
createdTickets: state.createdTickets.size > 0 ? [...state.createdTickets] : undefined,
|
|
3026
3108
|
spawnedTeam: state.spawnedTeam,
|
|
3109
|
+
todos: extractTodoProgressFromEvents(state.checklistEvents),
|
|
3110
|
+
recentDirectoriesTouched: state.recentDirectoriesTouched.length ? state.recentDirectoriesTouched : undefined,
|
|
3027
3111
|
};
|
|
3028
3112
|
}
|
|
3029
3113
|
/** Stream a Codex JSONL file and extract scan-level metadata (session ID, cwd, topic, tokens). */
|
|
@@ -3085,6 +3169,8 @@ export function serializeCodexParserState(state, offset) {
|
|
|
3085
3169
|
// be persisted.
|
|
3086
3170
|
ticketId: ticket?.id,
|
|
3087
3171
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
3172
|
+
checklistEvents: state.checklistEvents,
|
|
3173
|
+
recentDirectoriesTouched: state.recentDirectoriesTouched,
|
|
3088
3174
|
};
|
|
3089
3175
|
}
|
|
3090
3176
|
/**
|
|
@@ -3118,6 +3204,8 @@ export function hydrateCodexParseState(prior) {
|
|
|
3118
3204
|
createdTickets: new Set(prior.createdTickets),
|
|
3119
3205
|
pendingTicketTools: new Set(prior.pendingTicketTools),
|
|
3120
3206
|
spawnedTeam: prior.spawnedTeam,
|
|
3207
|
+
checklistEvents: prior.checklistEvents ?? [],
|
|
3208
|
+
recentDirectoriesTouched: prior.recentDirectoriesTouched ?? [],
|
|
3121
3209
|
};
|
|
3122
3210
|
}
|
|
3123
3211
|
/**
|
|
@@ -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.';
|
|
@@ -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"];
|
|
@@ -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.
|
|
@@ -528,7 +529,7 @@ export function renderSummary(events, cwd) {
|
|
|
528
529
|
// Commands with timestamps
|
|
529
530
|
const cmdList = [];
|
|
530
531
|
// Plan items
|
|
531
|
-
const todoItems = [];
|
|
532
|
+
const todoItems = extractTodoProgressFromEvents(events)?.items.map(item => item.content) ?? [];
|
|
532
533
|
let exitPlanContent = null;
|
|
533
534
|
let planFilePath = null;
|
|
534
535
|
// Subagent spawns
|
|
@@ -569,19 +570,6 @@ export function renderSummary(events, cwd) {
|
|
|
569
570
|
recentActivity.push({ kind: 'cmd', label: cmd, ts });
|
|
570
571
|
}
|
|
571
572
|
}
|
|
572
|
-
// Plan items: TodoWrite items + TaskCreate descriptions (project's task tracker)
|
|
573
|
-
if (tool === 'TodoWrite' && Array.isArray(args.todos)) {
|
|
574
|
-
for (const item of args.todos) {
|
|
575
|
-
const text = item.content || item.text || String(item);
|
|
576
|
-
if (text && !todoItems.includes(text))
|
|
577
|
-
todoItems.push(text);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
if (tool === 'TaskCreate' && (args.description || args.prompt)) {
|
|
581
|
-
const text = String(args.description || args.prompt || '').slice(0, 140);
|
|
582
|
-
if (text && !todoItems.includes(text))
|
|
583
|
-
todoItems.push(text);
|
|
584
|
-
}
|
|
585
573
|
if (tool === 'ExitPlanMode') {
|
|
586
574
|
exitPlanContent = args.result || args.plan || args.content || null;
|
|
587
575
|
}
|
|
@@ -796,6 +784,9 @@ export function parseRoleList(raw, flag) {
|
|
|
796
784
|
return parts;
|
|
797
785
|
}
|
|
798
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.
|
|
799
790
|
if (e.type === 'message' && e.role === 'user')
|
|
800
791
|
return 'user';
|
|
801
792
|
if (e.type === 'message' && e.role === 'assistant')
|
|
@@ -817,7 +808,14 @@ function applyRoleFilter(events, opts) {
|
|
|
817
808
|
const set = new Set(opts.include);
|
|
818
809
|
return events.filter(e => {
|
|
819
810
|
const role = roleOfEvent(e);
|
|
820
|
-
|
|
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);
|
|
821
819
|
});
|
|
822
820
|
}
|
|
823
821
|
if (opts.exclude && opts.exclude.length > 0) {
|
|
@@ -845,7 +843,11 @@ function applyTurnSlice(events, opts) {
|
|
|
845
843
|
if (!Number.isFinite(n) || n <= 0) {
|
|
846
844
|
throw new Error(`Turn count must be a positive integer, got ${n}`);
|
|
847
845
|
}
|
|
848
|
-
|
|
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;
|
|
849
851
|
const turnStartIdx = [];
|
|
850
852
|
for (let i = 0; i < events.length; i++)
|
|
851
853
|
if (isTurnStart(events[i]))
|
|
@@ -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. */
|