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.
- package/README.md +12 -5
- package/dist/backup/cache.js +1 -5
- package/dist/backup/match.js +0 -6
- package/dist/backup/run.js +3 -35
- package/dist/captureExclusions.js +0 -12
- package/dist/captureLog.js +0 -28
- package/dist/captureReadiness.js +0 -58
- package/dist/captureSignals.js +0 -11
- package/dist/cli.js +11 -33
- package/dist/cliSession.js +3 -60
- package/dist/codexHookTrust.js +0 -28
- package/dist/codexLimitScan.js +0 -107
- package/dist/codie.js +64 -57
- package/dist/codieBrandInstall.js +0 -18
- package/dist/codieLauncher.js +1 -72
- package/dist/codieRuntime.js +27 -32
- package/dist/codieRuntimeInstall.js +11 -43
- package/dist/codieUsage.js +0 -49
- package/dist/commands/agentTemplates.generated.js +136 -0
- package/dist/commands/agents.js +1088 -0
- package/dist/commands/agentsSdk.generated.js +7 -0
- package/dist/commands/ask.js +0 -3
- package/dist/commands/capture.js +0 -2
- package/dist/commands/createTicket.js +0 -10
- package/dist/commands/doctor.js +2 -72
- package/dist/commands/evals.js +3 -9
- package/dist/commands/inbox.js +0 -2
- package/dist/commands/library.js +0 -3
- package/dist/commands/pack.js +0 -29
- package/dist/commands/ping.js +0 -2
- package/dist/commands/pull.js +0 -9
- package/dist/commands/push.js +0 -12
- package/dist/commands/search.js +0 -4
- package/dist/commands/send.js +0 -5
- package/dist/commands/share.js +0 -4
- package/dist/commands/shared.js +0 -35
- package/dist/commands/traces.js +0 -13
- package/dist/commands/tracesLabel.js +0 -8
- package/dist/configDir.js +1 -18
- package/dist/connect.js +3 -75
- package/dist/connectFlow.js +0 -15
- package/dist/copilotHooks.js +0 -67
- package/dist/copilotInstallation.js +0 -1
- package/dist/cursorHooks.js +1 -11
- package/dist/cursorLifecycle.js +1 -7
- package/dist/detection.js +0 -30
- package/dist/harnessDirs.js +0 -19
- package/dist/hook.js +43 -293
- package/dist/inboxFormat.js +0 -24
- package/dist/launchContext.js +0 -50
- package/dist/launcher.js +0 -33
- package/dist/limitEvent.js +2 -34
- package/dist/managedPi.js +2 -50
- package/dist/mcp.js +0 -48
- package/dist/nativeSteering.js +1 -4
- package/dist/packs/codie.js +0 -15
- package/dist/packs/harnesses.js +1 -38
- package/dist/packs/hookGate.js +0 -33
- package/dist/packs/install.js +0 -20
- package/dist/packs/manifest.js +1 -15
- package/dist/packs/paths.js +0 -8
- package/dist/packs/shim.js +0 -8
- package/dist/packs/store.js +0 -12
- package/dist/packs/sync.js +0 -20
- package/dist/piHooks.js +4 -45
- package/dist/piRuntime.js +0 -18
- package/dist/repoResolver.js +0 -19
- package/dist/secret.js +0 -3
- package/dist/selfUpdate.js +2 -77
- package/dist/skillMarker.js +0 -14
- package/dist/subagents.js +0 -18
- package/dist/teamSkillContext.js +0 -1
- package/dist/transcriptModels.js +0 -8
- package/dist/upgradeNudge.js +1 -30
- package/dist/utils.js +165 -768
- package/dist/version.js +0 -10
- package/dist/versionCheck.js +0 -33
- package/package.json +13 -8
- package/dist/backup/cache.d.ts +0 -14
- package/dist/backup/match.d.ts +0 -2
- package/dist/backup/run.d.ts +0 -25
- package/dist/captureExclusions.d.ts +0 -30
- package/dist/captureLog.d.ts +0 -20
- package/dist/captureReadiness.d.ts +0 -65
- package/dist/captureSignals.d.ts +0 -47
- package/dist/cli.d.ts +0 -15
- package/dist/cliSession.d.ts +0 -108
- package/dist/codexHookTrust.d.ts +0 -49
- package/dist/codexLimitScan.d.ts +0 -64
- package/dist/codie.d.ts +0 -2
- package/dist/codieBrandInstall.d.ts +0 -11
- package/dist/codieLauncher.d.ts +0 -87
- package/dist/codieRuntime.d.ts +0 -35
- package/dist/codieRuntimeInstall.d.ts +0 -56
- package/dist/codieUsage.d.ts +0 -64
- package/dist/commands/ask.d.ts +0 -1
- package/dist/commands/capture.d.ts +0 -14
- package/dist/commands/createTicket.d.ts +0 -27
- package/dist/commands/delete.d.ts +0 -1
- package/dist/commands/doctor.d.ts +0 -9
- package/dist/commands/evals.d.ts +0 -3
- package/dist/commands/inbox.d.ts +0 -3
- package/dist/commands/library.d.ts +0 -2
- package/dist/commands/note.d.ts +0 -1
- package/dist/commands/pack.d.ts +0 -54
- package/dist/commands/ping.d.ts +0 -1
- package/dist/commands/pull.d.ts +0 -2
- package/dist/commands/push.d.ts +0 -33
- package/dist/commands/search.d.ts +0 -2
- package/dist/commands/send.d.ts +0 -2
- package/dist/commands/share.d.ts +0 -2
- package/dist/commands/shared.d.ts +0 -64
- package/dist/commands/traces.d.ts +0 -72
- package/dist/commands/tracesLabel.d.ts +0 -24
- package/dist/configDir.d.ts +0 -10
- package/dist/connect.d.ts +0 -1
- package/dist/connectFlow.d.ts +0 -34
- package/dist/copilotHooks.d.ts +0 -85
- package/dist/copilotInstallation.d.ts +0 -10
- package/dist/cursorHooks.d.ts +0 -13
- package/dist/cursorLifecycle.d.ts +0 -2
- package/dist/detection.d.ts +0 -30
- package/dist/harnessDirs.d.ts +0 -15
- package/dist/hook.d.ts +0 -6
- package/dist/inboxFormat.d.ts +0 -38
- package/dist/launchContext.d.ts +0 -46
- package/dist/launcher.d.ts +0 -25
- package/dist/limitEvent.d.ts +0 -15
- package/dist/limitEventReporter.d.ts +0 -21
- package/dist/managedPi.d.ts +0 -48
- package/dist/mcp.d.ts +0 -12
- package/dist/nativeSteering.d.ts +0 -5
- package/dist/packs/codie.d.ts +0 -45
- package/dist/packs/harnesses.d.ts +0 -74
- package/dist/packs/hookGate.d.ts +0 -53
- package/dist/packs/install.d.ts +0 -46
- package/dist/packs/manifest.d.ts +0 -52
- package/dist/packs/paths.d.ts +0 -17
- package/dist/packs/shim.d.ts +0 -26
- package/dist/packs/store.d.ts +0 -42
- package/dist/packs/sync.d.ts +0 -40
- package/dist/piHooks.d.ts +0 -22
- package/dist/piRuntime.d.ts +0 -26
- package/dist/repoResolver.d.ts +0 -17
- package/dist/secret.d.ts +0 -1
- package/dist/selfUpdate.d.ts +0 -64
- package/dist/skillMarker.d.ts +0 -6
- package/dist/subagents.d.ts +0 -36
- package/dist/teamSkillContext.d.ts +0 -9
- package/dist/transcriptModels.d.ts +0 -20
- package/dist/upgradeNudge.d.ts +0 -24
- package/dist/utils.d.ts +0 -396
- package/dist/version.d.ts +0 -1
- 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
|
-
|
|
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
|
-
|
|
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 {
|
|
502
|
+
catch { }
|
|
587
503
|
}
|
|
588
504
|
if (process.argv[2] === "observe-cursor") {
|
|
589
505
|
process.stdout.write("{}");
|
|
590
|
-
return;
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
911
|
-
|
|
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
|
}
|