codiedev 0.9.80 → 0.10.0-beta.10

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.
Files changed (154) hide show
  1. package/README.md +12 -5
  2. package/dist/backup/cache.js +1 -5
  3. package/dist/backup/match.js +0 -6
  4. package/dist/backup/run.js +3 -35
  5. package/dist/captureExclusions.js +0 -12
  6. package/dist/captureLog.js +0 -28
  7. package/dist/captureReadiness.js +0 -58
  8. package/dist/captureSignals.js +0 -11
  9. package/dist/cli.js +11 -33
  10. package/dist/cliSession.js +3 -60
  11. package/dist/codexHookTrust.js +0 -28
  12. package/dist/codexLimitScan.js +0 -107
  13. package/dist/codie.js +64 -57
  14. package/dist/codieBrandInstall.js +0 -18
  15. package/dist/codieLauncher.js +1 -72
  16. package/dist/codieRuntime.js +27 -32
  17. package/dist/codieRuntimeInstall.js +11 -43
  18. package/dist/codieUsage.js +0 -49
  19. package/dist/commands/agentTemplates.generated.js +136 -0
  20. package/dist/commands/agents.js +1088 -0
  21. package/dist/commands/agentsSdk.generated.js +7 -0
  22. package/dist/commands/ask.js +0 -3
  23. package/dist/commands/capture.js +0 -2
  24. package/dist/commands/createTicket.js +0 -10
  25. package/dist/commands/doctor.js +2 -72
  26. package/dist/commands/evals.js +3 -9
  27. package/dist/commands/inbox.js +0 -2
  28. package/dist/commands/library.js +0 -3
  29. package/dist/commands/pack.js +0 -29
  30. package/dist/commands/ping.js +0 -2
  31. package/dist/commands/pull.js +0 -9
  32. package/dist/commands/push.js +0 -12
  33. package/dist/commands/search.js +0 -4
  34. package/dist/commands/send.js +0 -5
  35. package/dist/commands/share.js +0 -4
  36. package/dist/commands/shared.js +0 -35
  37. package/dist/commands/traces.js +0 -13
  38. package/dist/commands/tracesLabel.js +0 -8
  39. package/dist/configDir.js +1 -18
  40. package/dist/connect.js +3 -75
  41. package/dist/connectFlow.js +0 -15
  42. package/dist/copilotHooks.js +0 -67
  43. package/dist/copilotInstallation.js +0 -1
  44. package/dist/cursorHooks.js +1 -11
  45. package/dist/cursorLifecycle.js +1 -7
  46. package/dist/detection.js +0 -30
  47. package/dist/harnessDirs.js +0 -19
  48. package/dist/hook.js +43 -293
  49. package/dist/inboxFormat.js +0 -24
  50. package/dist/launchContext.js +0 -50
  51. package/dist/launcher.js +0 -33
  52. package/dist/limitEvent.js +2 -34
  53. package/dist/managedPi.js +2 -50
  54. package/dist/mcp.js +0 -48
  55. package/dist/nativeSteering.js +1 -4
  56. package/dist/packs/codie.js +0 -15
  57. package/dist/packs/harnesses.js +1 -38
  58. package/dist/packs/hookGate.js +0 -33
  59. package/dist/packs/install.js +0 -20
  60. package/dist/packs/manifest.js +1 -15
  61. package/dist/packs/paths.js +0 -8
  62. package/dist/packs/shim.js +0 -8
  63. package/dist/packs/store.js +0 -12
  64. package/dist/packs/sync.js +0 -20
  65. package/dist/piHooks.js +4 -45
  66. package/dist/piRuntime.js +0 -18
  67. package/dist/repoResolver.js +0 -19
  68. package/dist/secret.js +0 -3
  69. package/dist/selfUpdate.js +2 -77
  70. package/dist/skillMarker.js +0 -14
  71. package/dist/subagents.js +0 -18
  72. package/dist/teamSkillContext.js +0 -1
  73. package/dist/transcriptModels.js +0 -8
  74. package/dist/upgradeNudge.js +1 -30
  75. package/dist/utils.js +165 -768
  76. package/dist/version.js +0 -10
  77. package/dist/versionCheck.js +0 -33
  78. package/package.json +13 -8
  79. package/dist/backup/cache.d.ts +0 -14
  80. package/dist/backup/match.d.ts +0 -2
  81. package/dist/backup/run.d.ts +0 -25
  82. package/dist/captureExclusions.d.ts +0 -30
  83. package/dist/captureLog.d.ts +0 -20
  84. package/dist/captureReadiness.d.ts +0 -65
  85. package/dist/captureSignals.d.ts +0 -47
  86. package/dist/cli.d.ts +0 -15
  87. package/dist/cliSession.d.ts +0 -108
  88. package/dist/codexHookTrust.d.ts +0 -49
  89. package/dist/codexLimitScan.d.ts +0 -64
  90. package/dist/codie.d.ts +0 -2
  91. package/dist/codieBrandInstall.d.ts +0 -11
  92. package/dist/codieLauncher.d.ts +0 -87
  93. package/dist/codieRuntime.d.ts +0 -35
  94. package/dist/codieRuntimeInstall.d.ts +0 -56
  95. package/dist/codieUsage.d.ts +0 -64
  96. package/dist/commands/ask.d.ts +0 -1
  97. package/dist/commands/capture.d.ts +0 -14
  98. package/dist/commands/createTicket.d.ts +0 -27
  99. package/dist/commands/delete.d.ts +0 -1
  100. package/dist/commands/doctor.d.ts +0 -9
  101. package/dist/commands/evals.d.ts +0 -3
  102. package/dist/commands/inbox.d.ts +0 -3
  103. package/dist/commands/library.d.ts +0 -2
  104. package/dist/commands/note.d.ts +0 -1
  105. package/dist/commands/pack.d.ts +0 -54
  106. package/dist/commands/ping.d.ts +0 -1
  107. package/dist/commands/pull.d.ts +0 -2
  108. package/dist/commands/push.d.ts +0 -33
  109. package/dist/commands/search.d.ts +0 -2
  110. package/dist/commands/send.d.ts +0 -2
  111. package/dist/commands/share.d.ts +0 -2
  112. package/dist/commands/shared.d.ts +0 -64
  113. package/dist/commands/traces.d.ts +0 -72
  114. package/dist/commands/tracesLabel.d.ts +0 -24
  115. package/dist/configDir.d.ts +0 -10
  116. package/dist/connect.d.ts +0 -1
  117. package/dist/connectFlow.d.ts +0 -34
  118. package/dist/copilotHooks.d.ts +0 -85
  119. package/dist/copilotInstallation.d.ts +0 -10
  120. package/dist/cursorHooks.d.ts +0 -13
  121. package/dist/cursorLifecycle.d.ts +0 -2
  122. package/dist/detection.d.ts +0 -30
  123. package/dist/harnessDirs.d.ts +0 -15
  124. package/dist/hook.d.ts +0 -6
  125. package/dist/inboxFormat.d.ts +0 -38
  126. package/dist/launchContext.d.ts +0 -46
  127. package/dist/launcher.d.ts +0 -25
  128. package/dist/limitEvent.d.ts +0 -15
  129. package/dist/limitEventReporter.d.ts +0 -21
  130. package/dist/managedPi.d.ts +0 -48
  131. package/dist/mcp.d.ts +0 -12
  132. package/dist/nativeSteering.d.ts +0 -5
  133. package/dist/packs/codie.d.ts +0 -45
  134. package/dist/packs/harnesses.d.ts +0 -74
  135. package/dist/packs/hookGate.d.ts +0 -53
  136. package/dist/packs/install.d.ts +0 -46
  137. package/dist/packs/manifest.d.ts +0 -52
  138. package/dist/packs/paths.d.ts +0 -17
  139. package/dist/packs/shim.d.ts +0 -26
  140. package/dist/packs/store.d.ts +0 -42
  141. package/dist/packs/sync.d.ts +0 -40
  142. package/dist/piHooks.d.ts +0 -22
  143. package/dist/piRuntime.d.ts +0 -26
  144. package/dist/repoResolver.d.ts +0 -17
  145. package/dist/secret.d.ts +0 -1
  146. package/dist/selfUpdate.d.ts +0 -64
  147. package/dist/skillMarker.d.ts +0 -6
  148. package/dist/subagents.d.ts +0 -36
  149. package/dist/teamSkillContext.d.ts +0 -9
  150. package/dist/transcriptModels.d.ts +0 -20
  151. package/dist/upgradeNudge.d.ts +0 -24
  152. package/dist/utils.d.ts +0 -396
  153. package/dist/version.d.ts +0 -1
  154. package/dist/versionCheck.d.ts +0 -68
