codiedev 0.10.0-beta.4 → 0.10.0-beta.6

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 (157) hide show
  1. package/README.md +11 -5
  2. package/assets/tui-brand-plugin.tsx +14 -2
  3. package/dist/backup/cache.js +1 -5
  4. package/dist/backup/match.js +0 -6
  5. package/dist/backup/run.js +3 -35
  6. package/dist/captureExclusions.js +0 -12
  7. package/dist/captureLog.js +0 -28
  8. package/dist/captureReadiness.js +0 -58
  9. package/dist/captureSignals.js +68 -3
  10. package/dist/cli.js +75 -64
  11. package/dist/codexHookTrust.js +125 -6
  12. package/dist/codexLimitScan.js +2 -109
  13. package/dist/codie.js +50 -114
  14. package/dist/codieBrandInstall.js +0 -18
  15. package/dist/codieLauncher.js +20 -92
  16. package/dist/codieRuntime.js +19 -23
  17. package/dist/codieRuntimeInstall.js +108 -23
  18. package/dist/codieUsage.js +38 -41
  19. package/dist/commands/agentTemplates.generated.js +136 -0
  20. package/dist/commands/agents.js +1066 -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 +53 -120
  26. package/dist/commands/evals.js +187 -0
  27. package/dist/commands/inbox.js +0 -2
  28. package/dist/commands/library.js +0 -3
  29. package/dist/commands/pack.js +573 -0
  30. package/dist/commands/ping.js +0 -2
  31. package/dist/commands/pull.js +6 -10
  32. package/dist/commands/push.js +2 -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 +344 -0
  38. package/dist/commands/tracesLabel.js +348 -0
  39. package/dist/configDir.js +0 -10
  40. package/dist/connect.js +19 -116
  41. package/dist/connectFlow.js +0 -9
  42. package/dist/copilotHooks.js +391 -0
  43. package/dist/copilotInstallation.js +64 -0
  44. package/dist/cursorHooks.js +38 -30
  45. package/dist/cursorLifecycle.js +109 -0
  46. package/dist/detection.js +0 -30
  47. package/dist/harnessDirs.js +77 -0
  48. package/dist/hook.js +182 -469
  49. package/dist/inboxFormat.js +0 -24
  50. package/dist/launchContext.js +150 -0
  51. package/dist/launcher.js +140 -0
  52. package/dist/limitEvent.js +41 -35
  53. package/dist/limitEventReporter.js +1 -1
  54. package/dist/managedPi.js +240 -0
  55. package/dist/mcp.js +3 -384
  56. package/dist/nativeSteering.js +24 -0
  57. package/dist/packs/codie.js +182 -0
  58. package/dist/packs/harnesses.js +411 -0
  59. package/dist/packs/hookGate.js +256 -0
  60. package/dist/packs/install.js +286 -0
  61. package/dist/packs/manifest.js +126 -0
  62. package/dist/packs/paths.js +81 -0
  63. package/dist/packs/shim.js +214 -0
  64. package/dist/packs/store.js +155 -0
  65. package/dist/packs/sync.js +170 -0
  66. package/dist/piHooks.js +216 -0
  67. package/dist/piRuntime.js +120 -0
  68. package/dist/repoResolver.js +0 -19
  69. package/dist/secret.js +0 -3
  70. package/dist/selfUpdate.js +376 -0
  71. package/dist/skillMarker.js +23 -0
  72. package/dist/subagents.js +135 -0
  73. package/dist/teamSkillContext.js +10 -0
  74. package/dist/transcriptModels.js +6 -8
  75. package/dist/upgradeNudge.js +0 -30
  76. package/dist/utils.js +494 -824
  77. package/dist/version.js +0 -10
  78. package/dist/versionCheck.js +0 -33
  79. package/package.json +8 -9
  80. package/dist/backup/cache.d.ts +0 -14
  81. package/dist/backup/match.d.ts +0 -2
  82. package/dist/backup/run.d.ts +0 -25
  83. package/dist/captureExclusions.d.ts +0 -30
  84. package/dist/captureLog.d.ts +0 -20
  85. package/dist/captureReadiness.d.ts +0 -65
  86. package/dist/captureSignals.d.ts +0 -19
  87. package/dist/cli.d.ts +0 -16
  88. package/dist/codexHookTrust.d.ts +0 -34
  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 -86
  93. package/dist/codieRuntime.d.ts +0 -29
  94. package/dist/codieRuntimeInstall.d.ts +0 -28
  95. package/dist/codieStartup.d.ts +0 -26
  96. package/dist/codieStartup.js +0 -30
  97. package/dist/codieUsage.d.ts +0 -57
  98. package/dist/commands/ask.d.ts +0 -1
  99. package/dist/commands/capture.d.ts +0 -14
  100. package/dist/commands/createTicket.d.ts +0 -27
  101. package/dist/commands/delete.d.ts +0 -1
  102. package/dist/commands/doctor.d.ts +0 -1
  103. package/dist/commands/inbox.d.ts +0 -3
  104. package/dist/commands/library.d.ts +0 -2
  105. package/dist/commands/note.d.ts +0 -1
  106. package/dist/commands/ping.d.ts +0 -1
  107. package/dist/commands/promote.d.ts +0 -1
  108. package/dist/commands/promote.js +0 -53
  109. package/dist/commands/pull.d.ts +0 -2
  110. package/dist/commands/push.d.ts +0 -33
  111. package/dist/commands/react.d.ts +0 -2
  112. package/dist/commands/react.js +0 -40
  113. package/dist/commands/reverseTicket.d.ts +0 -2
  114. package/dist/commands/reverseTicket.js +0 -477
  115. package/dist/commands/search.d.ts +0 -2
  116. package/dist/commands/security.d.ts +0 -1
  117. package/dist/commands/security.js +0 -156
  118. package/dist/commands/send.d.ts +0 -2
  119. package/dist/commands/share.d.ts +0 -2
  120. package/dist/commands/shared.d.ts +0 -64
  121. package/dist/configDir.d.ts +0 -1
  122. package/dist/connect.d.ts +0 -1
  123. package/dist/connectFlow.d.ts +0 -13
  124. package/dist/cursorHooks.d.ts +0 -23
  125. package/dist/detection.d.ts +0 -30
  126. package/dist/hook.d.ts +0 -6
  127. package/dist/inboxFormat.d.ts +0 -38
  128. package/dist/limitEvent.d.ts +0 -16
  129. package/dist/limitEventReporter.d.ts +0 -21
  130. package/dist/mcp.d.ts +0 -12
  131. package/dist/prUrl.d.ts +0 -9
  132. package/dist/prUrl.js +0 -33
  133. package/dist/repoResolver.d.ts +0 -17
  134. package/dist/secret.d.ts +0 -1
  135. package/dist/security.d.ts +0 -102
  136. package/dist/security.js +0 -437
  137. package/dist/securityDeepScan.d.ts +0 -41
  138. package/dist/securityDeepScan.js +0 -248
  139. package/dist/securityDoctor.d.ts +0 -32
  140. package/dist/securityDoctor.js +0 -63
  141. package/dist/securityFeature.d.ts +0 -5
  142. package/dist/securityFeature.js +0 -7
  143. package/dist/securityInstall.d.ts +0 -63
  144. package/dist/securityInstall.js +0 -239
  145. package/dist/securityInventory.d.ts +0 -54
  146. package/dist/securityInventory.js +0 -556
  147. package/dist/securityPilot.d.ts +0 -19
  148. package/dist/securityPilot.js +0 -61
  149. package/dist/securityPolicy.d.ts +0 -33
  150. package/dist/securityPolicy.js +0 -188
  151. package/dist/securitySession.d.ts +0 -6
  152. package/dist/securitySession.js +0 -72
  153. package/dist/transcriptModels.d.ts +0 -18
  154. package/dist/upgradeNudge.d.ts +0 -25
  155. package/dist/utils.d.ts +0 -346
  156. package/dist/version.d.ts +0 -1
  157. package/dist/versionCheck.d.ts +0 -68
package/dist/hook.js CHANGED
@@ -39,30 +39,28 @@ const captureSignals_1 = require("./captureSignals");
39
39
  const transcriptModels_1 = require("./transcriptModels");
40
40
  const fs = __importStar(require("fs"));
41
41
  const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
