codiedev 0.9.59 → 0.9.61

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.
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.recordCursorLifecycle = recordCursorLifecycle;
37
+ exports.cursorLifecycleRecords = cursorLifecycleRecords;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const crypto = __importStar(require("crypto"));
41
+ const configDir_1 = require("./configDir");
42
+ const limitEvent_1 = require("./limitEvent");
43
+ /** Retain aborted-turn evidence across non-final captures, which are snapshots. */
44
+ function fileFor(sessionId) {
45
+ const key = crypto.createHash("sha256").update(sessionId).digest("hex");
46
+ return path.join((0, configDir_1.codiedevStateDir)(), "cursor-lifecycle", `${key}.json`);
47
+ }
48
+ function recordCursorLifecycle(input, final) {
49
+ if (typeof input.session_id !== "string")
50
+ return;
51
+ const status = input.status ?? input.reason;
52
+ const file = fileFor(input.session_id);
53
+ let state = { ids: [] };
54
+ try {
55
+ state = JSON.parse(fs.readFileSync(file, "utf8"));
56
+ }
57
+ catch { /* first observation */ }
58
+ const model = typeof input.model_id === "string" && input.model_id.trim() ? input.model_id
59
+ : typeof input.model === "string" && input.model.trim() ? input.model : undefined;
60
+ if (model)
61
+ state.model = model;
62
+ const event = typeof input.hook_event_name === "string" ? input.hook_event_name : final ? "sessionEnd" : "stop";
63
+ // sessionEnd can arrive long after the agent stopped. Do not count that
64
+ // window-open time as active work. These are observed hook times, not
65
+ // invented per-message timestamps.
66
+ if (event !== "sessionEnd" || !state.observations?.length) {
67
+ const timestamp = new Date().toISOString();
68
+ state.observations = [...(state.observations ?? []), { event, timestamp, ...(model ? { model } : {}) }].slice(-1000);
69
+ }
70
+ const limit = event === "postToolUse" || event === "postToolUseFailure" ? null : (0, limitEvent_1.parseNativeLimitError)(input);
71
+ if (limit) {
72
+ const generationId = typeof input.generation_id === "string" ? input.generation_id : undefined;
73
+ const last = state.limits?.at(-1);
74
+ const retry = last && (generationId ? last.generationId === generationId
75
+ : final && state.lastStatus === "error" && last.reason === limit.reason && last.resetText === limit.resetText);
76
+ if (!retry)
77
+ state.limits = [...(state.limits ?? []), { ...limit, ...(generationId ? { generationId } : {}) }].slice(-1000);
78
+ }
79
+ if (event === "subagentStart" && typeof input.subagent_id === "string") {
80
+ state.subagents = [...new Set([...(state.subagents ?? []), input.subagent_id])].slice(-1000);
81
+ }
82
+ if (event === "postToolUseFailure" && input.failure_type === "permission_denied") {
83
+ const rejectionId = typeof input.tool_use_id === "string" ? input.tool_use_id : `${Date.now()}-${state.rejections?.length ?? 0}`;
84
+ state.rejections = [...new Set([...(state.rejections ?? []), rejectionId])].slice(-1000);
85
+ }
86
+ const aborted = status === "aborted" || input.is_interrupt === true;
87
+ // sessionEnd after an aborted stop is the same interruption. A repeated
88
+ // stop with the same generation_id is likewise a retry, not another turn.
89
+ const id = typeof input.generation_id === "string" ? input.generation_id
90
+ : `${Date.now()}-${state.ids.length}`;
91
+ if (aborted && !(final && state.lastStatus === "aborted") && !state.ids.includes(id))
92
+ state.ids.push(id);
93
+ if (typeof status === "string")
94
+ state.lastStatus = status;
95
+ state.ids = state.ids.slice(-1000);
96
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
97
+ fs.writeFileSync(file, JSON.stringify(state), { mode: 0o600 });
98
+ }
99
+ function cursorLifecycleRecords(sessionId) {
100
+ if (!sessionId)
101
+ return "";
102
+ try {
103
+ const state = JSON.parse(fs.readFileSync(fileFor(sessionId), "utf8"));
104
+ return [...(state.observations ?? []).map((observation) => JSON.stringify({ type: "system", subtype: "cursor_hook_observation", ...observation })),
105
+ ...(state.model ? [JSON.stringify({ type: "system", subtype: "cursor_model", model: state.model })] : []),
106
+ ...(state.rejections ?? []).map(id => JSON.stringify({ type: "agent.rejection", id })),
107
+ ...(state.subagents?.length ? [JSON.stringify({ type: "cursor.subagent_summary", count: state.subagents.length, truncated: state.subagents.length >= 1000 })] : []),
108
+ ...state.ids.map((id) => JSON.stringify({ type: "agent.interruption", id })),
109
+ ...(state.limits ?? []).map(({ generationId: _generationId, ...event }) => JSON.stringify({ type: "agent.limit", ...event })),
110
+ ].join("\n");
111
+ }
112
+ catch {
113
+ return "";
114
+ }
115
+ }
package/dist/hook.js CHANGED
@@ -44,11 +44,16 @@ const http = __importStar(require("http"));
44
44
  const child_process_1 = require("child_process");