package/dist/hook.js CHANGED
@@ -51,9 +51,7 @@ const codexLimitScan_1 = require("./codexLimitScan");
51
51
  const launchContext_1 = require("./launchContext");
52
52
  const selfUpdate_1 = require("./selfUpdate");
53
53
  const version_1 = require("./version");
54
- // --- starter packs (SIG-113 phase 2): autosync, nudge ---
55
54
  const sync_1 = require("./packs/sync");
56
- // --- end starter packs ---
57
55
  const captureLog_1 = require("./captureLog");
58
56
  const subagents_1 = require("./subagents");
59
57
  const captureExclusions_1 = require("./captureExclusions");
@@ -62,15 +60,9 @@ const copilotHooks_1 = require("./copilotHooks");
62
60
  const piHooks_1 = require("./piHooks");
63
61
  const cliSession_1 = require("./cliSession");
64
62
  const utils_2 = require("./utils");
65
- // Pure builder for the discovery additionalContext. Returns null when there are
66
- // no matches. The suggested `codiedev pull` command carries the EXACT top key
67
- // (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
68
- // paraphrase the key (pr-push) and the extension-sensitive `pull` 404s.
69
63
  function buildDiscoveryContext(skills) {
70
64
  if (skills.length === 0)
71
65
  return null;
72
- // Don't trust the API's ordering — rank by score so the suggested pull key is
73
- // always the highest-confidence match.
74
66
  const top = [...skills].sort((a, b) => b.score - a.score).slice(0, 3);
75
67
  const lines = top
76
68
  .map((s) => `- ${s.key} — ${(s.score * 100).toFixed(0)}% match`)
@@ -82,9 +74,6 @@ function buildDiscoveryContext(skills) {
82
74
  "`. It encodes the team's vetted " +
83
75
  "approach. Skip it only if it clearly doesn't apply.");
84
76
  }
85
- // UserPromptSubmit: query skill discovery for the prompt and, if a team skill
86
- // clears the confidence threshold, inject it as additionalContext so the agent
87
- // can offer to pull+run it. Fully silent on miss/error — never blocks input.
88
77
  async function discoverContext(prompt) {
89
78
  if (!prompt || !prompt.trim())
90
79
  return null;
@@ -92,15 +81,11 @@ async function discoverContext(prompt) {
92
81
  if (!config)
93
82
  return null;
94
83
  try {
95
- // POST body, never a query param: raw prompts in GET query strings land
96
- // in server/proxy access logs (SIG-16). The server keeps accepting legacy
97
- // GETs from older CLIs during the transition.
98
84
  const res = await postJsonForResponse(`${config.backendUrl}/api/cli/skill-discovery`, { q: prompt }, config.token);
99
85
  const skills = res?.skills ?? [];
100
86
  return buildDiscoveryContext(skills);
101
87
  }
102
88
  catch {
103
- // silent — discovery is best-effort
104
89
  }
105
90
  return null;
106
91
  }
@@ -126,9 +111,6 @@ function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
126
111
  fs.closeSync(handle);
127
112
  }
128
113
  }
129
- // StopFailure (matcher rate_limit): the dev just got blocked. Read the local
130
- // transcript tail for the ground-truth rate_limit line and POST a tiny event.
131
- // Best-effort + silent — StopFailure output is ignored by Claude Code anyway.
132
114
  async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
133
115
  const config = (0, utils_1.readConfig)();
134
116
  if (!config)
@@ -140,7 +122,6 @@ async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
140
122
  transcript = readTranscriptTailFile(tp);
141
123
  }
142
124
  catch {
143
- /* unreadable transcript — fall through to minimal event */
144
125
  }
145
126
  }
146
127
  try {
@@ -153,23 +134,12 @@ async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
153
134
  });
154
135
  }
155
136
  catch {
156
- /* best-effort */
157
137
  }
158
138
  }
159
139
  async function runStopFailure(hookInput) {
160
140
  await readAndPostLimitEvent(hookInput, "claude-code", true);
161
141
  process.exit(0);
162
142
  }