42
43
  const https = __importStar(require("https"));
43
44
  const http = __importStar(require("http"));
44
45
  const child_process_1 = require("child_process");
45
46
  const utils_1 = require("./utils");
46
47
  const limitEventReporter_1 = require("./limitEventReporter");
48
+ const limitEvent_1 = require("./limitEvent");
49
+ const cursorLifecycle_1 = require("./cursorLifecycle");
47
50
  const codexLimitScan_1 = require("./codexLimitScan");
51
+ const launchContext_1 = require("./launchContext");
52
+ const selfUpdate_1 = require("./selfUpdate");
48
53
  const version_1 = require("./version");
54
+ const sync_1 = require("./packs/sync");
49
55
  const captureLog_1 = require("./captureLog");
50
- const security_1 = require("./security");
51
- const securitySession_1 = require("./securitySession");
52
- const securityPolicy_1 = require("./securityPolicy");
53
- const securityFeature_1 = require("./securityFeature");
54
- const securityInventory_1 = require("./securityInventory");
56
+ const subagents_1 = require("./subagents");
55
57
  const captureExclusions_1 = require("./captureExclusions");
56
58
  const cursorHooks_1 = require("./cursorHooks");
57
- // Pure builder for the discovery additionalContext. Returns null when there are
58
- // no matches. The suggested `codiedev pull` command carries the EXACT top key
59
- // (e.g. "pr-push.md") rather than a "<key>" placeholder — otherwise agents
60
- // paraphrase the key (pr-push) and the extension-sensitive `pull` 404s.
59
+ const copilotHooks_1 = require("./copilotHooks");
60
+ const piHooks_1 = require("./piHooks");
61
61
  function buildDiscoveryContext(skills) {
62
62
  if (skills.length === 0)
63
63
  return null;
64
- // Don't trust the API's ordering — rank by score so the suggested pull key is
65
- // always the highest-confidence match.
66
64
  const top = [...skills].sort((a, b) => b.score - a.score).slice(0, 3);
67
65
  const lines = top
68
66
  .map((s) => `- ${s.key} — ${(s.score * 100).toFixed(0)}% match`)
@@ -74,31 +72,27 @@ function buildDiscoveryContext(skills) {
74
72
  "`. It encodes the team's vetted " +
75
73
  "approach. Skip it only if it clearly doesn't apply.");
76
74
  }
77
- // UserPromptSubmit: query skill discovery for the prompt and, if a team skill
78
- // clears the confidence threshold, inject it as additionalContext so the agent
79
- // can offer to pull+run it. Fully silent on miss/error — never blocks input.
80
- async function runDiscover(prompt) {
75
+ async function discoverContext(prompt) {
81
76
  if (!prompt || !prompt.trim())
82
- process.exit(0);
77
+ return null;
83
78
  const config = (0, utils_1.readConfig)();
84
79
  if (!config)
85
- process.exit(0);
80
+ return null;
86
81
  try {
87
- // POST body, never a query param: raw prompts in GET query strings land
88
- // in server/proxy access logs (SIG-16). The server keeps accepting legacy
89
- // GETs from older CLIs during the transition.
90
82
  const res = await postJsonForResponse(`${config.backendUrl}/api/cli/skill-discovery`, { q: prompt }, config.token);
91
83
  const skills = res?.skills ?? [];
92
- const additionalContext = buildDiscoveryContext(skills);
93
- if (additionalContext) {
94
- process.stdout.write(JSON.stringify({
95
- hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
96
- }));
97
- }
84
+ return buildDiscoveryContext(skills);
98
85
  }
99
86
  catch {
100
- // silent — discovery is best-effort
101
87
  }
88
+ return null;
89
+ }
90
+ async function runDiscover(prompt) {
91
+ const additionalContext = await discoverContext(prompt);
92
+ if (additionalContext)
93
+ process.stdout.write(JSON.stringify({
94
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
95
+ }));
102
96
  process.exit(0);
103
97
  }
104
98
  function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
@@ -115,9 +109,6 @@ function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
115
109
  fs.closeSync(handle);
116
110
  }
117
111
  }
118
- // StopFailure (matcher rate_limit): the dev just got blocked. Read the local
119
- // transcript tail for the ground-truth rate_limit line and POST a tiny event.
120
- // Best-effort + silent — StopFailure output is ignored by Claude Code anyway.
121
112
  async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
122
113
  const config = (0, utils_1.readConfig)();
123
114
  if (!config)
@@ -126,12 +117,9 @@ async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
126
117
  const tp = hookInput.transcript_path;
127
118
  if (tp) {
128
119
  try {
129
- // Codex invokes Stop after every turn. Bound this read so long-running
130
- // sessions do not repeatedly load their entire rollout into memory.
131
120
  transcript = readTranscriptTailFile(tp);
132
121
  }
133
122
  catch {
134
- /* unreadable transcript — fall through to minimal event */
135
123
  }
136
124
  }
137
125
  try {
@@ -144,23 +132,12 @@ async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
144
132
  });
145
133
  }
146
134
  catch {
147
- /* best-effort */
148
135
  }
149
136
  }
150
137
  async function runStopFailure(hookInput) {
151
138
  await readAndPostLimitEvent(hookInput, "claude-code", true);
152
139
  process.exit(0);
153
140
  }
154
- // Codex path. Two things differ from Claude Code:
155
- //
156
- // 1. There is no event that fires when a credit/rate-limit block kills a turn.
157
- // Codex skips its Stop hook for exactly that turn (measured: every normal
158
- // task_complete pairs with a Stop-hook run, the blocked one never does),
159
- // and its HookEventName enum has no StopFailure equivalent. So the block is
160
- // reported from the developer's NEXT prompt instead — see runCodexLimitSweep.
161
- // 2. Rollouts run to tens of megabytes and keep growing after the block, so a
162
- // fixed tail read loses the marker (in one real 44 MB rollout it sat 1.13 MB
163
- // from the end). Scan backwards for it instead.
164
141
  async function reportCodexLimitFromFile(config, transcriptPath, sessionId) {
165
142
  const hit = (0, codexLimitScan_1.scanCodexRolloutForLimit)(transcriptPath);
166
143
  if (!hit)
@@ -171,8 +148,6 @@ async function reportCodexLimitFromFile(config, transcriptPath, sessionId) {
171
148
  sessionId: sessionId ?? (0, codexLimitScan_1.sessionIdFromRolloutPath)(transcriptPath),
172
149
  transcriptTail: (0, codexLimitScan_1.readTailEndingAt)(transcriptPath, hit.lineEnd),
173
150
  });
174
- // Tell the prompt sweep this one is done, so it neither re-scans the
175
- // rollout nor re-posts an event the server would only deduplicate.
176
151
  (0, codexLimitScan_1.recordCodexLimitReported)(transcriptPath, hit.event.at);
177
152
  return true;
178
153
  }
@@ -203,10 +178,6 @@ async function runCodexLimit(hookInput) {
203
178
  process.exit(0);
204
179
  }
205
180
  const PROMPT_LIMIT_POST_TIMEOUT_MS = 2_500;
206
- // UserPromptSubmit: the first Codiedev hook that runs after a block, because
207
- // Codex runs no hook at the block itself. Scans the recent rollouts for any
208
- // block not yet reported. Cheap in the normal case — a per-file size cache
209
- // means an unchanged, unblocked rollout costs one statSync.
210
181
  async function runCodexLimitSweep(sessionId) {
211
182
  const config = (0, utils_1.readConfig)();
212
183
  if (!config)
@@ -214,12 +185,7 @@ async function runCodexLimitSweep(sessionId) {
214
185
  try {
215
186
  const posted = await (0, codexLimitScan_1.sweepCodexLimits)({
216
187
  sessionsDir: (0, codexLimitScan_1.codexSessionsDir)(),
217
- // A shorter timeout than the default: this is on the user's prompt
218
- // clock, and the next prompt retries anything that does not land.
219
188
  post: (payload) => (0, limitEventReporter_1.postLimitEvent)(config, payload, PROMPT_LIMIT_POST_TIMEOUT_MS),
220
- // Unchanged rollouts cost one statSync and do not spend the read
221
- // budget, so in steady state this is nearly free. The module default
222
- // covers a cold start; see DEFAULT_MAX_SCANS for why it is not small.
223
189
  findOptions: { withinMs: 24 * 3600_000 },
224
190
  scanOptions: { maxScanBytes: 4 * 1024 * 1024 },
225
191
  });
@@ -241,10 +207,6 @@ async function runCodexLimitSweep(sessionId) {
241
207
  });
242
208
  }
243
209
  }