45
45
  const utils_1 = require("./utils");
46
46
  const limitEventReporter_1 = require("./limitEventReporter");
47
+ const limitEvent_1 = require("./limitEvent");
48
+ const cursorLifecycle_1 = require("./cursorLifecycle");
47
49
  const codexLimitScan_1 = require("./codexLimitScan");
50
+ const launchContext_1 = require("./launchContext");
48
51
  const version_1 = require("./version");
49
52
  const captureLog_1 = require("./captureLog");
53
+ const subagents_1 = require("./subagents");
50
54
  const captureExclusions_1 = require("./captureExclusions");
51
55
  const cursorHooks_1 = require("./cursorHooks");
56
+ const copilotHooks_1 = require("./copilotHooks");
52
57
  // Pure builder for the discovery additionalContext. Returns null when there are
53
58
  // no matches. The suggested `codiedev pull` command carries the EXACT top key
54
59
  // (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
@@ -72,28 +77,31 @@ function buildDiscoveryContext(skills) {
72
77
  // UserPromptSubmit: query skill discovery for the prompt and, if a team skill
73
78
  // clears the confidence threshold, inject it as additionalContext so the agent
74
79
  // can offer to pull+run it. Fully silent on miss/error — never blocks input.
75
- async function runDiscover(prompt) {
80
+ async function discoverContext(prompt) {
76
81
  if (!prompt || !prompt.trim())
77
- process.exit(0);
82
+ return null;
78
83
  const config = (0, utils_1.readConfig)();
79
84
  if (!config)
80
- process.exit(0);
85
+ return null;
81
86
  try {
82
87
  // POST body, never a query param: raw prompts in GET query strings land
83
88
  // in server/proxy access logs (SIG-16). The server keeps accepting legacy
84
89
  // GETs from older CLIs during the transition.
85
90
  const res = await postJsonForResponse(`${config.backendUrl}/api/cli/skill-discovery`, { q: prompt }, config.token);
86
91
  const skills = res?.skills ?? [];
87
- const additionalContext = buildDiscoveryContext(skills);
88
- if (additionalContext) {
89
- process.stdout.write(JSON.stringify({
90
- hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
91
- }));
92
- }
92
+ return buildDiscoveryContext(skills);
93
93
  }
94
94
  catch {
95
95
  // silent — discovery is best-effort
96
96
  }
97
+ return null;
98
+ }
99
+ async function runDiscover(prompt) {
100
+ const additionalContext = await discoverContext(prompt);
101
+ if (additionalContext)
102
+ process.stdout.write(JSON.stringify({
103
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
104
+ }));
97
105
  process.exit(0);
98
106
  }
99
107
  function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
@@ -255,7 +263,7 @@ async function postSessionSkip(config, sessionId, reason) {
255
263
  async function runSessionStart(hookInput, source = "claude-code") {
256
264
  const config = (0, utils_1.readConfig)();
257
265
  if (!config)
258
- process.exit(0);
266
+ return;
259
267
  if (hookInput.session_id) {
260
268
  try {
261
269
  await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
@@ -268,7 +276,7 @@ async function runSessionStart(hookInput, source = "claude-code") {
268
276
  /* best-effort — never block session startup */
269
277
  }
270
278
  }
271
- process.exit(0);
279
+ return;
272
280
  }
273
281
  function postJson(url, body, bearerToken,
274
282
  // Socket timeout. Pass a SMALL value for tiny pings that run on (or near)
@@ -415,6 +423,8 @@ function resolveMode(arg) {
415
423
  return "codex";
416
424
  if (arg === "capture-cursor" || arg === "backup-cursor")
417
425
  return "cursor";
426
+ if (arg === "capture-copilot" || arg === "backup-copilot")
427
+ return "copilot";
418
428
  if (arg === "capture-opencode" || arg === "capture-opencode-final")
419
429
  return "opencode";
420
430
  if (arg === "capture-desktop" || arg === "capture-desktop-final")
@@ -524,7 +534,9 @@ async function main() {
524
534
  const parsed = JSON.parse(raw);
525
535
  hookInput = (process.argv[2]?.endsWith("-cursor")
526
536
  ? (0, cursorHooks_1.normalizeCursorHookInput)(parsed)
527
- : parsed);
537
+ : process.argv[2]?.endsWith("-copilot")
538
+ ? (0, copilotHooks_1.normalizeCopilotHookInput)(parsed)
539
+ : parsed);
528
540
  }
529
541
  }
530
542
  catch {
@@ -537,16 +549,39 @@ async function main() {
537
549
  if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
538
550
  process.exit(0);
539
551
  }
552
+ if (["session-start-cursor", "backup-cursor", "capture-cursor", "observe-cursor"].includes(process.argv[2])) {
553
+ try {
554
+ (0, cursorLifecycle_1.recordCursorLifecycle)(hookInput, process.argv[2] === "capture-cursor");
555
+ }
556
+ catch { /* best-effort local evidence */ }
557
+ }
558
+ if (process.argv[2] === "observe-cursor") {
559
+ process.stdout.write("{}");
560
+ return; // local evidence only
561
+ }
562
+ if (process.argv[2] === "limit-copilot") {
563
+ const config = (0, utils_1.readConfig)();
564
+ const event = (0, limitEvent_1.parseNativeLimitError)(hookInput);
565
+ if (config && event) {
566
+ try {
567
+ await (0, limitEventReporter_1.postLimitEvent)(config, { ...event, source: "copilot", sessionId: hookInput.session_id });
568
+ }
569
+ catch { /* best-effort */ }
570
+ }
571
+ return;
572
+ }
540
573
  // UserPromptSubmit discovery path — separate from session capture.
541
574
  if (process.argv[2] === "discover" ||
542
575
  process.argv[2] === "discover-codex" ||
543
- process.argv[2] === "discover-codie") {
576
+ process.argv[2] === "discover-codie" ||
577
+ process.argv[2] === "discover-copilot") {
544
578
  // A Codex block leaves no hook of its own, so this prompt is the first
545
579
  // chance to report it. Runs before runDiscover, which exits the process.
546
580
  if (process.argv[2] === "discover-codex") {
547
581
  await runCodexLimitSweep(hookInput.session_id);
548
582
  }
549
- await runDiscover(hookInput.prompt);
583
+ if (process.argv[2] !== "discover-copilot")
584
+ await runDiscover(hookInput.prompt);
550
585
  return;
551
586
  }
552
587
  // StopFailure (matcher rate_limit): post the rate-limit event.
@@ -561,12 +596,23 @@ async function main() {
561
596
  // SessionStart: post the capture-coverage denominator ping. Tiny + fast.
562
597
  if (process.argv[2] === "session-start" ||
563
598
  process.argv[2] === "session-start-cursor" ||
599
+ process.argv[2] === "session-start-copilot" ||
564
600
  process.argv[2] === "session-start-opencode") {
565
601
  await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
566
602
  ? "opencode"
567
603
  : process.argv[2] === "session-start-cursor"
568
604
  ? "cursor"
569
- : "claude-code");
605
+ : process.argv[2] === "session-start-copilot"
606
+ ? "copilot"
607
+ : "claude-code");
608
+ if ((0, utils_1.readConfig)() && (process.argv[2] === "session-start-cursor" || process.argv[2] === "session-start-copilot")) {
609
+ const source = process.argv[2] === "session-start-cursor" ? "cursor" : "copilot";
610
+ const { TEAM_SKILL_CONTEXT, teamSkillSessionOutput } = await Promise.resolve().then(() => __importStar(require("./teamSkillContext")));
611
+ const prompt = hookInput.initialPrompt
612
+ ?? hookInput.initial_prompt;
613
+ const discovered = typeof prompt === "string" ? await discoverContext(prompt) : null;
614
+ process.stdout.write(JSON.stringify(teamSkillSessionOutput(source, [TEAM_SKILL_CONTEXT, discovered].filter(Boolean).join("\n\n"))));
615
+ }
570
616
  return;
571
617
  }
572
618
  // Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
@@ -577,12 +623,22 @@ async function main() {
577
623
  if ((process.argv[2] ?? "").endsWith("-cursor")) {
578
624
  (0, utils_1.ensureCursorCodiedevHooksInstalled)();
579
625
  }
626
+ else if ((process.argv[2] ?? "").endsWith("-copilot")) {
627
+ (0, utils_1.ensureCopilotCodiedevHooksInstalled)();
628
+ }
580
629
  else {
581
630
  (0, utils_1.ensureAllCodiedevHooksInstalled)();
582
631
  }
583
632
  if (process.argv[2] === "capture-codex")
584
633
  (0, utils_1.ensureCodexLimitHookInstalled)();
585
634
  const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
635
+ // Read here, in the process the harness spawned: the detached capture child
636
+ // is reparented once this exits and can no longer see who launched it.
637
+ const launch = (0, launchContext_1.hookLaunch)(process.argv[2], hookInput, {
638
+ copilotClientName: process.argv[2]?.endsWith("-copilot")
639
+ ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path).clientName
640
+ : undefined,
641
+ });
586
642
  const workspaceRoot = workspace_roots && workspace_roots.length > 0
587
643
  ? workspace_roots[0]
588
644
  : cwd || process.cwd();
@@ -594,17 +650,20 @@ async function main() {
594
650
  // extension) where SessionEnd may never come — the silent zero-capture bug.
595
651
  // Same detached capture-finish child as SessionEnd, marked non-final so the
596
652
  // server renews the session lease instead of summarizing immediately.
597
- if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor") {
653
+ if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor" || process.argv[2] === "backup-copilot") {
598
654
  if (transcript_path) {
599
655
  try {
600
656
  (0, utils_1.dispatchCaptureFinish)({
601
- // "capture" = the claude-code entrypoint literal, same as the
602
- // SessionEnd path below (resolveMode maps it to claude-code).
603
- mode: process.argv[2] === "backup-cursor" ? "capture-cursor" : "capture",
657
+ mode: process.argv[2] === "backup-cursor"
658
+ ? "capture-cursor"
659
+ : process.argv[2] === "backup-copilot"
660
+ ? "capture-copilot"
661
+ : "capture",
604
662
  session_id,
605
663
  transcript_path,
606
664
  cwd,
607
665
  workspace_roots,
666
+ ...(launch ? { launch } : {}),
608
667
  // non-final: server should renew the lease, not close it yet.
609
668
  final: false,
610
669
  }, process.argv[1], {
@@ -660,10 +719,10 @@ async function main() {
660
719
  transcript_path,
661
720
  cwd,
662
721
  workspace_roots,
663
- // final:true ONLY for claude-code's SessionEnd. Codex routes its
664
- // per-turn Stop hook through "capture-codex" — final:true there would
665
- // close the server lease every turn. Cursor SessionEnd is final:true;
666
- // Codex remains the legacy final-omitted per-turn path.
722
+ ...(launch ? { launch } : {}),
723
+ // final:true for real SessionEnd signals (Claude Code, Cursor, Copilot).
724
+ // Codex routes its per-turn Stop hook through "capture-codex" —
725
+ // final:true there would close the server lease every turn.
667
726
  final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
668
727
  }, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
669
728
  }
@@ -728,6 +787,15 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
728
787
  let transcriptContent;
729
788
  try {
730
789
  transcriptContent = fs.readFileSync(transcript_path, "utf8");
790
+ // Copilot writes session.shutdown (the token totals) just AFTER sessionEnd
791
+ // fires. This child is detached, so wait briefly for it on the final
792
+ // capture. VS Code may never write one; then tokens stay unreported.
793
+ if (mode === "copilot" && input.final === true && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent)) {
794
+ for (let waited = 0; waited < COPILOT_SHUTDOWN_WAIT_MS && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent); waited += 250) {
795
+ await new Promise((resolve) => setTimeout(resolve, 250));
796
+ transcriptContent = fs.readFileSync(transcript_path, "utf8");
797
+ }
798
+ }
731
799
  }
732
800
  catch (err) {
733
801
  // ENOENT → "no-transcript" (phantom desktop-app helper session, excluded
@@ -751,11 +819,27 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
751
819
  // doesn't. Every other mode leaves transcriptContent untouched here, so this
752
820
  // is a no-op for them.
753
821
  const rawTranscriptContent = transcriptContent;
822
+ let copilotSubagents;
823
+ const copilotWorkspace = mode === "copilot" ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path) : {};
754
824
  if (parserMode === "opencode") {
755
825
  transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
756
826
  }
757
827
  else if (parserMode === "cursor") {
758
828
  transcriptContent = (0, cursorHooks_1.normalizeCursorTranscript)(transcriptContent);
829
+ const observations = (0, cursorLifecycle_1.cursorLifecycleRecords)(session_id);
830
+ if (observations)
831
+ transcriptContent += `\n${observations}`;
832
+ }
833
+ else if (parserMode === "copilot") {
834
+ const normalized = (0, copilotHooks_1.normalizeCopilotTranscript)(transcriptContent, { clientName: copilotWorkspace.clientName });
835
+ transcriptContent = normalized.content;
836
+ copilotSubagents = normalized.subagents.count > 0 ? normalized.subagents : undefined;
837
+ }
838
+ if (mode === "cursor" || mode === "copilot") {
839
+ try {
840
+ await (0, limitEventReporter_1.reportTranscriptLimitEvent)({ config, source: mode, sessionId: session_id, transcript: transcriptContent });
841
+ }
842
+ catch { /* capture proceeds if limit reporting fails */ }
759
843
  }
760
844
  // Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
761
845
  // need the git remote of the workspace.
@@ -784,19 +868,26 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
784
868
  const stats = parserMode === "codex"
785
869
  ? (0, utils_1.parseCodexStats)(transcriptContent)
786
870
  : (0, utils_1.parseClaudeCodeStats)(transcriptContent);
787
- // Never use parsed message count here: Cursor's private JSONL shape can
788
- // evolve independently, and parser drift must not discard a real session.
789
- if (parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)) {
871
+ // Cursor can emit duplicate lifecycle notifications while one transcript
872
+ // path is still byte-empty. Never use parsed message count here: Cursor's
873
+ // private JSONL shape can evolve independently, and parser drift must not
874
+ // silently discard a real non-empty session.
875
+ const emptyReason = parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)
876
+ ? "empty-cursor-transcript"
877
+ : parserMode === "copilot" && (0, copilotHooks_1.isCopilotTranscriptEmpty)(rawTranscriptContent)
878
+ ? "empty-copilot-transcript"
879
+ : null;
880
+ if (emptyReason) {
790
881
  (0, captureLog_1.appendCaptureLog)({
791
882
  stage: "capture-finish",
792
883
  sessionId: session_id,
793
884
  outcome: "skip",
794
- detail: "empty-cursor-transcript",
885
+ detail: emptyReason,
795
886
  });
796
887
  // A non-final stop snapshot is only a backup; reporting it as a skipped
797
888
  // session would close server-side coverage while the session is alive.
798
889
  if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
799
- await postSessionSkip(config, session_id, "empty-cursor-transcript");
890
+ await postSessionSkip(config, session_id, emptyReason);
800
891
  }
801
892
  return;
802
893
  }
@@ -818,7 +909,9 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
818
909
  // the `worktree-<name>` one the PR was opened from). Indexed server-side under
819
910
  // every branch in the array, so the worktree's PR attributes regardless.
820
911
  const authoredBranches = (0, utils_1.parseAuthoredBranches)(transcriptContent);
821
- const branches = [...new Set([...keys.branches, ...authoredBranches])];
912
+ // Copilot records the branch it started on in workspace.yaml.
913
+ const recordedBranches = copilotWorkspace.branch ? [copilotWorkspace.branch] : [];
914
+ const branches = [...new Set([...keys.branches, ...authoredBranches, ...recordedBranches])];
822
915
  const branch = keys.branch ?? branches[0] ?? null;
823
916
  // Union the worktree HEAD shas with the SHAs the agent actually committed/
824
917
  // pushed (parsed from the transcript's git output, resolved via the shared
@@ -834,6 +927,25 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
834
927
  // pre-normalization content for desktop; parseSteering routes through
835
928
  // parserMode like the parsers above.
836
929
  const usage = (0, utils_1.parseUsage)(mode === "desktop" ? rawTranscriptContent : transcriptContent, mode);
930
+ // Claude Code subagents: their transcripts sit beside this one and are part
931
+ // of this session's work (skill runs, tool calls, tokens). Folded in here,
932
+ // never uploaded as sessions of their own.
933
+ const subagents = mode === "claude-code"
934
+ ? (0, subagents_1.readSubagentTranscripts)(transcript_path, session_id)
935
+ : { contents: [], count: 0, truncated: false };
936
+ const subagentUsage = (0, subagents_1.foldSubagentModelUsage)([], subagents, mode);
937
+ for (const u of subagentUsage) {
938
+ usage.inputTokens = (usage.inputTokens ?? 0) + u.inputTokens;
939
+ usage.outputTokens = (usage.outputTokens ?? 0) + u.outputTokens;
940
+ usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + u.cacheReadTokens;
941
+ usage.cacheCreationTokens = (usage.cacheCreationTokens ?? 0) + u.cacheCreationTokens;
942
+ usage.cacheTokens = (usage.cacheTokens ?? 0) + u.cacheReadTokens + u.cacheCreationTokens;
943
+ }
944
+ const hasUsage = mode === "cursor" ? (0, cursorHooks_1.cursorTranscriptHasUsage)(transcriptContent)
945
+ : mode === "copilot" ? (0, transcriptModels_1.parseTranscriptModels)(transcriptContent, mode).some(model => model.tokensAvailable !== false)
946
+ : true;
947
+ if (mode === "cursor" && !usage.model)
948
+ usage.model = (0, cursorHooks_1.cursorObservedModel)(transcriptContent) ?? null;
837
949
  const sessionStartedAt = (0, utils_1.parseSessionStart)(transcriptContent);
838
950
  const steering = (0, utils_1.parseSteering)(transcriptContent, parserMode);
839
951
  const payload = (0, utils_1.buildCapturePayload)({
@@ -852,14 +964,14 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
852
964
  headShas,
853
965
  gitRefs,
854
966
  pullRequestUrls,
855
- inputTokens: usage.inputTokens,
856
- outputTokens: usage.outputTokens,
857
- cacheTokens: usage.cacheTokens,
858
- cacheReadTokens: usage.cacheReadTokens,
859
- cacheCreationTokens: usage.cacheCreationTokens,
967
+ inputTokens: hasUsage ? usage.inputTokens : undefined,
968
+ outputTokens: hasUsage ? usage.outputTokens : undefined,
969
+ cacheTokens: hasUsage ? usage.cacheTokens : undefined,
970
+ cacheReadTokens: hasUsage ? usage.cacheReadTokens : undefined,
971
+ cacheCreationTokens: hasUsage ? usage.cacheCreationTokens : undefined,
860
972
  model: usage.model,
861
- captureSignals: (0, captureSignals_1.parseCaptureSignals)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode),
862
- modelUsage: (0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode),
973
+ captureSignals: withCopilotSubagents((0, subagents_1.foldSubagentSignals)((0, captureSignals_1.withLaunch)((0, captureSignals_1.parseCaptureSignals)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode), input.launch), subagents, mode), copilotSubagents),
974
+ modelUsage: (0, subagents_1.foldSubagentModelUsage)((0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode), subagents, mode),
863
975
  sessionStartedAt,
864
976
  interruptCount: steering.interrupts,
865
977
  rejectionCount: steering.rejections,
@@ -953,6 +1065,12 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
953
1065
  process.stderr.write(`[codiedev-hook] Warning: Failed to dispatch session capture — ${err.message}\n`);
954
1066
  }
955
1067
  }
1068
+ // How long the final Copilot capture waits for session.shutdown.
1069
+ const COPILOT_SHUTDOWN_WAIT_MS = 5000;
1070
+ /** Copilot fleet sub-agents run inside the one events.jsonl. */
1071
+ function withCopilotSubagents(signals, subagents) {
1072
+ return subagents ? { ...signals, subagents } : signals;
1073
+ }
956
1074
  main().catch(() => {
957
1075
  process.exit(0);
958
1076
  });
@@ -0,0 +1,46 @@
1
+ export declare function codieLaunchFromArgv(argv: readonly string[]): string;
2
+ /** One launching process's command line → a Cursor launch value. */
3
+ export declare function cursorLaunchFromCommand(command: string): string | undefined;
4
+ /**
5
+ * One launching process's command line → a Copilot CLI launch value. The CLI
6
+ * runs as `…/@github/copilot-<platform>/copilot [flags]` under
7
+ * `node …/bin/copilot [flags]`; `-p`/`--prompt` is headless (scripted). VS
8
+ * Code's agent (`copilot-runtime --headless`) is named by workspace.yaml
9
+ * instead, so it never matches here.
10
+ */
11
+ export declare function copilotLaunchFromCommand(command: string): string | undefined;
12
+ type Ps = (pid: number) => {
13
+ ppid: number;
14
+ command: string;
15
+ } | null;
16
+ /**
17
+ * Windows: one PowerShell call returns the whole ancestor chain (spawning
18
+ * PowerShell per level would cost ~0.5s each). CommandLine needs CIM, which
19
+ * every supported Windows has; any failure means "unknown", never a guess.
20
+ */
21
+ export declare function windowsAncestors(startPid: number, depth: number): Array<{
22
+ pid: number;
23
+ ppid: number;
24
+ command: string;
25
+ }> | null;
26
+ type LaunchDeps = {
27
+ platform?: string;
28
+ ps?: Ps;
29
+ depth?: number;
30
+ windows?: typeof windowsAncestors;
31
+ };
32
+ /**
33
+ * Walk up to `depth` ancestors (a shell or `npx` usually sits between Cursor
34
+ * and the hook) and return the first Cursor launch found.
35
+ */
36
+ export declare function detectCursorLaunch(startPid?: number, deps?: LaunchDeps): string | undefined;
37
+ /** The Copilot CLI that launched this hook (`copilot:print` / `copilot:cli`). */
38
+ export declare function detectCopilotLaunch(startPid?: number, deps?: LaunchDeps): string | undefined;
39
+ /** The launch value a hook invocation should carry into its capture job. */
40
+ export declare function hookLaunch(hookCommand: string | undefined, input: {
41
+ launch?: unknown;
42
+ }, deps?: {
43
+ copilotClientName?: string;
44
+ detectCopilot?: () => string | undefined;
45
+ }): string | undefined;
46
+ export {};