163
- // Codex path. Two things differ from Claude Code:
164
- //
165
- // 1. There is no event that fires when a credit/rate-limit block kills a turn.
166
- // Codex skips its Stop hook for exactly that turn (measured: every normal
167
- // task_complete pairs with a Stop-hook run, the blocked one never does),
168
- // and its HookEventName enum has no StopFailure equivalent. So the block is
169
- // reported from the developer's NEXT prompt instead — see runCodexLimitSweep.
170
- // 2. Rollouts run to tens of megabytes and keep growing after the block, so a
171
- // fixed tail read loses the marker (in one real 44 MB rollout it sat 1.13 MB
172
- // from the end). Scan backwards for it instead.
173
143
  async function reportCodexLimitFromFile(config, transcriptPath, sessionId) {
174
144
  const hit = (0, codexLimitScan_1.scanCodexRolloutForLimit)(transcriptPath);
175
145
  if (!hit)
@@ -180,8 +150,6 @@ async function reportCodexLimitFromFile(config, transcriptPath, sessionId) {
180
150
  sessionId: sessionId ?? (0, codexLimitScan_1.sessionIdFromRolloutPath)(transcriptPath),
181
151
  transcriptTail: (0, codexLimitScan_1.readTailEndingAt)(transcriptPath, hit.lineEnd),
182
152
  });
183
- // Tell the prompt sweep this one is done, so it neither re-scans the
184
- // rollout nor re-posts an event the server would only deduplicate.
185
153
  (0, codexLimitScan_1.recordCodexLimitReported)(transcriptPath, hit.event.at);
186
154
  return true;
187
155
  }
@@ -212,10 +180,6 @@ async function runCodexLimit(hookInput) {
212
180
  process.exit(0);
213
181
  }
214
182
  const PROMPT_LIMIT_POST_TIMEOUT_MS = 2_500;
215
- // UserPromptSubmit: the first Codiedev hook that runs after a block, because
216
- // Codex runs no hook at the block itself. Scans the recent rollouts for any
217
- // block not yet reported. Cheap in the normal case — a per-file size cache
218
- // means an unchanged, unblocked rollout costs one statSync.
219
183
  async function runCodexLimitSweep(sessionId) {
220
184
  const config = (0, utils_1.readConfig)();
221
185
  if (!config)
@@ -223,12 +187,7 @@ async function runCodexLimitSweep(sessionId) {
223
187
  try {
224
188
  const posted = await (0, codexLimitScan_1.sweepCodexLimits)({
225
189
  sessionsDir: (0, codexLimitScan_1.codexSessionsDir)(),
226
- // A shorter timeout than the default: this is on the user's prompt
227
- // clock, and the next prompt retries anything that does not land.
228
190
  post: (payload) => (0, limitEventReporter_1.postLimitEvent)(config, payload, PROMPT_LIMIT_POST_TIMEOUT_MS),
229
- // Unchanged rollouts cost one statSync and do not spend the read
230
- // budget, so in steady state this is nearly free. The module default
231
- // covers a cold start; see DEFAULT_MAX_SCANS for why it is not small.
232
191
  findOptions: { withinMs: 24 * 3600_000 },
233
192
  scanOptions: { maxScanBytes: 4 * 1024 * 1024 },
234
193
  });
@@ -250,10 +209,6 @@ async function runCodexLimitSweep(sessionId) {
250
209
  });
251
210
  }
252
211
  }
253
- // Skip-reason ping: this session is ending WITHOUT a capture, and here's why.
254
- // Turns silent coverage loss into per-reason buckets on the admin Capture
255
- // Health panel (patches the session's aiSessionStarts row server-side). Tiny
256
- // like the sessionStart ping. Best-effort, never throws.
257
212
  async function postSessionSkip(config, sessionId, reason) {
258
213
  if (!sessionId)
259
214
  return;
@@ -261,13 +216,8 @@ async function postSessionSkip(config, sessionId, reason) {
261
216
  await postJson(`${config.backendUrl}/api/cli/sessionSkip`, { sessionId, reason, at: Date.now() }, config.token, PING_TIMEOUT_MS);
262
217
  }
263
218
  catch {
264
- /* best-effort */
265
219
  }
266
220
  }
267
- // SessionStart: the capture-coverage denominator. Record that a session opened
268
- // so the backend can later compute captured ÷ started. Deliberately tiny — no
269
- // transcript read, no git — so it never slows session startup. Best-effort +
270
- // silent (SessionStart hook output is ignored by Claude Code).
271
221
  async function runSessionStart(hookInput, source = "claude-code") {
272
222
  const config = (0, utils_1.readConfig)();
273
223
  if (!config)
@@ -281,18 +231,11 @@ async function runSessionStart(hookInput, source = "claude-code") {
281
231
  }, config.token, PING_TIMEOUT_MS);
282
232
  }
283
233
  catch {
284
- /* best-effort — never block session startup */
285
234
  }
286
235
  }
287
236
  return;
288
237
  }
289
- function postJson(url, body, bearerToken,
290
- // Socket timeout. Pass a SMALL value for tiny pings that run on (or near)
291
- // Claude Code's shutdown clock — a black-holed connection (proxy dropping
292
- // packets without RST) otherwise hangs the hook until CC kills it. Leave
293
- // unset for transcript uploads, which legitimately take a while on slow
294
- // corporate links and already run in detached children with retry.
295
- timeoutMs) {
238
+ function postJson(url, body, bearerToken, timeoutMs) {
296
239
  return new Promise((resolve, reject) => {
297
240
  const parsed = new URL(url);
298
241
  const data = JSON.stringify(body);
@@ -333,10 +276,7 @@ timeoutMs) {
333
276
  req.end();
334
277
  });
335
278
  }
336
- // postJson variant that returns the parsed response body — the upload-URL
337
- // handshake needs the { uploadUrl } the server hands back. No timeout: runs
338
- // only in detached uploader children, never on the shutdown clock.
339
- function postJsonForResponse(url, body, bearerToken) {
279
+ function postJsonForResponse(url, body, bearerToken, timeoutMs) {
340
280
  return new Promise((resolve, reject) => {
341
281
  const parsed = new URL(url);
342
282
  const data = JSON.stringify(body);
@@ -370,12 +310,15 @@ function postJsonForResponse(url, body, bearerToken) {
370
310
  });
371
311
  });
372
312
  req.on("error", reject);
313
+ if (timeoutMs) {
314
+ req.setTimeout(timeoutMs, () => {
315
+ req.destroy(new Error(`timeout after ${timeoutMs}ms`));
316
+ });
317
+ }
373
318
  req.write(data);
374
319
  req.end();
375
320
  });
376
321
  }