244
- // Skip-reason ping: this session is ending WITHOUT a capture, and here's why.
245
- // Turns silent coverage loss into per-reason buckets on the admin Capture
246
- // Health panel (patches the session's aiSessionStarts row server-side). Tiny
247
- // like the sessionStart ping. Best-effort, never throws.
248
210
  async function postSessionSkip(config, sessionId, reason) {
249
211
  if (!sessionId)
250
212
  return;
@@ -252,17 +214,12 @@ async function postSessionSkip(config, sessionId, reason) {
252
214
  await postJson(`${config.backendUrl}/api/cli/sessionSkip`, { sessionId, reason, at: Date.now() }, config.token, PING_TIMEOUT_MS);
253
215
  }
254
216
  catch {
255
- /* best-effort */
256
217
  }
257
218
  }
258
- // SessionStart: the capture-coverage denominator. Record that a session opened
259
- // so the backend can later compute captured ÷ started. Deliberately tiny — no
260
- // transcript read, no git — so it never slows session startup. Best-effort +
261
- // silent (SessionStart hook output is ignored by Claude Code).
262
219
  async function runSessionStart(hookInput, source = "claude-code") {
263
220
  const config = (0, utils_1.readConfig)();
264
221
  if (!config)
265
- process.exit(0);
222
+ return;
266
223
  if (hookInput.session_id) {
267
224
  try {
268
225
  await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
@@ -272,18 +229,11 @@ async function runSessionStart(hookInput, source = "claude-code") {
272
229
  }, config.token, PING_TIMEOUT_MS);
273
230
  }
274
231
  catch {
275
- /* best-effort — never block session startup */
276
232
  }
277
233
  }
278
- process.exit(0);
234
+ return;
279
235
  }
280
- function postJson(url, body, bearerToken,
281
- // Socket timeout. Pass a SMALL value for tiny pings that run on (or near)
282
- // Claude Code's shutdown clock — a black-holed connection (proxy dropping
283
- // packets without RST) otherwise hangs the hook until CC kills it. Leave
284
- // unset for transcript uploads, which legitimately take a while on slow
285
- // corporate links and already run in detached children with retry.
286
- timeoutMs) {
236
+ function postJson(url, body, bearerToken, timeoutMs) {
287
237
  return new Promise((resolve, reject) => {
288
238
  const parsed = new URL(url);
289
239
  const data = JSON.stringify(body);
@@ -324,9 +274,6 @@ timeoutMs) {
324
274
  req.end();
325
275
  });
326
276
  }
327
- // postJson variant that returns the parsed response body — the upload-URL
328
- // handshake needs the { uploadUrl } the server hands back. No timeout: runs
329
- // only in detached uploader children, never on the shutdown clock.
330
277
  function postJsonForResponse(url, body, bearerToken, timeoutMs) {
331
278
  return new Promise((resolve, reject) => {
332
279
  const parsed = new URL(url);
@@ -370,138 +317,6 @@ function postJsonForResponse(url, body, bearerToken, timeoutMs) {
370
317
  req.end();
371
318
  });
372
319
  }
373
- // Local policy runs synchronously before a tool executes. Workflow telemetry
374
- // is dispatched separately and fail-open, so a detector outage or timeout can
375
- // never delay or override the deterministic endpoint decision.
376
- async function runSecurityHook(hookInput, phase, source, uploadTelemetry = true) {
377
- const config = (0, utils_1.readConfig)();
378
- if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
379
- return;
380
- const policy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
381
- const sessionId = hookInput.session_id ?? hookInput.sessionId;
382
- const sessionSignal = phase === "pre" ? (0, securitySession_1.readSessionSecuritySignal)(sessionId) : undefined;
383
- const localDecision = (0, security_1.evaluateToolSecurity)(hookInput, phase, policy, source, sessionSignal);
384
- if (phase === "post" && (0, security_1.securityInputHasSensitivePath)(hookInput)) {
385
- (0, securitySession_1.recordSensitiveReadSignal)(sessionId);
386
- }
387
- const event = (0, security_1.buildSecurityWorkflowEvent)(hookInput, phase, source);
388
- if (event) {
389
- event.localDecision = localDecision.action;
390
- event.localCategory = localDecision.category;
391
- }
392
- if (localDecision.action === "block" && phase === "pre") {
393
- (0, captureLog_1.appendCaptureLog)({
394
- stage: "security",
395
- sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
396
- outcome: "ok",
397
- detail: `phase=${phase} localDecision=block category=${localDecision.category}`,
398
- });
399
- if (uploadTelemetry)
400
- dispatchSecurityEventUpload(event);
401
- const response = source === "cursor"
402
- ? (0, cursorHooks_1.cursorPreToolBlockResponse)(localDecision.reason ?? "Codie blocked this action")
403
- : {
404
- hookSpecificOutput: {
405
- hookEventName: "PreToolUse",
406
- permissionDecision: "deny",
407
- permissionDecisionReason: localDecision.reason,
408
- },
409
- };
410
- process.stdout.write(JSON.stringify(response));
411
- return;
412
- }
413
- if (!event)
414
- return;
415
- if (uploadTelemetry)
416
- dispatchSecurityEventUpload(event);
417
- }
418
- function runPromptSecurityHook(hookInput, source, uploadTelemetry = true) {
419
- const config = (0, utils_1.readConfig)();
420
- if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
421
- return false;
422
- const decision = (0, security_1.evaluatePromptSecurity)(hookInput.prompt, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId), source);
423
- if (decision.action !== "block")
424
- return false;
425
- (0, captureLog_1.appendCaptureLog)({
426
- stage: "security",
427
- sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
428
- outcome: "ok",
429
- detail: `phase=prompt localDecision=block category=${decision.category} telemetry=${uploadTelemetry ? "enabled" : "suppressed"} cwd=${hookInput.cwd ?? "unknown"}`,
430
- });
431
- if (uploadTelemetry) {
432
- dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
433
- }
434
- const response = source === "cursor"
435
- ? (0, cursorHooks_1.cursorPromptBlockResponse)(decision.reason ?? "Codie blocked this prompt")
436
- : { decision: "block", reason: decision.reason };
437
- process.stdout.write(JSON.stringify(response));
438
- return true;
439
- }
440
- function dispatchAllowedPromptSecurityEvent(hookInput, source) {
441
- const config = (0, utils_1.readConfig)();
442
- if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
443
- return;
444
- const event = (0, security_1.buildSecurityPromptEvent)(hookInput, source, Date.now, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId));
445
- if (event)
446
- dispatchSecurityEventUpload(event);
447
- }
448
- function dispatchSecurityEventUpload(event) {
449
- if (!event || !(0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()))
450
- return;
451
- try {
452
- const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-upload"], {
453
- detached: true,
454
- stdio: ["pipe", "ignore", "ignore"],
455
- });
456
- child.stdin?.end(JSON.stringify(event));
457
- child.unref();
458
- }
459
- catch {
460
- // Telemetry is best-effort and must never delay local enforcement.
461
- }
462
- }
463
- async function runSecurityEventUpload() {
464
- const config = (0, utils_1.readConfig)();
465
- if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
466
- return;
467
- const startedAt = Date.now();
468
- try {
469
- const raw = await readStdin();
470
- const event = JSON.parse(raw);
471
- const cachedPolicy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
472
- const eventWithEndpoint = (0, security_1.withEndpointIdentity)(event, {
473
- endpointId: config.deviceId,
474
- endpointName: config.deviceName,
475
- cliVersion: version_1.CLI_VERSION,
476
- securityRuntimeVersion: security_1.SECURITY_RUNTIME_VERSION,
477
- securityPolicyVersion: cachedPolicy?.policyVersion,
478
- });
479
- const result = await postJsonForResponse(`${config.backendUrl}/api/cli/securityEvent`, { ...eventWithEndpoint }, config.token,
480
- // Leave room for a cold backend and durable scheduling. This uploader is
481
- // detached, so the network budget never delays the developer's prompt or
482
- // tool call. Policy refreshes use the separately signed sync endpoint.
483
- 5_000);
484
- if (result?.policy?.schemaVersion === 1) {
485
- (0, securityPolicy_1.writeEndpointSecurityPolicy)(result.policy, undefined, undefined, config.companyId);
486
- }
487
- (0, captureLog_1.appendCaptureLog)({
488
- stage: "security",
489
- sessionId: event.sessionId,
490
- outcome: "ok",
491
- detail: `phase=${event.phase} elapsedMs=${Date.now() - startedAt} verdict=${String(result?.verdict ?? "unknown")}`,
492
- });
493
- }
494
- catch (err) {
495
- (0, captureLog_1.appendCaptureLog)({
496
- stage: "security",
497
- sessionId: "upload",
498
- outcome: "ERROR",
499
- detail: `elapsedMs=${Date.now() - startedAt} ${err.message}`,
500
- });
501
- }
502
- }
503
- // Raw-bytes POST to a Convex storage upload URL (no auth header — the URL
504
- // itself is the one-time credential). Returns the parsed { storageId }.
505
320
  function postBytes(url, bytes, contentType) {
506
321
  return new Promise((resolve, reject) => {
507
322
  const parsed = new URL(url);
@@ -537,9 +352,6 @@ function postBytes(url, bytes, contentType) {
537
352
  req.end();
538
353
  });
539
354
  }
540
- // Short leash for the tiny coverage pings (sessionStart / sessionSkip): they
541
- // carry a few bytes and must never stall a hook. 3s is generous for a ping
542
- // and a hard bound for the shutdown clock.
543
355
  const PING_TIMEOUT_MS = 3000;
544
356
  async function readStdin() {
545
357
  return new Promise((resolve, reject) => {
@@ -557,50 +369,34 @@ function resolveMode(arg) {
557
369
  return "codex";
558
370
  if (arg === "capture-cursor" || arg === "backup-cursor")
559
371
  return "cursor";
372
+ if (arg === "capture-copilot" || arg === "backup-copilot")
373
+ return "copilot";
560
374
  if (arg === "capture-opencode" || arg === "capture-opencode-final")
561
375
  return "opencode";
376
+ if (arg === "capture-pi" || arg === "capture-pi-final")
377
+ return "pi";
562
378
  if (arg === "capture-desktop" || arg === "capture-desktop-final")
563
379
  return "desktop";
564
380
  return "claude-code";
565
381
  }
566
382
  async function main() {
567
- // Shared upload deps used by both "upload" and "upload-deferred" branches.
568
- // Extracted to keep both branches in lockstep — any new dep added here
569
- // automatically applies to the deferred path too.
570
383
  function makeUploadDeps() {
571
384
  return {
572
385
  post: postJson,
573
- // Direct-to-storage flow for big transcripts (see utils.ts
574
- // DIRECT_UPLOAD_THRESHOLD_CHARS): fetch a one-time upload URL, then
575
- // POST the raw bytes to it. Keeps ~7MB+ transcripts out of the
576
- // backend's 64MB captureSession isolate.
577
386
  requestJson: postJsonForResponse,
578
387
  uploadBytes: postBytes,
579
388
  readFileSync: fs.readFileSync,
580
389
  unlinkSync: fs.unlinkSync,
581
- // Persist a bumped attempt count on failure so a later sweep retries
582
- // (instead of dropping a session on a transient network blip).
583
390
  writeFileSync: fs.writeFileSync,
584
- // On a successful upload, drop the "last session captured" breadcrumb
585
- // that `codiedev doctor` reads to confirm the session landed.
586
391
  recordSuccess: (job) => (0, utils_1.writeLastCapture)((0, utils_1.buildLastCaptureRecord)(job, Date.now())),
587
- // Write structured outcome lines (ok / ERROR / gave-up) to the
588
- // rotating capture log so users and doctor can see why captures fail.
589
392
  log: (e) => (0, captureLog_1.appendCaptureLog)(e),
590
- // Write per-session upload state so the throttle can defer later
591
- // non-final uploads within the same window.
592
393
  writeSessionState: utils_1.writeSessionUploadState,
593
- // Re-read local config in the uploader so adding an exclusion after a
594
- // job was queued still prevents its transcript from leaving the device.
595
394
  shouldSkipJob: (job) => {
596
395
  const config = (0, utils_1.readConfig)();
597
396
  return (0, captureExclusions_1.shouldSkipCaptureUploadJob)(job, config);
598
397
  },
599
398
  };
600
399
  }
601
- // Detached uploader child (spawned by the capture path below). The job —
602
- // including transcript and token — is on disk; no stdin is needed, so handle
603
- // this before readStdin() to avoid blocking on the ignored stdin pipe.
604
400
  if (process.argv[2] === "upload") {
605
401
  const jobFile = process.argv[3];
606
402
  if (jobFile) {
@@ -608,10 +404,6 @@ async function main() {
608
404
  }
609
405
  process.exit(0);
610
406
  }
611
- // Deferred uploader child (spawned by runCaptureFinish when the per-turn
612
- // Stop capture is inside the throttle window). Sleeps for the remaining
613
- // delay, then claims the job file (if it hasn't been superseded by a newer
614
- // turn) and uploads. No stdin needed — handle before readStdin().
615
407
  if (process.argv[2] === "upload-deferred") {
616
408
  const jobFile = process.argv[3];
617
409
  const delayMs = Math.max(0, Number(process.argv[4]) || 0);
@@ -623,35 +415,11 @@ async function main() {
623
415
  }
624
416
  process.exit(0);
625
417
  }
626
- // Daily component drift scan runs in a detached hook process. The parent
627
- // SessionStart path only performs a timestamp read, so agent startup never
628
- // waits for filesystem traversal or an inventory upload.
629
- if (process.argv[2] === "security-inventory-upload") {
630
- const config = (0, utils_1.readConfig)();
631
- if (config && (0, securityFeature_1.isAgentSecurityEnabled)(config)) {
632
- const inventory = (0, securityInventory_1.buildSecurityInventory)({ workspace: process.argv[3] || process.cwd() });
633
- try {
634
- await (0, securityInventory_1.uploadSecurityInventory)(inventory, config);
635
- }
636
- catch { /* retry after the daily inventory window */ }
637
- }
638
- process.exit(0);
639
- }
640
- // Detached backup uploader child (spawned by runBackup below). The job file
641
- // carries only { root, files }; the child re-reads config from disk. No stdin
642
- // needed, so handle it before readStdin() like the capture uploader above.
643
418
  if (process.argv[2] === "backup-upload") {
644
419
  const { runBackupUpload } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
645
420
  await runBackupUpload(process.argv[3]);
646
421
  process.exit(0);
647
422
  }
648
- // Detached capture-finish child (spawned by the SessionEnd capture path
649
- // below). The job carries only the raw hook inputs; this child does ALL the
650
- // heavy work — read the transcript, parse it, resolve git branch/SHAs (the
651
- // per-SHA `git rev-parse` calls that were overrunning CC's shutdown window
652
- // on Windows), build the payload, and dispatch the upload. Running here means
653
- // none of it is on the SessionEnd shutdown clock, so it can never be
654
- // "Hook cancelled". No stdin needed — handle it before readStdin().
655
423
  if (process.argv[2] === "capture-finish") {
656
424
  const jobFile = process.argv[3];
657
425
  if (jobFile) {
@@ -660,26 +428,25 @@ async function main() {
660
428
  await runCaptureFinish(job);
661
429
  }
662
430
  catch {
663
- // best-effort — never throw out of the detached child
664
431
  }
665
432
  finally {
666
433
  try {
667
434
  fs.unlinkSync(jobFile);
668
435
  }
669
436
  catch {
670
- /* ignore */
671
437
  }
672
438
  }
673
439
  }
674
440
  process.exit(0);
675
441
  }
676
- // Detached, best-effort security telemetry. Local policy decisions have
677
- // already completed in the parent hook; this child can never delay or alter
678
- // the developer's action.
679
- if (process.argv[2] === "security-upload") {
680
- await runSecurityEventUpload();
442
+ if (process.argv[2] === "self-update") {
443
+ await (0, selfUpdate_1.runSelfUpdate)(process.argv[1]);
681
444
  process.exit(0);
682
445
  }
446
+ const migrated = process.env.CODIEDEV_VIA_LAUNCHER ? [] : (0, utils_1.migrateHooksToLauncher)();
447
+ if (migrated.length === 0)
448
+ (0, selfUpdate_1.maybeSpawnSelfUpdate)(process.argv[1]);
449
+ (0, selfUpdate_1.maybeRefreshInstructions)();
683
450
  let hookInput = {};
684
451
  try {
685
452
  const raw = await readStdin();
@@ -687,64 +454,65 @@ async function main() {
687
454
  const parsed = JSON.parse(raw);
688
455
  hookInput = (process.argv[2]?.endsWith("-cursor")
689
456
  ? (0, cursorHooks_1.normalizeCursorHookInput)(parsed)
690
- : parsed);
457
+ : process.argv[2]?.endsWith("-copilot")
458
+ ? (0, copilotHooks_1.normalizeCopilotHookInput)(parsed)
459
+ : parsed);
691
460
  }
692
461
  }
693
462
  catch {
694
463
  process.exit(0);
695
464
  }
696
- // Privacy exclusions suppress every Codiedev upload, but they must never
697
- // disable local security enforcement. Security hooks run first with telemetry
698
- // disabled; all other hook modes still exit before making a network request.
699
- const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
700
- const securityCommand = process.argv[2] ?? "";
701
- const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|cursor|codie))?$/);
702
- if (securityMatch) {
703
- const source = securityMatch[2] === "codex"
704
- ? "codex"
705
- : securityMatch[2] === "cursor"
706
- ? "cursor"
707
- : securityMatch[2] === "codie"
708
- ? "codie-harness"
709
- : "claude-code";
710
- await runSecurityHook(hookInput, securityMatch[1], source, !captureIgnored);
465
+ if (process.argv[2]?.startsWith("security-"))
711
466
  process.exit(0);
712
- }
713
- // UserPromptSubmit discovery path — separate from session capture.
467
+ const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
714
468
  const promptCommand = process.argv[2] ?? "";
715
- if (promptCommand === "security-prompt-cursor") {
716
- if (runPromptSecurityHook(hookInput, "cursor", !captureIgnored))
717
- return;
718
- if (captureIgnored)
719
- return;
720
- dispatchAllowedPromptSecurityEvent(hookInput, "cursor");
721
- process.exit(0);
722
- }
723
469
  if (process.argv[2] === "discover" ||
724
470
  process.argv[2] === "discover-codex" ||
725
- process.argv[2] === "discover-codie") {
471
+ process.argv[2] === "discover-codie" ||
472
+ process.argv[2] === "discover-copilot" ||
473
+ process.argv[2] === "discover-pi") {
726
474
  const source = promptCommand === "discover-codex"
727
475
  ? "codex"
728
476
  : promptCommand === "discover-codie"
729
477
  ? "codie-harness"
730
- : "claude-code";
731
- // Security runs first and locally. A blocked prompt is erased before it
732
- // reaches the model, so discovery must not send it to the backend.
733
- if (runPromptSecurityHook(hookInput, source, !captureIgnored))
734
- return;
478
+ : promptCommand === "discover-copilot"
479
+ ? "copilot"
480
+ : promptCommand === "discover-pi"
481
+ ? "pi"
482
+ : "claude-code";
735
483
  if (captureIgnored)
736
484
  return;
737
- dispatchAllowedPromptSecurityEvent(hookInput, source);
738
- // A Codex block leaves no hook of its own, so this prompt is the first
739
- // chance to report it. Runs before runDiscover, which exits the process.
740
485
  if (source === "codex")
741
486
  await runCodexLimitSweep(hookInput.session_id);
742
- await runDiscover(hookInput.prompt);
487
+ if (source === "codex")
488
+ (0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
489
+ if (source !== "copilot")
490
+ await runDiscover(hookInput.prompt);
743
491
  return;
744
492
  }
745
493
  if (captureIgnored)
746
494
  process.exit(0);
747
- // StopFailure (matcher rate_limit): post the rate-limit event.
495
+ if (["session-start-cursor", "backup-cursor", "capture-cursor", "observe-cursor"].includes(process.argv[2])) {
496
+ try {
497
+ (0, cursorLifecycle_1.recordCursorLifecycle)(hookInput, process.argv[2] === "capture-cursor");
498
+ }
499
+ catch { }
500
+ }
501
+ if (process.argv[2] === "observe-cursor") {
502
+ process.stdout.write("{}");
503
+ return;
504
+ }
505
+ if (process.argv[2] === "limit-copilot") {
506
+ const config = (0, utils_1.readConfig)();
507
+ const event = (0, limitEvent_1.parseNativeLimitError)(hookInput);
508
+ if (config && event) {
509
+ try {
510
+ await (0, limitEventReporter_1.postLimitEvent)(config, { ...event, source: "copilot", sessionId: hookInput.session_id });
511
+ }
512
+ catch { }
513
+ }
514
+ return;
515
+ }
748
516
  if (process.argv[2] === "limit") {
749
517
  await runStopFailure(hookInput);
750
518
  return;
@@ -753,109 +521,100 @@ async function main() {
753
521
  await runCodexLimit(hookInput);
754
522
  return;
755
523
  }
756
- // SessionStart: post the capture-coverage denominator ping. Tiny + fast.
757
524
  if (process.argv[2] === "session-start" ||
758
525
  process.argv[2] === "session-start-cursor" ||
759
- process.argv[2] === "session-start-opencode") {
760
- if ((0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()) && (0, securityInventory_1.securityInventoryDue)()) {
761
- try {
762
- const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-inventory-upload", hookInput.cwd || process.cwd()], {
763
- detached: true,
764
- stdio: "ignore",
765
- });
766
- child.unref();
767
- }
768
- catch { /* daily inventory is best-effort */ }
769
- }
526
+ process.argv[2] === "session-start-copilot" ||
527
+ process.argv[2] === "session-start-opencode" ||
528
+ process.argv[2] === "session-start-pi") {
529
+ (0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
530
+ const packNag = process.argv[2] === "session-start-opencode" || process.argv[2] === "session-start-pi" ? null : (0, sync_1.packNagLine)();
531
+ if (packNag && process.argv[2] === "session-start")
532
+ process.stdout.write(`${packNag}\n`);
770
533
  await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
771
534
  ? "opencode"
772
535
  : process.argv[2] === "session-start-cursor"
773
536
  ? "cursor"
774
- : "claude-code");
537
+ : process.argv[2] === "session-start-copilot"
538
+ ? "copilot"
539
+ : process.argv[2] === "session-start-pi"
540
+ ? "pi"
541
+ : "claude-code");
542
+ if ((0, utils_1.readConfig)() && (process.argv[2] === "session-start-cursor" || process.argv[2] === "session-start-copilot")) {
543
+ const source = process.argv[2] === "session-start-cursor" ? "cursor" : "copilot";
544
+ const { TEAM_SKILL_CONTEXT, teamSkillSessionOutput } = await Promise.resolve().then(() => __importStar(require("./teamSkillContext")));
545
+ const prompt = hookInput.initialPrompt
546
+ ?? hookInput.initial_prompt;
547
+ const discovered = typeof prompt === "string" ? await discoverContext(prompt) : null;
548
+ process.stdout.write(JSON.stringify(teamSkillSessionOutput(source, [TEAM_SKILL_CONTEXT, discovered, packNag].filter(Boolean).join("\n\n"))));
549
+ }
775
550
  return;
776
551
  }
777
- // Self-heal: a user who upgraded the CLI but hasn't re-run `connect` gets
778
- // every codiedev hook — INCLUDING SessionEnd→capture, which previously had
779
- // no self-heal and could stay stripped forever while SessionStart kept
780
- // pinging (the Sonifi "starts arrive, captures never" loss) — re-wired on
781
- // their next Stop/SessionEnd. One read + one write, only when missing.
782
552
  if ((process.argv[2] ?? "").endsWith("-cursor")) {
783
553
  (0, utils_1.ensureCursorCodiedevHooksInstalled)();
784
554
  }
555
+ else if ((process.argv[2] ?? "").endsWith("-copilot")) {
556
+ (0, utils_1.ensureCopilotCodiedevHooksInstalled)();
557
+ }
558
+ else if (/-pi(?:-final)?$/.test(process.argv[2] ?? "")) {
559
+ (0, utils_1.ensurePiExtensionInstalled)();
560
+ }
785
561
  else {
786
562
  (0, utils_1.ensureAllCodiedevHooksInstalled)();
787
563
  }
788
564
  if (process.argv[2] === "capture-codex")
789
565
  (0, utils_1.ensureCodexLimitHookInstalled)();
566
+ if (/-opencode(?:-final)?$/.test(process.argv[2] ?? ""))
567
+ (0, utils_1.ensureOpenCodePluginCurrent)();
790
568
  const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
569
+ const launch = (0, launchContext_1.hookLaunch)(process.argv[2], hookInput, {
570
+ copilotClientName: process.argv[2]?.endsWith("-copilot")
571
+ ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path).clientName
572
+ : undefined,
573
+ });
791
574
  const workspaceRoot = workspace_roots && workspace_roots.length > 0
792
575
  ? workspace_roots[0]
793
576
  : cwd || process.cwd();
794
- // Stop/sessionEnd backup path — separate from session capture and needs no
795
- // transcript. Lists changed artifact files in the workspace and dispatches a
796
- // detached child that backs them up privately. Best-effort, never blocks.
797
- //
798
- // Per-turn capture: Stop fires reliably in editor-embedded clients (VS Code
799
- // extension) where SessionEnd may never come — the silent zero-capture bug.
800
- // Same detached capture-finish child as SessionEnd, marked non-final so the
801
- // server renews the session lease instead of summarizing immediately.
802
- if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor") {
803
- if (transcript_path) {
577
+ if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor" || process.argv[2] === "backup-copilot" || process.argv[2] === "backup-pi") {
578
+ if (transcript_path && !(0, utils_1.isCodexRolloutPath)(transcript_path)) {
804
579
  try {
805
580
  (0, utils_1.dispatchCaptureFinish)({
806
- mode: process.argv[2] === "backup-cursor" ? "capture-cursor" : "capture",
581
+ mode: process.argv[2] === "backup-cursor"
582
+ ? "capture-cursor"
583
+ : process.argv[2] === "backup-copilot"
584
+ ? "capture-copilot"
585
+ : process.argv[2] === "backup-pi"
586
+ ? "capture-pi"
587
+ : "capture",
807
588
  session_id,
808
589
  transcript_path,
809
590
  cwd,
810
591
  workspace_roots,
811
- // non-final: server should renew the lease, not close it yet.
592
+ ...(launch ? { launch } : {}),
812
593
  final: false,
813
594
  }, process.argv[1], {
814
595
  spawn: child_process_1.spawn,
815
596
  writeFileSync: fs.writeFileSync,
816
597
  openSync: fs.openSync,
817
598
  closeSync: fs.closeSync,
818
- // UNIQUE per dispatch (sid+pid+ts): Stop and SessionEnd children
819
- // each own their input file exclusively — a shared per-session
820
- // name let one child's `finally` unlink eat the other's input
821
- // (lost final:true) or double-dispatch. See finishInputTmpFile.
822
599
  tmpFile: (0, utils_1.finishInputTmpFile)(session_id),
823
600
  });
824
601
  }
825
602
  catch {
826
- // capture is best-effort; never break backup
827
603
  }
828
604
  }
829
605
  const { runBackup } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
830
606
  runBackup(workspaceRoot, process.argv[1]);
831
607
  process.exit(0);
832
608
  }
833
- // Capture path (SessionEnd). Do the ABSOLUTE MINIMUM inline so this hook
834
- // exits 0 within Claude Code's SessionEnd shutdown window: a cheap config /
835
- // transcript guard, then hand ALL the heavy work (transcript read + parse +
836
- // per-SHA `git rev-parse` + upload) to a detached `capture-finish` child.
837
- // Detaching only the upload (the old #240 fix) left the synchronous parse and
838
- // git work on the shutdown clock, which still overran on Windows — where each
839
- // subprocess spawn costs ~50-150ms — producing "Hook cancelled" even though
840
- // the data still landed. JS can't be interrupted mid-sync-chain, so the work
841
- // finished and the upload went through, but CC had already given up waiting.
842
609
  const config = (0, utils_1.readConfig)();
843
610
  if (!config) {
844
- // No config = no token; nothing can be sent, not even a skip ping.
845
611
  process.exit(0);
846
612
  }
847
613
  if (!transcript_path) {
848
- // Nothing to capture — but say so, instead of leaving a start ping with
849
- // no matching capture (silent coverage loss). One tiny POST; the rare
850
- // no-transcript case has no other work on the shutdown clock.
851
614
  await postSessionSkip(config, session_id, "no-transcript-path");
852
615
  process.exit(0);
853
616
  }
854
617
  try {
855
- // "codiedev-finish-" prefix (NOT "codiedev-capture-") so the sweep regex
856
- // /^codiedev-capture-.*\.json$/ never mistakes a finish-input file for an
857
- // upload job. UNIQUE per dispatch (sid+pid+ts) so a concurrent Stop-hook
858
- // child's `finally` unlink can never eat this input (lost final:true).
859
618
  const jobFile = (0, utils_1.finishInputTmpFile)(session_id);
860
619
  (0, utils_1.dispatchCaptureFinish)({
861
620
  mode: process.argv[2] ?? "capture",
@@ -863,9 +622,7 @@ async function main() {
863
622
  transcript_path,
864
623
  cwd,
865
624
  workspace_roots,
866
- // final:true for real SessionEnd signals (Claude Code and Cursor).
867
- // Codex routes its per-turn Stop hook through "capture-codex" —
868
- // final:true there would close the server lease every turn.
625
+ ...(launch ? { launch } : {}),
869
626
  final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
870
627
  }, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
871
628
  }
@@ -874,19 +631,10 @@ async function main() {
874
631
  }
875
632
  process.exit(0);
876
633
  }
877
- // Runs inside the detached `capture-finish` child spawned by the SessionEnd
878
- // hook above. This is everything that used to run inline in main(): read the
879
- // transcript, resolve the repo, parse stats/usage/steering, resolve the git
880
- // branch + authored SHAs (the per-SHA `git rev-parse` calls that overran the
881
- // shutdown window on Windows), build the payload, and fire the upload from a
882
- // further-detached uploader. Running here means none of it is on the SessionEnd
883
- // shutdown clock, so it can never be "Hook cancelled". Best-effort throughout.
884
634
  async function runCaptureFinish(input) {
885
635
  const config = (0, utils_1.readConfig)();
886
636
  if (!config)
887
637
  return;
888
- // Re-check in the detached child: the user may have added an exclusion
889
- // after the parent hook dispatched this job but before it started.
890
638
  if ((0, captureExclusions_1.shouldIgnoreCaptureInput)(config, input)) {
891
639
  (0, captureLog_1.appendCaptureLog)({
892
640
  stage: "capture-finish",
@@ -901,8 +649,6 @@ async function runCaptureFinish(input) {
901
649
  await postSessionSkip(config, session_id, "no-transcript-path");
902
650
  return;
903
651
  }
904
- // Log that this child has started processing — the first diagnostic proof
905
- // that the detached capture-finish child actually ran (visible in doctor).
906
652
  (0, captureLog_1.appendCaptureLog)({ stage: "capture-finish", sessionId: session_id, outcome: "start" });
907
653
  try {
908
654
  await runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots);
@@ -918,10 +664,6 @@ async function runCaptureFinish(input) {
918
664
  }
919
665
  async function runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots) {
920
666
  const mode = resolveMode(input.mode);
921
- // Desktop transcripts (opencode SDK {info,parts}[] dumps) are byte-identical
922
- // to opencode's, so every parser below EXCEPT parseUsage treats desktop
923
- // exactly as opencode via this mapping. parseUsage is the one exception —
924
- // see the rawTranscriptContent note below.
925
667
  const parserMode = mode === "desktop" ? "opencode" : mode;
926
668
  const workspaceRoot = workspace_roots && workspace_roots.length > 0
927
669
  ? workspace_roots[0]
@@ -930,11 +672,14 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
930
672
  let transcriptContent;
931
673
  try {
932
674
  transcriptContent = fs.readFileSync(transcript_path, "utf8");
675
+ if (mode === "copilot" && input.final === true && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent)) {
676
+ for (let waited = 0; waited < COPILOT_SHUTDOWN_WAIT_MS && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent); waited += 250) {
677
+ await new Promise((resolve) => setTimeout(resolve, 250));
678
+ transcriptContent = fs.readFileSync(transcript_path, "utf8");
679
+ }
680
+ }
933
681
  }
934
682
  catch (err) {
935
- // ENOENT → "no-transcript" (phantom desktop-app helper session, excluded
936
- // from coverage server-side); anything else → "transcript-unreadable:<code>"
937
- // (real transcript, read failed — a genuine, diagnosable loss).
938
683
  const reason = (0, utils_1.transcriptReadSkipReason)(err);
939
684
  (0, captureLog_1.appendCaptureLog)({
940
685
  stage: "capture-finish",
@@ -947,98 +692,93 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
947
692
  }
948
693
  return;
949
694
  }
950
- // Snapshot the RAW transcript before normalization below flattens it to
951
- // Claude Code's newline-JSONL shape — that flattening drops info.cost /
952
- // info.providerID, which desktop's parseUsage (below) needs and opencode's
953
- // doesn't. Every other mode leaves transcriptContent untouched here, so this
954
- // is a no-op for them.
955
695
  const rawTranscriptContent = transcriptContent;
696
+ let copilotSubagents;
697
+ const copilotWorkspace = mode === "copilot" ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path) : {};
956
698
  if (parserMode === "opencode") {
957
699
  transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
958
700
  }
959
701
  else if (parserMode === "cursor") {
960
702
  transcriptContent = (0, cursorHooks_1.normalizeCursorTranscript)(transcriptContent);
703
+ const observations = (0, cursorLifecycle_1.cursorLifecycleRecords)(session_id);
704
+ if (observations)
705
+ transcriptContent += `\n${observations}`;
706
+ }
707
+ else if (parserMode === "copilot") {
708
+ const normalized = (0, copilotHooks_1.normalizeCopilotTranscript)(transcriptContent, { clientName: copilotWorkspace.clientName });
709
+ transcriptContent = normalized.content;
710
+ copilotSubagents = normalized.subagents.count > 0 ? normalized.subagents : undefined;
711
+ }
712
+ else if (parserMode === "pi") {
713
+ transcriptContent = (0, piHooks_1.normalizePiTranscript)(transcriptContent);
714
+ }
715
+ if (mode === "cursor" || mode === "copilot" || mode === "pi") {
716
+ try {
717
+ await (0, limitEventReporter_1.reportTranscriptLimitEvent)({ config, source: mode, sessionId: session_id, transcript: transcriptContent });
718
+ }
719
+ catch { }
961
720
  }
962
- // Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
963
- // need the git remote of the workspace.
964
721
  let remoteUrl = null;
965
722
  if (parserMode === "codex") {
966
723
  remoteUrl = (0, utils_1.extractCodexRepoUrl)(transcriptContent);
967
724
  }
968
- // The worktrees the session actually wrote to — resolved up front because
969
- // they both anchor the branch/sha join keys below AND back-fill the repo tag
970
- // when the launch dir is a multi-repo WORKSPACE root (a parent folder of
971
- // several cloned repos) that has no remote of its own.
972
725
  const captureDirs = (0, utils_1.resolveCaptureDirs)(transcriptContent, workspaceRoot);
973
726
  if (!remoteUrl) {
974
727
  remoteUrl = (0, utils_1.resolveCaptureRemoteUrl)(workspaceRoot, captureDirs);
975
728
  }
976
- // Capture-first: the transcript is the asset; the repo is just a tag. We do
977
- // NOT bail when `remoteUrl` is null — that previously dropped EVERY session
978
- // for users whose workspace couldn't resolve a remote (no git binary on lean
979
- // Alpine images, no `origin`, or a submodule whose parent .git isn't mounted),
980
- // with no error and a green `doctor`. Upload regardless; the backend accepts
981
- // an unmatched capture (repoId null) and surfaces it for triage.
982
- //
983
- // Best-effort local match — backend re-resolves from `remoteUrl` against the
984
- // company's current repo list, so a stale/empty local cache doesn't matter.
729
+ if (remoteUrl)
730
+ remoteUrl = (0, utils_1.stripRepoUrlCredentials)(remoteUrl);
985
731
  const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
986
732
  const stats = parserMode === "codex"
987
733
  ? (0, utils_1.parseCodexStats)(transcriptContent)
988
734
  : (0, utils_1.parseClaudeCodeStats)(transcriptContent);
989
- // Cursor can emit duplicate lifecycle notifications while one transcript
990
- // path is still byte-empty. Never use parsed message count here: Cursor's
991
- // private JSONL shape can evolve independently, and parser drift must not
992
- // silently discard a real non-empty session.
993
- if (parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)) {
735
+ const emptyReason = parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)
736
+ ? "empty-cursor-transcript"
737
+ : parserMode === "copilot" && (0, copilotHooks_1.isCopilotTranscriptEmpty)(rawTranscriptContent)
738
+ ? "empty-copilot-transcript"
739
+ : parserMode === "pi" && (0, piHooks_1.isPiTranscriptEmpty)(rawTranscriptContent)
740
+ ? "empty-pi-transcript"
741
+ : null;
742
+ if (emptyReason) {
994
743
  (0, captureLog_1.appendCaptureLog)({
995
744
  stage: "capture-finish",
996
745
  sessionId: session_id,
997
746
  outcome: "skip",
998
- detail: "empty-cursor-transcript",
747
+ detail: emptyReason,
999
748
  });
1000
- // A non-final stop snapshot is only a backup; reporting it as a skipped
1001
- // session would close/remove server-side start coverage while the session
1002
- // is still alive. The real SessionEnd remains responsible for a skip ping.
1003
749
  if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
1004
- await postSessionSkip(config, session_id, "empty-cursor-transcript");
750
+ await postSessionSkip(config, session_id, emptyReason);
1005
751
  }
1006
752
  return;
1007
753
  }
1008
754
  const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
1009
- // Session→merge join keys: the branch + HEAD sha the work happened on. Matched
1010
- // server-side against a merged MR/PR's source_branch / commits for AI attribution.
1011
- // Capture from the worktree the agent actually worked in (the launch dir stays
1012
- // on main/detached when work happens in a `git worktree`), not workspaceRoot.
1013
- // One session can touch several worktrees (→ several PRs); capture EVERY
1014
- // worktree's branch+sha so all the resulting PRs attribute. branch/headSha
1015
- // remain the primary (most-recent) values for back-compat.
1016
755
  const keys = (0, utils_1.collectCaptureKeys)(captureDirs);
1017
756
  const gitRefs = (0, utils_1.collectCaptureGitRefs)(captureDirs);
1018
- // Union the worktree-dir branches with the branches the agent actually
1019
- // committed/pushed to (parsed from the transcript's git output). Like the
1020
- // SHAs below, these are independent of end-of-session filesystem state, so
1021
- // they rescue branch-join when the worktree was REMOVED before SessionEnd
1022
- // (the dominant miss: dir-resolution falls back to the parent's branch, never
1023
- // the `worktree-<name>` one the PR was opened from). Indexed server-side under
1024
- // every branch in the array, so the worktree's PR attributes regardless.
1025
757
  const authoredBranches = (0, utils_1.parseAuthoredBranches)(transcriptContent);
1026
- const branches = [...new Set([...keys.branches, ...authoredBranches])];
758
+ const recordedBranches = copilotWorkspace.branch ? [copilotWorkspace.branch] : [];
759
+ const branches = [...new Set([...keys.branches, ...authoredBranches, ...recordedBranches])];
1027
760
  const branch = keys.branch ?? branches[0] ?? null;
1028
- // Union the worktree HEAD shas with the SHAs the agent actually committed/
1029
- // pushed (parsed from the transcript's git output, resolved via the shared
1030
- // object store). The authored shas are independent of end-of-session
1031
- // filesystem state, so they rescue attribution when a worktree is stale/gone.
1032
761
  const authoredShas = (0, utils_1.parseAuthoredShas)(transcriptContent, workspaceRoot);
1033
762
  const headShas = [...new Set([...keys.headShas, ...authoredShas])];
1034
763
  const headSha = keys.headSha ?? headShas[0] ?? null;
1035
764
  const pullRequestUrls = (0, utils_1.parsePullRequestUrls)(transcriptContent);
1036
- // Cost/velocity/steering signals parsed straight from the transcript we're
1037
- // already holding — no extra hooks, no extra git/process calls. parseUsage
1038
- // keeps the literal mode (it has its own desktop parser) and reads the RAW
1039
- // pre-normalization content for desktop; parseSteering routes through
1040
- // parserMode like the parsers above.
1041
765
  const usage = (0, utils_1.parseUsage)(mode === "desktop" ? rawTranscriptContent : transcriptContent, mode);
766
+ const subagents = mode === "claude-code"
767
+ ? (0, subagents_1.readSubagentTranscripts)(transcript_path, session_id)
768
+ : { contents: [], count: 0, truncated: false };
769
+ const subagentUsage = (0, subagents_1.foldSubagentModelUsage)([], subagents, mode);
770
+ for (const u of subagentUsage) {
771
+ usage.inputTokens = (usage.inputTokens ?? 0) + u.inputTokens;
772
+ usage.outputTokens = (usage.outputTokens ?? 0) + u.outputTokens;
773
+ usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + u.cacheReadTokens;
774
+ usage.cacheCreationTokens = (usage.cacheCreationTokens ?? 0) + u.cacheCreationTokens;
775
+ usage.cacheTokens = (usage.cacheTokens ?? 0) + u.cacheReadTokens + u.cacheCreationTokens;
776
+ }
777
+ const hasUsage = mode === "cursor" ? (0, cursorHooks_1.cursorTranscriptHasUsage)(transcriptContent)
778
+ : mode === "copilot" ? (0, transcriptModels_1.parseTranscriptModels)(transcriptContent, mode).some(model => model.tokensAvailable !== false)
779
+ : true;
780
+ if (mode === "cursor" && !usage.model)
781
+ usage.model = (0, cursorHooks_1.cursorObservedModel)(transcriptContent) ?? null;
1042
782
  const sessionStartedAt = (0, utils_1.parseSessionStart)(transcriptContent);
1043
783
  const steering = (0, utils_1.parseSteering)(transcriptContent, parserMode);
1044
784
  const payload = (0, utils_1.buildCapturePayload)({
@@ -1057,33 +797,26 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1057
797
  headShas,
1058
798
  gitRefs,
1059
799
  pullRequestUrls,
1060
- inputTokens: usage.inputTokens,
1061
- outputTokens: usage.outputTokens,
1062
- cacheTokens: usage.cacheTokens,
1063
- cacheReadTokens: usage.cacheReadTokens,
1064
- cacheCreationTokens: usage.cacheCreationTokens,
800
+ inputTokens: hasUsage ? usage.inputTokens : undefined,
801
+ outputTokens: hasUsage ? usage.outputTokens : undefined,
802
+ cacheTokens: hasUsage ? usage.cacheTokens : undefined,
803
+ cacheReadTokens: hasUsage ? usage.cacheReadTokens : undefined,
804
+ cacheCreationTokens: hasUsage ? usage.cacheCreationTokens : undefined,
1065
805
  model: usage.model,
1066
- captureSignals: (0, captureSignals_1.parseCaptureSignals)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode),
1067
- modelUsage: (0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode),
806
+ 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),
807
+ modelUsage: (0, subagents_1.foldSubagentModelUsage)((0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode), subagents, mode),
1068
808
  sessionStartedAt,
1069
809
  interruptCount: steering.interrupts,
1070
810
  rejectionCount: steering.rejections,
1071
- // Carry the final flag into the HTTP body — the server uses it to decide
1072
- // whether to close the session lease (true) or renew it (false/absent).
1073
811
  final: input.final,
1074
- // Desktop-only measured spend (undefined for every other source).
1075
812
  costUsd: usage.costUsd,
1076
813
  });