377
- // Raw-bytes POST to a Convex storage upload URL (no auth header — the URL
378
- // itself is the one-time credential). Returns the parsed { storageId }.
379
322
  function postBytes(url, bytes, contentType) {
380
323
  return new Promise((resolve, reject) => {
381
324
  const parsed = new URL(url);
@@ -411,9 +354,6 @@ function postBytes(url, bytes, contentType) {
411
354
  req.end();
412
355
  });
413
356
  }
414
- // Short leash for the tiny coverage pings (sessionStart / sessionSkip): they
415
- // carry a few bytes and must never stall a hook. 3s is generous for a ping
416
- // and a hard bound for the shutdown clock.
417
357
  const PING_TIMEOUT_MS = 3000;
418
358
  async function readStdin() {
419
359
  return new Promise((resolve, reject) => {
@@ -442,48 +382,24 @@ function resolveMode(arg) {
442
382
  return "claude-code";
443
383
  }
444
384
  async function main() {
445
- // Browser sign-in: refresh the access token under the shared lock before
446
- // anything reads config (cliSession.ts). A no-op for pasted API tokens and
447
- // for tokens with time left; never throws, never prints (hook stdout is
448
- // the agent's).
449
385
  await (0, cliSession_1.ensureFreshToken)({ configPath: utils_2.CONFIG_PATH }).catch(() => undefined);
450
- // Shared upload deps used by both "upload" and "upload-deferred" branches.
451
- // Extracted to keep both branches in lockstep — any new dep added here
452
- // automatically applies to the deferred path too.
453
386
  function makeUploadDeps() {
454
387
  return {
455
388
  post: postJson,
456
- // Direct-to-storage flow for big transcripts (see utils.ts
457
- // DIRECT_UPLOAD_THRESHOLD_CHARS): fetch a one-time upload URL, then
458
- // POST the raw bytes to it. Keeps ~7MB+ transcripts out of the
459
- // backend's 64MB captureSession isolate.
460
389
  requestJson: postJsonForResponse,
461
390
  uploadBytes: postBytes,
462
391
  readFileSync: fs.readFileSync,
463
392
  unlinkSync: fs.unlinkSync,
464
- // Persist a bumped attempt count on failure so a later sweep retries
465
- // (instead of dropping a session on a transient network blip).
466
393
  writeFileSync: fs.writeFileSync,
467
- // On a successful upload, drop the "last session captured" breadcrumb
468
- // that `codiedev doctor` reads to confirm the session landed.
469
394
  recordSuccess: (job) => (0, utils_1.writeLastCapture)((0, utils_1.buildLastCaptureRecord)(job, Date.now())),
470
- // Write structured outcome lines (ok / ERROR / gave-up) to the
471
- // rotating capture log so users and doctor can see why captures fail.
472
395
  log: (e) => (0, captureLog_1.appendCaptureLog)(e),
473
- // Write per-session upload state so the throttle can defer later
474
- // non-final uploads within the same window.
475
396
  writeSessionState: utils_1.writeSessionUploadState,
476
- // Re-read local config in the uploader so adding an exclusion after a
477
- // job was queued still prevents its transcript from leaving the device.
478
397
  shouldSkipJob: (job) => {
479
398
  const config = (0, utils_1.readConfig)();
480
399
  return (0, captureExclusions_1.shouldSkipCaptureUploadJob)(job, config);
481
400
  },
482
401
  };
483
402
  }
484
- // Detached uploader child (spawned by the capture path below). The job —
485
- // including transcript and token — is on disk; no stdin is needed, so handle
486
- // this before readStdin() to avoid blocking on the ignored stdin pipe.
487
403
  if (process.argv[2] === "upload") {
488
404
  const jobFile = process.argv[3];
489
405
  if (jobFile) {
@@ -491,10 +407,6 @@ async function main() {
491
407
  }
492
408
  process.exit(0);
493
409
  }
494
- // Deferred uploader child (spawned by runCaptureFinish when the per-turn
495
- // Stop capture is inside the throttle window). Sleeps for the remaining
496
- // delay, then claims the job file (if it hasn't been superseded by a newer
497
- // turn) and uploads. No stdin needed — handle before readStdin().
498
410
  if (process.argv[2] === "upload-deferred") {
499
411
  const jobFile = process.argv[3];
500
412
  const delayMs = Math.max(0, Number(process.argv[4]) || 0);
@@ -506,21 +418,11 @@ async function main() {
506
418
  }
507
419
  process.exit(0);
508
420
  }
509
- // Detached backup uploader child (spawned by runBackup below). The job file
510
- // carries only { root, files }; the child re-reads config from disk. No stdin
511
- // needed, so handle it before readStdin() like the capture uploader above.
512
421
  if (process.argv[2] === "backup-upload") {
513
422
  const { runBackupUpload } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
514
423
  await runBackupUpload(process.argv[3]);
515
424
  process.exit(0);
516
425
  }
517
- // Detached capture-finish child (spawned by the SessionEnd capture path
518
- // below). The job carries only the raw hook inputs; this child does ALL the
519
- // heavy work — read the transcript, parse it, resolve git branch/SHAs (the
520
- // per-SHA `git rev-parse` calls that were overrunning CC's shutdown window
521
- // on Windows), build the payload, and dispatch the upload. Running here means
522
- // none of it is on the SessionEnd shutdown clock, so it can never be
523
- // "Hook cancelled". No stdin needed — handle it before readStdin().
524
426
  if (process.argv[2] === "capture-finish") {
525
427
  const jobFile = process.argv[3];
526
428
  if (jobFile) {
@@ -529,31 +431,22 @@ async function main() {
529
431
  await runCaptureFinish(job);
530
432
  }
531
433
  catch {
532
- // best-effort — never throw out of the detached child
533
434
  }
534
435
  finally {
535
436
  try {
536
437
  fs.unlinkSync(jobFile);
537
438
  }
538
439
  catch {
539
- /* ignore */
540
440
  }
541
441
  }
542
442
  }
543
443
  process.exit(0);
544
444
  }
545
- // Detached daily self-upgrade of this install (see selfUpdate.ts).
546
445
  if (process.argv[2] === "self-update") {
547
446
  await (0, selfUpdate_1.runSelfUpdate)(process.argv[1]);
548
447
  process.exit(0);
549
448
  }
550
- // A hook reached directly (not via the stable launcher) means some harness
551
- // config predates the launcher: move them all onto it once. Steady state is
552
- // every call arriving via the launcher, which skips this entirely.
553
449
  const migrated = process.env.CODIEDEV_VIA_LAUNCHER ? [] : (0, utils_1.migrateHooksToLauncher)();
554
- // Every harness reaches this point, so one check covers all of them. Right
555
- // after a migration, wait for the next call: it arrives via the launcher,
556
- // which is what makes a pnpm install safe to upgrade.
557
450
  if (migrated.length === 0)
558
451
  (0, selfUpdate_1.maybeSpawnSelfUpdate)(process.argv[1]);
559
452
  (0, selfUpdate_1.maybeRefreshInstructions)();
@@ -572,22 +465,45 @@ async function main() {
572
465
  catch {
573
466
  process.exit(0);
574
467
  }
575
- // Local privacy boundary: every stdin-driven hook mode passes through this
576
- // check BEFORE prompt discovery, limit-event tails, SessionStart pings,
577
- // backup, self-heal, transcript reads, or detached dispatch. Ignored means
578
- // no Codiedev request at all for this session.
579
- if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
468
+ if (process.argv[2]?.startsWith("security-"))
580
469
  process.exit(0);
470
+ const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
471
+ const promptCommand = process.argv[2] ?? "";
472
+ if (process.argv[2] === "discover" ||
473
+ process.argv[2] === "discover-codex" ||
474
+ process.argv[2] === "discover-codie" ||
475
+ process.argv[2] === "discover-copilot" ||
476
+ process.argv[2] === "discover-pi") {
477
+ const source = promptCommand === "discover-codex"
478
+ ? "codex"
479
+ : promptCommand === "discover-codie"
480
+ ? "codie-harness"
481
+ : promptCommand === "discover-copilot"
482
+ ? "copilot"
483
+ : promptCommand === "discover-pi"
484
+ ? "pi"
485
+ : "claude-code";
486
+ if (captureIgnored)
487
+ return;
488
+ if (source === "codex")
489
+ await runCodexLimitSweep(hookInput.session_id);
490
+ if (source === "codex")
491
+ (0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
492
+ if (source !== "copilot")
493
+ await runDiscover(hookInput.prompt);
494
+ return;
581
495
  }
496
+ if (captureIgnored)
497
+ process.exit(0);
582
498
  if (["session-start-cursor", "backup-cursor", "capture-cursor", "observe-cursor"].includes(process.argv[2])) {
583
499
  try {
584
500
  (0, cursorLifecycle_1.recordCursorLifecycle)(hookInput, process.argv[2] === "capture-cursor");
585
501
  }
586
- catch { /* best-effort local evidence */ }
502
+ catch { }
587
503
  }
588
504
  if (process.argv[2] === "observe-cursor") {
589
505
  process.stdout.write("{}");
590
- return; // local evidence only
506
+ return;
591
507
  }
592
508
  if (process.argv[2] === "limit-copilot") {
593
509
  const config = (0, utils_1.readConfig)();
@@ -596,29 +512,10 @@ async function main() {
596
512
  try {
597
513
  await (0, limitEventReporter_1.postLimitEvent)(config, { ...event, source: "copilot", sessionId: hookInput.session_id });
598
514
  }
599
- catch { /* best-effort */ }
600
- }
601
- return;
602
- }
603
- // UserPromptSubmit discovery path — separate from session capture.
604
- if (process.argv[2] === "discover" ||
605
- process.argv[2] === "discover-codex" ||
606
- process.argv[2] === "discover-codie" ||
607
- process.argv[2] === "discover-copilot" ||
608
- process.argv[2] === "discover-pi") {
609
- // A Codex block leaves no hook of its own, so this prompt is the first
610
- // chance to report it. Runs before runDiscover, which exits the process.
611
- if (process.argv[2] === "discover-codex") {
612
- await runCodexLimitSweep(hookInput.session_id);
613
- // --- starter packs: Codex has no SessionStart hook; same hourly throttle ---
614
- (0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
615
- // --- end starter packs ---
515
+ catch { }
616
516
  }
617
- if (process.argv[2] !== "discover-copilot")
618
- await runDiscover(hookInput.prompt);
619
517
  return;
620
518
  }
621
- // StopFailure (matcher rate_limit): post the rate-limit event.
622
519
  if (process.argv[2] === "limit") {
623
520
  await runStopFailure(hookInput);
624
521
  return;
@@ -627,19 +524,15 @@ async function main() {
627
524
  await runCodexLimit(hookInput);
628
525
  return;
629
526
  }
630
- // SessionStart: post the capture-coverage denominator ping. Tiny + fast.
631
527
  if (process.argv[2] === "session-start" ||
632
528
  process.argv[2] === "session-start-cursor" ||
633
529
  process.argv[2] === "session-start-copilot" ||
634
530
  process.argv[2] === "session-start-opencode" ||
635
531
  process.argv[2] === "session-start-pi") {
636
- // --- starter packs: hourly detached `pack install --quiet` + daily nudge ---
637
532
  (0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
638
533
  const packNag = process.argv[2] === "session-start-opencode" || process.argv[2] === "session-start-pi" ? null : (0, sync_1.packNagLine)();
639
- // Claude Code adds SessionStart stdout to the session context.
640
534
  if (packNag && process.argv[2] === "session-start")
641
535
  process.stdout.write(`${packNag}\n`);
642
- // --- end starter packs ---
643
536
  await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
644
537
  ? "opencode"
645
538
  : process.argv[2] === "session-start-cursor"
@@ -659,11 +552,6 @@ async function main() {
659
552
  }
660
553
  return;
661
554
  }
662
- // Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
663
- // every codiedev hook — INCLUDING SessionEnd→capture, which previously had
664
- // no self-heal and could stay stripped forever while SessionStart kept
665
- // pinging (the Sonifi "starts arrive, captures never" loss) — re-wired on
666
- // their next Stop/SessionEnd. One read + one write, only when missing.
667
555
  if ((process.argv[2] ?? "").endsWith("-cursor")) {
668
556
  (0, utils_1.ensureCursorCodiedevHooksInstalled)();
669
557
  }
@@ -678,9 +566,9 @@ async function main() {
678
566
  }
679
567
  if (process.argv[2] === "capture-codex")
680
568
  (0, utils_1.ensureCodexLimitHookInstalled)();
569
+ if (/-opencode(?:-final)?$/.test(process.argv[2] ?? ""))
570
+ (0, utils_1.ensureOpenCodePluginCurrent)();
681
571
  const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
682
- // Read here, in the process the harness spawned: the detached capture child
683
- // is reparented once this exits and can no longer see who launched it.
684
572
  const launch = (0, launchContext_1.hookLaunch)(process.argv[2], hookInput, {
685
573
  copilotClientName: process.argv[2]?.endsWith("-copilot")
686
574
  ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path).clientName
@@ -689,14 +577,6 @@ async function main() {
689
577
  const workspaceRoot = workspace_roots && workspace_roots.length > 0
690
578
  ? workspace_roots[0]
691
579
  : cwd || process.cwd();
692
- // Stop/sessionEnd backup path — separate from session capture and needs no
693
- // transcript. Lists changed artifact files in the workspace and dispatches a
694
- // detached child that backs them up privately. Best-effort, never blocks.
695
- //
696
- // Per-turn capture: Stop fires reliably in editor-embedded clients (VS Code
697
- // extension) where SessionEnd may never come — the silent zero-capture bug.
698
- // Same detached capture-finish child as SessionEnd, marked non-final so the
699
- // server renews the session lease instead of summarizing immediately.
700
580
  if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor" || process.argv[2] === "backup-copilot" || process.argv[2] === "backup-pi") {
701
581
  if (transcript_path && !(0, utils_1.isCodexRolloutPath)(transcript_path)) {
702
582
  try {
@@ -713,54 +593,31 @@ async function main() {
713
593
  cwd,
714
594
  workspace_roots,
715
595
  ...(launch ? { launch } : {}),
716
- // non-final: server should renew the lease, not close it yet.
717
596
  final: false,
718
597
  }, process.argv[1], {
719
598
  spawn: child_process_1.spawn,
720
599
  writeFileSync: fs.writeFileSync,
721
600
  openSync: fs.openSync,
722
601
  closeSync: fs.closeSync,
723
- // UNIQUE per dispatch (sid+pid+ts): Stop and SessionEnd children
724
- // each own their input file exclusively — a shared per-session
725
- // name let one child's `finally` unlink eat the other's input
726
- // (lost final:true) or double-dispatch. See finishInputTmpFile.
727
602
  tmpFile: (0, utils_1.finishInputTmpFile)(session_id),
728
603
  });
729
604
  }
730
605
  catch {
731
- // capture is best-effort; never break backup
732
606
  }
733
607
  }
734
608
  const { runBackup } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
735
609
  runBackup(workspaceRoot, process.argv[1]);
736
610
  process.exit(0);
737
611
  }
738
- // Capture path (SessionEnd). Do the ABSOLUTE MINIMUM inline so this hook
739
- // exits 0 within Claude Code's SessionEnd shutdown window: a cheap config /
740
- // transcript guard, then hand ALL the heavy work (transcript read + parse +
741
- // per-SHA `git rev-parse` + upload) to a detached `capture-finish` child.
742
- // Detaching only the upload (the old #240 fix) left the synchronous parse and
743
- // git work on the shutdown clock, which still overran on Windows — where each
744
- // subprocess spawn costs ~50-150ms — producing "Hook cancelled" even though
745
- // the data still landed. JS can't be interrupted mid-sync-chain, so the work
746
- // finished and the upload went through, but CC had already given up waiting.
747
612
  const config = (0, utils_1.readConfig)();
748
613
  if (!config) {
749
- // No config = no token; nothing can be sent, not even a skip ping.
750
614
  process.exit(0);
751
615
  }
752
616
  if (!transcript_path) {
753
- // Nothing to capture — but say so, instead of leaving a start ping with
754
- // no matching capture (silent coverage loss). One tiny POST; the rare
755
- // no-transcript case has no other work on the shutdown clock.
756
617
  await postSessionSkip(config, session_id, "no-transcript-path");
757
618
  process.exit(0);
758
619
  }
759
620
  try {
760
- // "codiedev-finish-" prefix (NOT "codiedev-capture-") so the sweep regex
761
- // /^codiedev-capture-.*\.json$/ never mistakes a finish-input file for an
762
- // upload job. UNIQUE per dispatch (sid+pid+ts) so a concurrent Stop-hook
763
- // child's `finally` unlink can never eat this input (lost final:true).
764
621
  const jobFile = (0, utils_1.finishInputTmpFile)(session_id);
765
622
  (0, utils_1.dispatchCaptureFinish)({
766
623
  mode: process.argv[2] ?? "capture",
@@ -769,9 +626,6 @@ async function main() {
769
626
  cwd,
770
627
  workspace_roots,
771
628
  ...(launch ? { launch } : {}),
772
- // final:true for real SessionEnd signals (Claude Code, Cursor, Copilot).
773
- // Codex routes its per-turn Stop hook through "capture-codex" —
774
- // final:true there would close the server lease every turn.
775
629
  final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
776
630
  }, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
777
631
  }
@@ -780,19 +634,10 @@ async function main() {
780
634
  }
781
635
  process.exit(0);
782
636
  }
783
- // Runs inside the detached `capture-finish` child spawned by the SessionEnd
784
- // hook above. This is everything that used to run inline in main(): read the
785
- // transcript, resolve the repo, parse stats/usage/steering, resolve the git
786
- // branch + authored SHAs (the per-SHA `git rev-parse` calls that overran the
787
- // shutdown window on Windows), build the payload, and fire the upload from a
788
- // further-detached uploader. Running here means none of it is on the SessionEnd
789
- // shutdown clock, so it can never be "Hook cancelled". Best-effort throughout.
790
637
  async function runCaptureFinish(input) {
791
638
  const config = (0, utils_1.readConfig)();
792
639
  if (!config)
793
640
  return;
794
- // Re-check in the detached child: the user may have added an exclusion
795
- // after the parent hook dispatched this job but before it started.
796
641
  if ((0, captureExclusions_1.shouldIgnoreCaptureInput)(config, input)) {
797
642
  (0, captureLog_1.appendCaptureLog)({
798
643
  stage: "capture-finish",
@@ -807,8 +652,6 @@ async function runCaptureFinish(input) {
807
652
  await postSessionSkip(config, session_id, "no-transcript-path");
808
653
  return;
809
654
  }
810
- // Log that this child has started processing — the first diagnostic proof
811
- // that the detached capture-finish child actually ran (visible in doctor).
812
655
  (0, captureLog_1.appendCaptureLog)({ stage: "capture-finish", sessionId: session_id, outcome: "start" });
813
656
  try {
814
657
  await runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots);
@@ -824,10 +667,6 @@ async function runCaptureFinish(input) {
824
667
  }
825
668
  async function runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots) {
826
669
  const mode = resolveMode(input.mode);
827
- // Desktop transcripts (opencode SDK {info,parts}[] dumps) are byte-identical
828
- // to opencode's, so every parser below EXCEPT parseUsage treats desktop
829
- // exactly as opencode via this mapping. parseUsage is the one exception —
830
- // see the rawTranscriptContent note below.
831
670
  const parserMode = mode === "desktop" ? "opencode" : mode;
832
671
  const workspaceRoot = workspace_roots && workspace_roots.length > 0
833
672
  ? workspace_roots[0]
@@ -836,9 +675,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
836
675
  let transcriptContent;
837
676
  try {
838
677
  transcriptContent = fs.readFileSync(transcript_path, "utf8");
839
- // Copilot writes session.shutdown (the token totals) just AFTER sessionEnd
840
- // fires. This child is detached, so wait briefly for it on the final
841
- // capture. VS Code may never write one; then tokens stay unreported.
842
678
  if (mode === "copilot" && input.final === true && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent)) {
843
679
  for (let waited = 0; waited < COPILOT_SHUTDOWN_WAIT_MS && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent); waited += 250) {
844
680
  await new Promise((resolve) => setTimeout(resolve, 250));
@@ -847,9 +683,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
847
683
  }
848
684
  }
849
685
  catch (err) {
850
- // ENOENT → "no-transcript" (phantom desktop-app helper session, excluded
851
- // from coverage server-side); anything else → "transcript-unreadable:<code>"
852
- // (real transcript, read failed — a genuine, diagnosable loss).
853
686
  const reason = (0, utils_1.transcriptReadSkipReason)(err);
854
687
  (0, captureLog_1.appendCaptureLog)({
855
688
  stage: "capture-finish",
@@ -862,11 +695,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
862
695
  }
863
696
  return;
864
697
  }
865
- // Snapshot the RAW transcript before normalization below flattens it to
866
- // Claude Code's newline-JSONL shape — that flattening drops info.cost /
867
- // info.providerID, which desktop's parseUsage (below) needs and opencode's
868
- // doesn't. Every other mode leaves transcriptContent untouched here, so this
869
- // is a no-op for them.
870
698
  const rawTranscriptContent = transcriptContent;
871
699
  let copilotSubagents;
872
700
  const copilotWorkspace = mode === "copilot" ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path) : {};
@@ -891,39 +719,22 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
891
719
  try {
892
720
  await (0, limitEventReporter_1.reportTranscriptLimitEvent)({ config, source: mode, sessionId: session_id, transcript: transcriptContent });
893
721
  }
894
- catch { /* capture proceeds if limit reporting fails */ }
722
+ catch { }
895
723
  }
896
- // Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
897
- // need the git remote of the workspace.
898
724
  let remoteUrl = null;
899
725
  if (parserMode === "codex") {
900
726
  remoteUrl = (0, utils_1.extractCodexRepoUrl)(transcriptContent);
901
727
  }
902
- // The worktrees the session actually wrote to — resolved up front because
903
- // they both anchor the branch/sha join keys below AND back-fill the repo tag
904
- // when the launch dir is a multi-repo WORKSPACE root (a parent folder of
905
- // several cloned repos) that has no remote of its own.
906
728
  const captureDirs = (0, utils_1.resolveCaptureDirs)(transcriptContent, workspaceRoot);
907
729
  if (!remoteUrl) {
908
730
  remoteUrl = (0, utils_1.resolveCaptureRemoteUrl)(workspaceRoot, captureDirs);
909
731
  }
910
- // Capture-first: the transcript is the asset; the repo is just a tag. We do
911
- // NOT bail when `remoteUrl` is null — that previously dropped EVERY session
912
- // for users whose workspace couldn't resolve a remote (no git binary on lean
913
- // Alpine images, no `origin`, or a submodule whose parent .git isn't mounted),
914
- // with no error and a green `doctor`. Upload regardless; the backend accepts
915
- // an unmatched capture (repoId null) and surfaces it for triage.
916
- //
917
- // Best-effort local match — backend re-resolves from `remoteUrl` against the
918
- // company's current repo list, so a stale/empty local cache doesn't matter.
732
+ if (remoteUrl)
733
+ remoteUrl = (0, utils_1.stripRepoUrlCredentials)(remoteUrl);
919
734
  const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
920
735
  const stats = parserMode === "codex"
921
736
  ? (0, utils_1.parseCodexStats)(transcriptContent)
922
737
  : (0, utils_1.parseClaudeCodeStats)(transcriptContent);
923
- // Cursor can emit duplicate lifecycle notifications while one transcript
924
- // path is still byte-empty. Never use parsed message count here: Cursor's
925
- // private JSONL shape can evolve independently, and parser drift must not
926
- // silently discard a real non-empty session.
927
738
  const emptyReason = parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)
928
739
  ? "empty-cursor-transcript"
929
740
  : parserMode === "copilot" && (0, copilotHooks_1.isCopilotTranscriptEmpty)(rawTranscriptContent)
@@ -938,52 +749,23 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
938
749
  outcome: "skip",
939
750
  detail: emptyReason,
940
751
  });
941
- // A non-final stop snapshot is only a backup; reporting it as a skipped
942
- // session would close server-side coverage while the session is alive.
943
752
  if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
944
753
  await postSessionSkip(config, session_id, emptyReason);
945
754
  }
946
755
  return;
947
756
  }
948
757
  const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
949
- // Session→merge join keys: the branch + HEAD sha the work happened on. Matched
950
- // server-side against a merged MR/PR's source_branch / commits for AI attribution.
951
- // Capture from the worktree the agent actually worked in (the launch dir stays
952
- // on main/detached when work happens in a `git worktree`), not workspaceRoot.
953
- // One session can touch several worktrees (→ several PRs); capture EVERY
954
- // worktree's branch+sha so all the resulting PRs attribute. branch/headSha
955
- // remain the primary (most-recent) values for back-compat.
956
758
  const keys = (0, utils_1.collectCaptureKeys)(captureDirs);
957
759
  const gitRefs = (0, utils_1.collectCaptureGitRefs)(captureDirs);
958
- // Union the worktree-dir branches with the branches the agent actually
959
- // committed/pushed to (parsed from the transcript's git output). Like the
960
- // SHAs below, these are independent of end-of-session filesystem state, so
961
- // they rescue branch-join when the worktree was REMOVED before SessionEnd
962
- // (the dominant miss: dir-resolution falls back to the parent's branch, never
963
- // the `worktree-<name>` one the PR was opened from). Indexed server-side under
964
- // every branch in the array, so the worktree's PR attributes regardless.
965
760
  const authoredBranches = (0, utils_1.parseAuthoredBranches)(transcriptContent);
966
- // Copilot records the branch it started on in workspace.yaml.
967
761
  const recordedBranches = copilotWorkspace.branch ? [copilotWorkspace.branch] : [];
968
762
  const branches = [...new Set([...keys.branches, ...authoredBranches, ...recordedBranches])];
969
763
  const branch = keys.branch ?? branches[0] ?? null;
970
- // Union the worktree HEAD shas with the SHAs the agent actually committed/
971
- // pushed (parsed from the transcript's git output, resolved via the shared
972
- // object store). The authored shas are independent of end-of-session
973
- // filesystem state, so they rescue attribution when a worktree is stale/gone.
974
764
  const authoredShas = (0, utils_1.parseAuthoredShas)(transcriptContent, workspaceRoot);
975
765
  const headShas = [...new Set([...keys.headShas, ...authoredShas])];
976
766
  const headSha = keys.headSha ?? headShas[0] ?? null;
977
767
  const pullRequestUrls = (0, utils_1.parsePullRequestUrls)(transcriptContent);
978
- // Cost/velocity/steering signals parsed straight from the transcript we're
979
- // already holding — no extra hooks, no extra git/process calls. parseUsage
980
- // keeps the literal mode (it has its own desktop parser) and reads the RAW
981
- // pre-normalization content for desktop; parseSteering routes through
982
- // parserMode like the parsers above.
983
768
  const usage = (0, utils_1.parseUsage)(mode === "desktop" ? rawTranscriptContent : transcriptContent, mode);
984
- // Claude Code subagents: their transcripts sit beside this one and are part
985
- // of this session's work (skill runs, tool calls, tokens). Folded in here,
986
- // never uploaded as sessions of their own.
987
769
  const subagents = mode === "claude-code"
988
770
  ? (0, subagents_1.readSubagentTranscripts)(transcript_path, session_id)
989
771
  : { contents: [], count: 0, truncated: false };
@@ -1029,22 +811,15 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1029
811
  sessionStartedAt,
1030
812
  interruptCount: steering.interrupts,
1031
813
  rejectionCount: steering.rejections,
1032
- // Carry the final flag into the HTTP body — the server uses it to decide
1033
- // whether to close the session lease (true) or renew it (false/absent).
1034
814
  final: input.final,
1035
- // Desktop-only measured spend (undefined for every other source).
1036
815
  costUsd: usage.costUsd,
1037
816
  });
1038
- // Throttle decision: final captures go immediately; non-final (per-turn Stop)
1039
- // are deferred if we uploaded this session recently, to avoid per-turn churn.
1040
817
  const uploadState = (0, utils_1.readSessionUploadState)(session_id ?? "");
1041
818
  const decision = (0, utils_1.decideDispatch)({
1042
819
  final: input.final === true,
1043
820
  lastUploadAt: uploadState?.lastUploadAt ?? null,
1044
821
  now: Date.now(),
1045
822
  });
1046
- // Log capture-finish outcome with dispatch context so doctor can see whether
1047
- // the upload went immediately or was deferred (and by how much).
1048
823
  (0, captureLog_1.appendCaptureLog)({
1049
824
  stage: "capture-finish",
1050
825
  sessionId: session_id,
@@ -1053,11 +828,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1053
828
  ? `dispatch=deferred delayMs=${decision.delayMs} final=${input.final === true} repo=${remoteUrl ?? "-"} branch=${branch ?? "-"} msgs=${stats.messageCount}`
1054
829
  : `dispatch=immediate final=${input.final === true} repo=${remoteUrl ?? "-"} branch=${branch ?? "-"} msgs=${stats.messageCount}`,
1055
830
  });
1056
- // Fire the upload from a further-detached child. We're already off the
1057
- // shutdown clock here, but reusing dispatchCaptureDetached keeps the upload
1058
- // retry + last-capture breadcrumb logic (runCaptureUpload) intact verbatim.
1059
- // Per-session job file: one file per session; newer turns overwrite it,
1060
- // which is the "supersede" mechanism for the deferred path.
1061
831
  try {
1062
832
  const hookScriptPath = process.argv[1];
1063
833
  const tmpFile = (0, utils_1.captureUploadTmpFile)(session_id, decision.mode);
@@ -1068,20 +838,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1068
838
  body: payload,
1069
839
  scopePaths,
1070
840
  }, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync });
1071
- // Persist the immediate payload before removing the older scheduled
1072
- // snapshot. Every immediate dispatch supersedes a sleeping deferred job.
1073
841
  if (session_id)
1074
842
  (0, utils_1.cancelDeferredCaptureUpload)(session_id);
1075
- // Retry any earlier capture jobs that failed to upload (transient network
1076
- // blips), excluding the one we just dispatched. Best-effort, detached.
1077
- // Skip the sweep on deferred path — the deferred child will handle uploading.
1078
- //
1079
- // Orphan pings are COLLECTED and awaited below: this child process calls
1080
- // process.exit(0) as soon as runCaptureFinish resolves, so a
1081
- // fire-and-forget POST here would be killed mid-flight — after the
1082
- // orphan file was already unlinked, i.e. the loss would stay invisible
1083
- // forever. The await is off the shutdown clock (detached child) and
1084
- // bounded by PING_TIMEOUT_MS.
1085
843
  const orphanPings = [];
1086
844
  (0, utils_1.sweepPendingCaptures)({
1087
845
  dir: os.tmpdir(),
@@ -1093,9 +851,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1093
851
  exclude: tmpFile,
1094
852
  openSync: fs.openSync,
1095
853
  closeSync: fs.closeSync,
1096
- // Orphaned finish-inputs (capture-finish child died before writing an
1097
- // upload job): unrecoverable payload, but report the loss so it lands
1098
- // as a skip-reason instead of a silent coverage hole.
1099
854
  unlinkSync: fs.unlinkSync,
1100
855
  reportOrphan: (sid) => {
1101
856
  orphanPings.push(postSessionSkip(config, sid, "finish-orphaned"));
@@ -1104,9 +859,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1104
859
  await Promise.all(orphanPings);
1105
860
  }
1106
861
  else {
1107
- // Deferred: write the job file (newer turns overwrite = supersede), then
1108
- // spawn the deferred-upload child which will sleep, claim, and upload.
1109
- // Same stdio/fd-hygiene contract as dispatchCaptureDetached.
1110
862
  (0, utils_1.dispatchDeferredUpload)({
1111
863
  url: `${config.backendUrl}/api/captureSession`,
1112
864
  token: config.token,
@@ -1119,9 +871,7 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1119
871
  process.stderr.write(`[codiedev-hook] Warning: Failed to dispatch session capture — ${err.message}\n`);
1120
872
  }
1121
873
  }
1122
- // How long the final Copilot capture waits for session.shutdown.
1123
874
  const COPILOT_SHUTDOWN_WAIT_MS = 5000;
1124
- /** Copilot fleet sub-agents run inside the one events.jsonl. */
1125
875
  function withCopilotSubagents(signals, subagents) {
1126
876
  return subagents ? { ...signals, subagents } : signals;
1127
877
  }