1077
- // Throttle decision: final captures go immediately; non-final (per-turn Stop)
1078
- // are deferred if we uploaded this session recently, to avoid per-turn churn.
1079
814
  const uploadState = (0, utils_1.readSessionUploadState)(session_id ?? "");
1080
815
  const decision = (0, utils_1.decideDispatch)({
1081
816
  final: input.final === true,
1082
817
  lastUploadAt: uploadState?.lastUploadAt ?? null,
1083
818
  now: Date.now(),
1084
819
  });
1085
- // Log capture-finish outcome with dispatch context so doctor can see whether
1086
- // the upload went immediately or was deferred (and by how much).
1087
820
  (0, captureLog_1.appendCaptureLog)({
1088
821
  stage: "capture-finish",
1089
822
  sessionId: session_id,
@@ -1092,11 +825,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1092
825
  ? `dispatch=deferred delayMs=${decision.delayMs} final=${input.final === true} repo=${remoteUrl ?? "-"} branch=${branch ?? "-"} msgs=${stats.messageCount}`
1093
826
  : `dispatch=immediate final=${input.final === true} repo=${remoteUrl ?? "-"} branch=${branch ?? "-"} msgs=${stats.messageCount}`,
1094
827
  });
1095
- // Fire the upload from a further-detached child. We're already off the
1096
- // shutdown clock here, but reusing dispatchCaptureDetached keeps the upload
1097
- // retry + last-capture breadcrumb logic (runCaptureUpload) intact verbatim.
1098
- // Per-session job file: one file per session; newer turns overwrite it,
1099
- // which is the "supersede" mechanism for the deferred path.
1100
828
  try {
1101
829
  const hookScriptPath = process.argv[1];
1102
830
  const tmpFile = (0, utils_1.captureUploadTmpFile)(session_id, decision.mode);
@@ -1107,21 +835,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1107
835
  body: payload,
1108
836
  scopePaths,
1109
837
  }, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync });
1110
- // The immediate payload is durable before removing the older scheduled
1111
- // snapshot. Do this for every immediate dispatch, not only SessionEnd:
1112
- // first/outside-window snapshots also supersede a sleeping deferred job.
1113
838
  if (session_id)
1114
839
  (0, utils_1.cancelDeferredCaptureUpload)(session_id);
1115
- // Retry any earlier capture jobs that failed to upload (transient network
1116
- // blips), excluding the one we just dispatched. Best-effort, detached.
1117
- // Skip the sweep on deferred path — the deferred child will handle uploading.
1118
- //
1119
- // Orphan pings are COLLECTED and awaited below: this child process calls
1120
- // process.exit(0) as soon as runCaptureFinish resolves, so a
1121
- // fire-and-forget POST here would be killed mid-flight — after the
1122
- // orphan file was already unlinked, i.e. the loss would stay invisible
1123
- // forever. The await is off the shutdown clock (detached child) and
1124
- // bounded by PING_TIMEOUT_MS.
1125
840
  const orphanPings = [];
1126
841
  (0, utils_1.sweepPendingCaptures)({
1127
842
  dir: os.tmpdir(),
@@ -1133,9 +848,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1133
848
  exclude: tmpFile,
1134
849
  openSync: fs.openSync,
1135
850
  closeSync: fs.closeSync,
1136
- // Orphaned finish-inputs (capture-finish child died before writing an
1137
- // upload job): unrecoverable payload, but report the loss so it lands
1138
- // as a skip-reason instead of a silent coverage hole.
1139
851
  unlinkSync: fs.unlinkSync,
1140
852
  reportOrphan: (sid) => {
1141
853
  orphanPings.push(postSessionSkip(config, sid, "finish-orphaned"));
@@ -1144,9 +856,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1144
856
  await Promise.all(orphanPings);
1145
857
  }
1146
858
  else {
1147
- // Deferred: write the job file (newer turns overwrite = supersede), then
1148
- // spawn the deferred-upload child which will sleep, claim, and upload.
1149
- // Same stdio/fd-hygiene contract as dispatchCaptureDetached.
1150
859
  (0, utils_1.dispatchDeferredUpload)({
1151
860
  url: `${config.backendUrl}/api/captureSession`,
1152
861
  token: config.token,
@@ -1159,6 +868,10 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
1159
868
  process.stderr.write(`[codiedev-hook] Warning: Failed to dispatch session capture — ${err.message}\n`);
1160
869
  }
1161
870
  }
871
+ const COPILOT_SHUTDOWN_WAIT_MS = 5000;
872
+ function withCopilotSubagents(signals, subagents) {
873
+ return subagents ? { ...signals, subagents } : signals;
874
+ }
1162
875
  main().catch(() => {
1163
876
  process.exit(0);
1164
877
  });