codiedev 0.10.0-beta.3 → 0.10.0-beta.5
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 +7 -1
- package/assets/tui-brand-plugin.tsx +73 -0
- 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 +66 -3
- package/dist/cli.js +41 -18
- package/dist/codexHookTrust.js +61 -5
- package/dist/codexLimitScan.js +2 -109
- package/dist/codie.js +41 -56
- package/dist/codieBrandInstall.js +80 -0
- package/dist/codieLauncher.js +59 -59
- package/dist/codieRuntime.js +19 -23
- package/dist/codieRuntimeInstall.js +108 -23
- package/dist/codieStartup.js +0 -3
- package/dist/codieUsage.js +181 -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 +41 -82
- package/dist/commands/inbox.js +0 -2
- package/dist/commands/library.js +0 -3
- package/dist/commands/pack.js +573 -0
- package/dist/commands/ping.js +0 -2
- package/dist/commands/pull.js +6 -10
- package/dist/commands/push.js +2 -12
- package/dist/commands/react.js +0 -2
- package/dist/commands/reverseTicket.js +0 -33
- 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 +280 -0
- package/dist/configDir.js +0 -10
- package/dist/connect.js +21 -77
- package/dist/connectFlow.js +0 -9
- package/dist/copilotHooks.js +395 -0
- package/dist/copilotInstallation.js +64 -0
- package/dist/cursorHooks.js +40 -17
- package/dist/cursorLifecycle.js +109 -0
- package/dist/detection.js +0 -30
- package/dist/harnessDirs.js +73 -0
- package/dist/hook.js +193 -316
- package/dist/inboxFormat.js +0 -24
- package/dist/launchContext.js +150 -0
- package/dist/launcher.js +140 -0
- package/dist/limitEvent.js +41 -35
- package/dist/limitEventReporter.js +1 -1
- package/dist/mcp.js +3 -77
- package/dist/nativeSteering.js +24 -0
- package/dist/packs/codie.js +182 -0
- package/dist/packs/harnesses.js +374 -0
- package/dist/packs/hookGate.js +256 -0
- package/dist/packs/install.js +285 -0
- package/dist/packs/manifest.js +126 -0
- package/dist/packs/paths.js +81 -0
- package/dist/packs/shim.js +214 -0
- package/dist/packs/store.js +155 -0
- package/dist/packs/sync.js +170 -0
- package/dist/prUrl.js +0 -5
- package/dist/repoResolver.js +0 -19
- package/dist/secret.js +0 -3
- package/dist/security.js +0 -20
- package/dist/securityDeepScan.js +1 -14
- package/dist/securityDoctor.js +0 -2
- package/dist/securityFeature.js +0 -1
- package/dist/securityInstall.js +6 -13
- package/dist/securityInventory.js +23 -13
- package/dist/securityPilot.js +0 -2
- package/dist/securityPolicy.js +0 -7
- package/dist/selfUpdate.js +331 -0
- package/dist/skillMarker.js +23 -0
- package/dist/subagents.js +135 -0
- package/dist/teamSkillContext.js +10 -0
- package/dist/transcriptModels.js +6 -8
- package/dist/upgradeNudge.js +0 -30
- package/dist/utils.js +286 -733
- package/dist/version.js +0 -10
- package/dist/versionCheck.js +0 -33
- package/package.json +8 -7
- 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 -19
- package/dist/cli.d.ts +0 -16
- package/dist/codexHookTrust.d.ts +0 -34
- package/dist/codexLimitScan.d.ts +0 -64
- package/dist/codie.d.ts +0 -2
- package/dist/codieLauncher.d.ts +0 -53
- package/dist/codieRuntime.d.ts +0 -29
- package/dist/codieRuntimeInstall.d.ts +0 -28
- package/dist/codieStartup.d.ts +0 -26
- 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 -1
- 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/ping.d.ts +0 -1
- package/dist/commands/promote.d.ts +0 -1
- package/dist/commands/pull.d.ts +0 -2
- package/dist/commands/push.d.ts +0 -33
- package/dist/commands/react.d.ts +0 -2
- package/dist/commands/reverseTicket.d.ts +0 -2
- package/dist/commands/search.d.ts +0 -2
- package/dist/commands/security.d.ts +0 -1
- 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/configDir.d.ts +0 -1
- package/dist/connect.d.ts +0 -1
- package/dist/connectFlow.d.ts +0 -13
- package/dist/cursorHooks.d.ts +0 -23
- package/dist/detection.d.ts +0 -30
- package/dist/hook.d.ts +0 -6
- package/dist/inboxFormat.d.ts +0 -38
- package/dist/limitEvent.d.ts +0 -16
- package/dist/limitEventReporter.d.ts +0 -21
- package/dist/mcp.d.ts +0 -12
- package/dist/prUrl.d.ts +0 -9
- package/dist/repoResolver.d.ts +0 -17
- package/dist/secret.d.ts +0 -1
- package/dist/security.d.ts +0 -102
- package/dist/securityDeepScan.d.ts +0 -41
- package/dist/securityDoctor.d.ts +0 -32
- package/dist/securityFeature.d.ts +0 -5
- package/dist/securityInstall.d.ts +0 -63
- package/dist/securityInventory.d.ts +0 -54
- package/dist/securityPilot.d.ts +0 -19
- package/dist/securityPolicy.d.ts +0 -33
- package/dist/securitySession.d.ts +0 -6
- package/dist/transcriptModels.d.ts +0 -18
- package/dist/upgradeNudge.d.ts +0 -25
- package/dist/utils.d.ts +0 -346
- package/dist/version.d.ts +0 -1
- package/dist/versionCheck.d.ts +0 -68
package/dist/hook.js
CHANGED
|
@@ -39,30 +39,32 @@ 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
56
|
const security_1 = require("./security");
|
|
51
57
|
const securitySession_1 = require("./securitySession");
|
|
52
58
|
const securityPolicy_1 = require("./securityPolicy");
|
|
59
|
+
const subagents_1 = require("./subagents");
|
|
53
60
|
const securityFeature_1 = require("./securityFeature");
|
|
54
61
|
const securityInventory_1 = require("./securityInventory");
|
|
55
62
|
const captureExclusions_1 = require("./captureExclusions");
|
|
56
63
|
const cursorHooks_1 = require("./cursorHooks");
|
|
57
|
-
|
|
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.
|
|
64
|
+
const copilotHooks_1 = require("./copilotHooks");
|
|
61
65
|
function buildDiscoveryContext(skills) {
|
|
62
66
|
if (skills.length === 0)
|
|
63
67
|
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
68
|
const top = [...skills].sort((a, b) => b.score - a.score).slice(0, 3);
|
|
67
69
|
const lines = top
|
|
68
70
|
.map((s) => `- ${s.key} — ${(s.score * 100).toFixed(0)}% match`)
|
|
@@ -74,31 +76,27 @@ function buildDiscoveryContext(skills) {
|
|
|
74
76
|
"`. It encodes the team's vetted " +
|
|
75
77
|
"approach. Skip it only if it clearly doesn't apply.");
|
|
76
78
|
}
|
|
77
|
-
|
|
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) {
|
|
79
|
+
async function discoverContext(prompt) {
|
|
81
80
|
if (!prompt || !prompt.trim())
|
|
82
|
-
|
|
81
|
+
return null;
|
|
83
82
|
const config = (0, utils_1.readConfig)();
|
|
84
83
|
if (!config)
|
|
85
|
-
|
|
84
|
+
return null;
|
|
86
85
|
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
86
|
const res = await postJsonForResponse(`${config.backendUrl}/api/cli/skill-discovery`, { q: prompt }, config.token);
|
|
91
87
|
const skills = res?.skills ?? [];
|
|
92
|
-
|
|
93
|
-
if (additionalContext) {
|
|
94
|
-
process.stdout.write(JSON.stringify({
|
|
95
|
-
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
|
|
96
|
-
}));
|
|
97
|
-
}
|
|
88
|
+
return buildDiscoveryContext(skills);
|
|
98
89
|
}
|
|
99
90
|
catch {
|
|
100
|
-
// silent — discovery is best-effort
|
|
101
91
|
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
async function runDiscover(prompt) {
|
|
95
|
+
const additionalContext = await discoverContext(prompt);
|
|
96
|
+
if (additionalContext)
|
|
97
|
+
process.stdout.write(JSON.stringify({
|
|
98
|
+
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext },
|
|
99
|
+
}));
|
|
102
100
|
process.exit(0);
|
|
103
101
|
}
|
|
104
102
|
function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
|
|
@@ -115,9 +113,6 @@ function readTranscriptTailFile(transcriptPath, maxBytes = 512 * 1024) {
|
|
|
115
113
|
fs.closeSync(handle);
|
|
116
114
|
}
|
|
117
115
|
}
|
|
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
116
|
async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
|
|
122
117
|
const config = (0, utils_1.readConfig)();
|
|
123
118
|
if (!config)
|
|
@@ -126,12 +121,9 @@ async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
|
|
|
126
121
|
const tp = hookInput.transcript_path;
|
|
127
122
|
if (tp) {
|
|
128
123
|
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
124
|
transcript = readTranscriptTailFile(tp);
|
|
132
125
|
}
|
|
133
126
|
catch {
|
|
134
|
-
/* unreadable transcript — fall through to minimal event */
|
|
135
127
|
}
|
|
136
128
|
}
|
|
137
129
|
try {
|
|
@@ -144,23 +136,12 @@ async function readAndPostLimitEvent(hookInput, source, emitMinimalOnMiss) {
|
|
|
144
136
|
});
|
|
145
137
|
}
|
|
146
138
|
catch {
|
|
147
|
-
/* best-effort */
|
|
148
139
|
}
|
|
149
140
|
}
|
|
150
141
|
async function runStopFailure(hookInput) {
|
|
151
142
|
await readAndPostLimitEvent(hookInput, "claude-code", true);
|
|
152
143
|
process.exit(0);
|
|
153
144
|
}
|
|
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
145
|
async function reportCodexLimitFromFile(config, transcriptPath, sessionId) {
|
|
165
146
|
const hit = (0, codexLimitScan_1.scanCodexRolloutForLimit)(transcriptPath);
|
|
166
147
|
if (!hit)
|
|
@@ -171,8 +152,6 @@ async function reportCodexLimitFromFile(config, transcriptPath, sessionId) {
|
|
|
171
152
|
sessionId: sessionId ?? (0, codexLimitScan_1.sessionIdFromRolloutPath)(transcriptPath),
|
|
172
153
|
transcriptTail: (0, codexLimitScan_1.readTailEndingAt)(transcriptPath, hit.lineEnd),
|
|
173
154
|
});
|
|
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
155
|
(0, codexLimitScan_1.recordCodexLimitReported)(transcriptPath, hit.event.at);
|
|
177
156
|
return true;
|
|
178
157
|
}
|
|
@@ -203,10 +182,6 @@ async function runCodexLimit(hookInput) {
|
|
|
203
182
|
process.exit(0);
|
|
204
183
|
}
|
|
205
184
|
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
185
|
async function runCodexLimitSweep(sessionId) {
|
|
211
186
|
const config = (0, utils_1.readConfig)();
|
|
212
187
|
if (!config)
|
|
@@ -214,12 +189,7 @@ async function runCodexLimitSweep(sessionId) {
|
|
|
214
189
|
try {
|
|
215
190
|
const posted = await (0, codexLimitScan_1.sweepCodexLimits)({
|
|
216
191
|
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
192
|
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
193
|
findOptions: { withinMs: 24 * 3600_000 },
|
|
224
194
|
scanOptions: { maxScanBytes: 4 * 1024 * 1024 },
|
|
225
195
|
});
|
|
@@ -241,10 +211,6 @@ async function runCodexLimitSweep(sessionId) {
|
|
|
241
211
|
});
|
|
242
212
|
}
|
|
243
213
|
}
|
|
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
214
|
async function postSessionSkip(config, sessionId, reason) {
|
|
249
215
|
if (!sessionId)
|
|
250
216
|
return;
|
|
@@ -252,17 +218,12 @@ async function postSessionSkip(config, sessionId, reason) {
|
|
|
252
218
|
await postJson(`${config.backendUrl}/api/cli/sessionSkip`, { sessionId, reason, at: Date.now() }, config.token, PING_TIMEOUT_MS);
|
|
253
219
|
}
|
|
254
220
|
catch {
|
|
255
|
-
/* best-effort */
|
|
256
221
|
}
|
|
257
222
|
}
|
|
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
223
|
async function runSessionStart(hookInput, source = "claude-code") {
|
|
263
224
|
const config = (0, utils_1.readConfig)();
|
|
264
225
|
if (!config)
|
|
265
|
-
|
|
226
|
+
return;
|
|
266
227
|
if (hookInput.session_id) {
|
|
267
228
|
try {
|
|
268
229
|
await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
|
|
@@ -272,18 +233,11 @@ async function runSessionStart(hookInput, source = "claude-code") {
|
|
|
272
233
|
}, config.token, PING_TIMEOUT_MS);
|
|
273
234
|
}
|
|
274
235
|
catch {
|
|
275
|
-
/* best-effort — never block session startup */
|
|
276
236
|
}
|
|
277
237
|
}
|
|
278
|
-
|
|
238
|
+
return;
|
|
279
239
|
}
|
|
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) {
|
|
240
|
+
function postJson(url, body, bearerToken, timeoutMs) {
|
|
287
241
|
return new Promise((resolve, reject) => {
|
|
288
242
|
const parsed = new URL(url);
|
|
289
243
|
const data = JSON.stringify(body);
|
|
@@ -324,9 +278,6 @@ timeoutMs) {
|
|
|
324
278
|
req.end();
|
|
325
279
|
});
|
|
326
280
|
}
|
|
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
281
|
function postJsonForResponse(url, body, bearerToken, timeoutMs) {
|
|
331
282
|
return new Promise((resolve, reject) => {
|
|
332
283
|
const parsed = new URL(url);
|
|
@@ -370,13 +321,10 @@ function postJsonForResponse(url, body, bearerToken, timeoutMs) {
|
|
|
370
321
|
req.end();
|
|
371
322
|
});
|
|
372
323
|
}
|
|
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
324
|
async function runSecurityHook(hookInput, phase, source, uploadTelemetry = true) {
|
|
377
325
|
const config = (0, utils_1.readConfig)();
|
|
378
326
|
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
379
|
-
return;
|
|
327
|
+
return false;
|
|
380
328
|
const policy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
|
|
381
329
|
const sessionId = hookInput.session_id ?? hookInput.sessionId;
|
|
382
330
|
const sessionSignal = phase === "pre" ? (0, securitySession_1.readSessionSecuritySignal)(sessionId) : undefined;
|
|
@@ -400,20 +348,21 @@ async function runSecurityHook(hookInput, phase, source, uploadTelemetry = true)
|
|
|
400
348
|
dispatchSecurityEventUpload(event);
|
|
401
349
|
const response = source === "cursor"
|
|
402
350
|
? (0, cursorHooks_1.cursorPreToolBlockResponse)(localDecision.reason ?? "Codie blocked this action")
|
|
403
|
-
:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
351
|
+
: source === "copilot"
|
|
352
|
+
? (0, copilotHooks_1.copilotPreToolBlockResponse)(localDecision.reason ?? "Codie blocked this action")
|
|
353
|
+
: {
|
|
354
|
+
hookSpecificOutput: {
|
|
355
|
+
hookEventName: "PreToolUse",
|
|
356
|
+
permissionDecision: "deny",
|
|
357
|
+
permissionDecisionReason: localDecision.reason,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
410
360
|
process.stdout.write(JSON.stringify(response));
|
|
411
|
-
return;
|
|
361
|
+
return true;
|
|
412
362
|
}
|
|
413
|
-
if (
|
|
414
|
-
return;
|
|
415
|
-
if (uploadTelemetry)
|
|
363
|
+
if (event && uploadTelemetry)
|
|
416
364
|
dispatchSecurityEventUpload(event);
|
|
365
|
+
return false;
|
|
417
366
|
}
|
|
418
367
|
function runPromptSecurityHook(hookInput, source, uploadTelemetry = true) {
|
|
419
368
|
const config = (0, utils_1.readConfig)();
|
|
@@ -431,10 +380,12 @@ function runPromptSecurityHook(hookInput, source, uploadTelemetry = true) {
|
|
|
431
380
|
if (uploadTelemetry) {
|
|
432
381
|
dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
|
|
433
382
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
383
|
+
if (source !== "copilot") {
|
|
384
|
+
const response = source === "cursor"
|
|
385
|
+
? (0, cursorHooks_1.cursorPromptBlockResponse)(decision.reason ?? "Codie blocked this prompt")
|
|
386
|
+
: { decision: "block", reason: decision.reason };
|
|
387
|
+
process.stdout.write(JSON.stringify(response));
|
|
388
|
+
}
|
|
438
389
|
return true;
|
|
439
390
|
}
|
|
440
391
|
function dispatchAllowedPromptSecurityEvent(hookInput, source) {
|
|
@@ -457,7 +408,6 @@ function dispatchSecurityEventUpload(event) {
|
|
|
457
408
|
child.unref();
|
|
458
409
|
}
|
|
459
410
|
catch {
|
|
460
|
-
// Telemetry is best-effort and must never delay local enforcement.
|
|
461
411
|
}
|
|
462
412
|
}
|
|
463
413
|
async function runSecurityEventUpload() {
|
|
@@ -476,11 +426,7 @@ async function runSecurityEventUpload() {
|
|
|
476
426
|
securityRuntimeVersion: security_1.SECURITY_RUNTIME_VERSION,
|
|
477
427
|
securityPolicyVersion: cachedPolicy?.policyVersion,
|
|
478
428
|
});
|
|
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);
|
|
429
|
+
const result = await postJsonForResponse(`${config.backendUrl}/api/cli/securityEvent`, { ...eventWithEndpoint }, config.token, 5_000);
|
|
484
430
|
if (result?.policy?.schemaVersion === 1) {
|
|
485
431
|
(0, securityPolicy_1.writeEndpointSecurityPolicy)(result.policy, undefined, undefined, config.companyId);
|
|
486
432
|
}
|
|
@@ -500,8 +446,6 @@ async function runSecurityEventUpload() {
|
|
|
500
446
|
});
|
|
501
447
|
}
|
|
502
448
|
}
|
|
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
449
|
function postBytes(url, bytes, contentType) {
|
|
506
450
|
return new Promise((resolve, reject) => {
|
|
507
451
|
const parsed = new URL(url);
|
|
@@ -537,9 +481,6 @@ function postBytes(url, bytes, contentType) {
|
|
|
537
481
|
req.end();
|
|
538
482
|
});
|
|
539
483
|
}
|
|
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
484
|
const PING_TIMEOUT_MS = 3000;
|
|
544
485
|
async function readStdin() {
|
|
545
486
|
return new Promise((resolve, reject) => {
|
|
@@ -557,6 +498,8 @@ function resolveMode(arg) {
|
|
|
557
498
|
return "codex";
|
|
558
499
|
if (arg === "capture-cursor" || arg === "backup-cursor")
|
|
559
500
|
return "cursor";
|
|
501
|
+
if (arg === "capture-copilot" || arg === "backup-copilot")
|
|
502
|
+
return "copilot";
|
|
560
503
|
if (arg === "capture-opencode" || arg === "capture-opencode-final")
|
|
561
504
|
return "opencode";
|
|
562
505
|
if (arg === "capture-desktop" || arg === "capture-desktop-final")
|
|
@@ -564,43 +507,23 @@ function resolveMode(arg) {
|
|
|
564
507
|
return "claude-code";
|
|
565
508
|
}
|
|
566
509
|
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
510
|
function makeUploadDeps() {
|
|
571
511
|
return {
|
|
572
512
|
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
513
|
requestJson: postJsonForResponse,
|
|
578
514
|
uploadBytes: postBytes,
|
|
579
515
|
readFileSync: fs.readFileSync,
|
|
580
516
|
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
517
|
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
518
|
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
519
|
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
520
|
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
521
|
shouldSkipJob: (job) => {
|
|
596
522
|
const config = (0, utils_1.readConfig)();
|
|
597
523
|
return (0, captureExclusions_1.shouldSkipCaptureUploadJob)(job, config);
|
|
598
524
|
},
|
|
599
525
|
};
|
|
600
526
|
}
|
|
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
527
|
if (process.argv[2] === "upload") {
|
|
605
528
|
const jobFile = process.argv[3];
|
|
606
529
|
if (jobFile) {
|
|
@@ -608,10 +531,6 @@ async function main() {
|
|
|
608
531
|
}
|
|
609
532
|
process.exit(0);
|
|
610
533
|
}
|
|
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
534
|
if (process.argv[2] === "upload-deferred") {
|
|
616
535
|
const jobFile = process.argv[3];
|
|
617
536
|
const delayMs = Math.max(0, Number(process.argv[4]) || 0);
|
|
@@ -623,9 +542,6 @@ async function main() {
|
|
|
623
542
|
}
|
|
624
543
|
process.exit(0);
|
|
625
544
|
}
|
|
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
545
|
if (process.argv[2] === "security-inventory-upload") {
|
|
630
546
|
const config = (0, utils_1.readConfig)();
|
|
631
547
|
if (config && (0, securityFeature_1.isAgentSecurityEnabled)(config)) {
|
|
@@ -633,25 +549,15 @@ async function main() {
|
|
|
633
549
|
try {
|
|
634
550
|
await (0, securityInventory_1.uploadSecurityInventory)(inventory, config);
|
|
635
551
|
}
|
|
636
|
-
catch {
|
|
552
|
+
catch { }
|
|
637
553
|
}
|
|
638
554
|
process.exit(0);
|
|
639
555
|
}
|
|
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
556
|
if (process.argv[2] === "backup-upload") {
|
|
644
557
|
const { runBackupUpload } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
|
|
645
558
|
await runBackupUpload(process.argv[3]);
|
|
646
559
|
process.exit(0);
|
|
647
560
|
}
|
|
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
561
|
if (process.argv[2] === "capture-finish") {
|
|
656
562
|
const jobFile = process.argv[3];
|
|
657
563
|
if (jobFile) {
|
|
@@ -660,26 +566,28 @@ async function main() {
|
|
|
660
566
|
await runCaptureFinish(job);
|
|
661
567
|
}
|
|
662
568
|
catch {
|
|
663
|
-
// best-effort — never throw out of the detached child
|
|
664
569
|
}
|
|
665
570
|
finally {
|
|
666
571
|
try {
|
|
667
572
|
fs.unlinkSync(jobFile);
|
|
668
573
|
}
|
|
669
574
|
catch {
|
|
670
|
-
/* ignore */
|
|
671
575
|
}
|
|
672
576
|
}
|
|
673
577
|
}
|
|
674
578
|
process.exit(0);
|
|
675
579
|
}
|
|
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
580
|
if (process.argv[2] === "security-upload") {
|
|
680
581
|
await runSecurityEventUpload();
|
|
681
582
|
process.exit(0);
|
|
682
583
|
}
|
|
584
|
+
if (process.argv[2] === "self-update") {
|
|
585
|
+
await (0, selfUpdate_1.runSelfUpdate)(process.argv[1]);
|
|
586
|
+
process.exit(0);
|
|
587
|
+
}
|
|
588
|
+
const migrated = process.env.CODIEDEV_VIA_LAUNCHER ? [] : (0, utils_1.migrateHooksToLauncher)();
|
|
589
|
+
if (migrated.length === 0)
|
|
590
|
+
(0, selfUpdate_1.maybeSpawnSelfUpdate)(process.argv[1]);
|
|
683
591
|
let hookInput = {};
|
|
684
592
|
try {
|
|
685
593
|
const raw = await readStdin();
|
|
@@ -687,18 +595,17 @@ async function main() {
|
|
|
687
595
|
const parsed = JSON.parse(raw);
|
|
688
596
|
hookInput = (process.argv[2]?.endsWith("-cursor")
|
|
689
597
|
? (0, cursorHooks_1.normalizeCursorHookInput)(parsed)
|
|
690
|
-
:
|
|
598
|
+
: process.argv[2]?.endsWith("-copilot")
|
|
599
|
+
? (0, copilotHooks_1.normalizeCopilotHookInput)(parsed)
|
|
600
|
+
: parsed);
|
|
691
601
|
}
|
|
692
602
|
}
|
|
693
603
|
catch {
|
|
694
604
|
process.exit(0);
|
|
695
605
|
}
|
|
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
606
|
const captureIgnored = (0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput);
|
|
700
607
|
const securityCommand = process.argv[2] ?? "";
|
|
701
|
-
const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|cursor|codie))?$/);
|
|
608
|
+
const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|cursor|codie|copilot))?$/);
|
|
702
609
|
if (securityMatch) {
|
|
703
610
|
const source = securityMatch[2] === "codex"
|
|
704
611
|
? "codex"
|
|
@@ -706,11 +613,19 @@ async function main() {
|
|
|
706
613
|
? "cursor"
|
|
707
614
|
: securityMatch[2] === "codie"
|
|
708
615
|
? "codie-harness"
|
|
709
|
-
: "
|
|
710
|
-
|
|
616
|
+
: securityMatch[2] === "copilot"
|
|
617
|
+
? "copilot"
|
|
618
|
+
: "claude-code";
|
|
619
|
+
const calls = hookInput.copilotToolCalls;
|
|
620
|
+
const inputs = calls?.length
|
|
621
|
+
? calls.map((call) => ({ ...hookInput, tool_name: call.name, tool_input: call.input, tool_use_id: call.id }))
|
|
622
|
+
: [hookInput];
|
|
623
|
+
for (const input of inputs) {
|
|
624
|
+
if (await runSecurityHook(input, securityMatch[1], source, !captureIgnored))
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
711
627
|
process.exit(0);
|
|
712
628
|
}
|
|
713
|
-
// UserPromptSubmit discovery path — separate from session capture.
|
|
714
629
|
const promptCommand = process.argv[2] ?? "";
|
|
715
630
|
if (promptCommand === "security-prompt-cursor") {
|
|
716
631
|
if (runPromptSecurityHook(hookInput, "cursor", !captureIgnored))
|
|
@@ -722,29 +637,51 @@ async function main() {
|
|
|
722
637
|
}
|
|
723
638
|
if (process.argv[2] === "discover" ||
|
|
724
639
|
process.argv[2] === "discover-codex" ||
|
|
725
|
-
process.argv[2] === "discover-codie"
|
|
640
|
+
process.argv[2] === "discover-codie" ||
|
|
641
|
+
process.argv[2] === "discover-copilot") {
|
|
726
642
|
const source = promptCommand === "discover-codex"
|
|
727
643
|
? "codex"
|
|
728
644
|
: promptCommand === "discover-codie"
|
|
729
645
|
? "codie-harness"
|
|
730
|
-
: "
|
|
731
|
-
|
|
732
|
-
|
|
646
|
+
: promptCommand === "discover-copilot"
|
|
647
|
+
? "copilot"
|
|
648
|
+
: "claude-code";
|
|
733
649
|
if (runPromptSecurityHook(hookInput, source, !captureIgnored))
|
|
734
650
|
return;
|
|
735
651
|
if (captureIgnored)
|
|
736
652
|
return;
|
|
737
653
|
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
654
|
if (source === "codex")
|
|
741
655
|
await runCodexLimitSweep(hookInput.session_id);
|
|
742
|
-
|
|
656
|
+
if (source === "codex")
|
|
657
|
+
(0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
|
|
658
|
+
if (source !== "copilot")
|
|
659
|
+
await runDiscover(hookInput.prompt);
|
|
743
660
|
return;
|
|
744
661
|
}
|
|
745
662
|
if (captureIgnored)
|
|
746
663
|
process.exit(0);
|
|
747
|
-
|
|
664
|
+
if (["session-start-cursor", "backup-cursor", "capture-cursor", "observe-cursor"].includes(process.argv[2])) {
|
|
665
|
+
try {
|
|
666
|
+
(0, cursorLifecycle_1.recordCursorLifecycle)(hookInput, process.argv[2] === "capture-cursor");
|
|
667
|
+
}
|
|
668
|
+
catch { }
|
|
669
|
+
}
|
|
670
|
+
if (process.argv[2] === "observe-cursor") {
|
|
671
|
+
process.stdout.write("{}");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (process.argv[2] === "limit-copilot") {
|
|
675
|
+
const config = (0, utils_1.readConfig)();
|
|
676
|
+
const event = (0, limitEvent_1.parseNativeLimitError)(hookInput);
|
|
677
|
+
if (config && event) {
|
|
678
|
+
try {
|
|
679
|
+
await (0, limitEventReporter_1.postLimitEvent)(config, { ...event, source: "copilot", sessionId: hookInput.session_id });
|
|
680
|
+
}
|
|
681
|
+
catch { }
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
748
685
|
if (process.argv[2] === "limit") {
|
|
749
686
|
await runStopFailure(hookInput);
|
|
750
687
|
return;
|
|
@@ -753,9 +690,9 @@ async function main() {
|
|
|
753
690
|
await runCodexLimit(hookInput);
|
|
754
691
|
return;
|
|
755
692
|
}
|
|
756
|
-
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
757
693
|
if (process.argv[2] === "session-start" ||
|
|
758
694
|
process.argv[2] === "session-start-cursor" ||
|
|
695
|
+
process.argv[2] === "session-start-copilot" ||
|
|
759
696
|
process.argv[2] === "session-start-opencode") {
|
|
760
697
|
if ((0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()) && (0, securityInventory_1.securityInventoryDue)()) {
|
|
761
698
|
try {
|
|
@@ -765,97 +702,88 @@ async function main() {
|
|
|
765
702
|
});
|
|
766
703
|
child.unref();
|
|
767
704
|
}
|
|
768
|
-
catch {
|
|
705
|
+
catch { }
|
|
769
706
|
}
|
|
707
|
+
(0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
|
|
708
|
+
const packNag = process.argv[2] === "session-start-opencode" ? null : (0, sync_1.packNagLine)();
|
|
709
|
+
if (packNag && process.argv[2] === "session-start")
|
|
710
|
+
process.stdout.write(`${packNag}\n`);
|
|
770
711
|
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
771
712
|
? "opencode"
|
|
772
713
|
: process.argv[2] === "session-start-cursor"
|
|
773
714
|
? "cursor"
|
|
774
|
-
: "
|
|
715
|
+
: process.argv[2] === "session-start-copilot"
|
|
716
|
+
? "copilot"
|
|
717
|
+
: "claude-code");
|
|
718
|
+
if ((0, utils_1.readConfig)() && (process.argv[2] === "session-start-cursor" || process.argv[2] === "session-start-copilot")) {
|
|
719
|
+
const source = process.argv[2] === "session-start-cursor" ? "cursor" : "copilot";
|
|
720
|
+
const { TEAM_SKILL_CONTEXT, teamSkillSessionOutput } = await Promise.resolve().then(() => __importStar(require("./teamSkillContext")));
|
|
721
|
+
const prompt = hookInput.initialPrompt
|
|
722
|
+
?? hookInput.initial_prompt;
|
|
723
|
+
const discovered = typeof prompt === "string" ? await discoverContext(prompt) : null;
|
|
724
|
+
process.stdout.write(JSON.stringify(teamSkillSessionOutput(source, [TEAM_SKILL_CONTEXT, discovered, packNag].filter(Boolean).join("\n\n"))));
|
|
725
|
+
}
|
|
775
726
|
return;
|
|
776
727
|
}
|
|
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
728
|
if ((process.argv[2] ?? "").endsWith("-cursor")) {
|
|
783
729
|
(0, utils_1.ensureCursorCodiedevHooksInstalled)();
|
|
784
730
|
}
|
|
731
|
+
else if ((process.argv[2] ?? "").endsWith("-copilot")) {
|
|
732
|
+
(0, utils_1.ensureCopilotCodiedevHooksInstalled)();
|
|
733
|
+
}
|
|
785
734
|
else {
|
|
786
735
|
(0, utils_1.ensureAllCodiedevHooksInstalled)();
|
|
787
736
|
}
|
|
788
737
|
if (process.argv[2] === "capture-codex")
|
|
789
738
|
(0, utils_1.ensureCodexLimitHookInstalled)();
|
|
790
739
|
const { session_id, transcript_path, cwd, workspace_roots } = hookInput;
|
|
740
|
+
const launch = (0, launchContext_1.hookLaunch)(process.argv[2], hookInput, {
|
|
741
|
+
copilotClientName: process.argv[2]?.endsWith("-copilot")
|
|
742
|
+
? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path).clientName
|
|
743
|
+
: undefined,
|
|
744
|
+
});
|
|
791
745
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
792
746
|
? workspace_roots[0]
|
|
793
747
|
: cwd || process.cwd();
|
|
794
|
-
|
|
795
|
-
|
|
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) {
|
|
748
|
+
if (process.argv[2] === "backup" || process.argv[2] === "backup-cursor" || process.argv[2] === "backup-copilot") {
|
|
749
|
+
if (transcript_path && !(0, utils_1.isCodexRolloutPath)(transcript_path)) {
|
|
804
750
|
try {
|
|
805
751
|
(0, utils_1.dispatchCaptureFinish)({
|
|
806
|
-
mode: process.argv[2] === "backup-cursor"
|
|
752
|
+
mode: process.argv[2] === "backup-cursor"
|
|
753
|
+
? "capture-cursor"
|
|
754
|
+
: process.argv[2] === "backup-copilot"
|
|
755
|
+
? "capture-copilot"
|
|
756
|
+
: "capture",
|
|
807
757
|
session_id,
|
|
808
758
|
transcript_path,
|
|
809
759
|
cwd,
|
|
810
760
|
workspace_roots,
|
|
811
|
-
|
|
761
|
+
...(launch ? { launch } : {}),
|
|
812
762
|
final: false,
|
|
813
763
|
}, process.argv[1], {
|
|
814
764
|
spawn: child_process_1.spawn,
|
|
815
765
|
writeFileSync: fs.writeFileSync,
|
|
816
766
|
openSync: fs.openSync,
|
|
817
767
|
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
768
|
tmpFile: (0, utils_1.finishInputTmpFile)(session_id),
|
|
823
769
|
});
|
|
824
770
|
}
|
|
825
771
|
catch {
|
|
826
|
-
// capture is best-effort; never break backup
|
|
827
772
|
}
|
|
828
773
|
}
|
|
829
774
|
const { runBackup } = await Promise.resolve().then(() => __importStar(require("./backup/run")));
|
|
830
775
|
runBackup(workspaceRoot, process.argv[1]);
|
|
831
776
|
process.exit(0);
|
|
832
777
|
}
|
|
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
778
|
const config = (0, utils_1.readConfig)();
|
|
843
779
|
if (!config) {
|
|
844
|
-
// No config = no token; nothing can be sent, not even a skip ping.
|
|
845
780
|
process.exit(0);
|
|
846
781
|
}
|
|
847
782
|
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
783
|
await postSessionSkip(config, session_id, "no-transcript-path");
|
|
852
784
|
process.exit(0);
|
|
853
785
|
}
|
|
854
786
|
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
787
|
const jobFile = (0, utils_1.finishInputTmpFile)(session_id);
|
|
860
788
|
(0, utils_1.dispatchCaptureFinish)({
|
|
861
789
|
mode: process.argv[2] ?? "capture",
|
|
@@ -863,9 +791,7 @@ async function main() {
|
|
|
863
791
|
transcript_path,
|
|
864
792
|
cwd,
|
|
865
793
|
workspace_roots,
|
|
866
|
-
|
|
867
|
-
// Codex routes its per-turn Stop hook through "capture-codex" —
|
|
868
|
-
// final:true there would close the server lease every turn.
|
|
794
|
+
...(launch ? { launch } : {}),
|
|
869
795
|
final: (0, utils_1.resolveCaptureFinal)(process.argv[2]),
|
|
870
796
|
}, process.argv[1], { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile: jobFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
871
797
|
}
|
|
@@ -874,19 +800,10 @@ async function main() {
|
|
|
874
800
|
}
|
|
875
801
|
process.exit(0);
|
|
876
802
|
}
|
|
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
803
|
async function runCaptureFinish(input) {
|
|
885
804
|
const config = (0, utils_1.readConfig)();
|
|
886
805
|
if (!config)
|
|
887
806
|
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
807
|
if ((0, captureExclusions_1.shouldIgnoreCaptureInput)(config, input)) {
|
|
891
808
|
(0, captureLog_1.appendCaptureLog)({
|
|
892
809
|
stage: "capture-finish",
|
|
@@ -901,8 +818,6 @@ async function runCaptureFinish(input) {
|
|
|
901
818
|
await postSessionSkip(config, session_id, "no-transcript-path");
|
|
902
819
|
return;
|
|
903
820
|
}
|
|
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
821
|
(0, captureLog_1.appendCaptureLog)({ stage: "capture-finish", sessionId: session_id, outcome: "start" });
|
|
907
822
|
try {
|
|
908
823
|
await runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots);
|
|
@@ -918,10 +833,6 @@ async function runCaptureFinish(input) {
|
|
|
918
833
|
}
|
|
919
834
|
async function runCaptureFinishInner(input, config, session_id, transcript_path, cwd, workspace_roots) {
|
|
920
835
|
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
836
|
const parserMode = mode === "desktop" ? "opencode" : mode;
|
|
926
837
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
927
838
|
? workspace_roots[0]
|
|
@@ -930,11 +841,14 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
930
841
|
let transcriptContent;
|
|
931
842
|
try {
|
|
932
843
|
transcriptContent = fs.readFileSync(transcript_path, "utf8");
|
|
844
|
+
if (mode === "copilot" && input.final === true && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent)) {
|
|
845
|
+
for (let waited = 0; waited < COPILOT_SHUTDOWN_WAIT_MS && !(0, copilotHooks_1.hasCopilotShutdown)(transcriptContent); waited += 250) {
|
|
846
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
847
|
+
transcriptContent = fs.readFileSync(transcript_path, "utf8");
|
|
848
|
+
}
|
|
849
|
+
}
|
|
933
850
|
}
|
|
934
851
|
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
852
|
const reason = (0, utils_1.transcriptReadSkipReason)(err);
|
|
939
853
|
(0, captureLog_1.appendCaptureLog)({
|
|
940
854
|
stage: "capture-finish",
|
|
@@ -947,98 +861,88 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
947
861
|
}
|
|
948
862
|
return;
|
|
949
863
|
}
|
|
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
864
|
const rawTranscriptContent = transcriptContent;
|
|
865
|
+
let copilotSubagents;
|
|
866
|
+
const copilotWorkspace = mode === "copilot" ? (0, copilotHooks_1.readCopilotWorkspace)(transcript_path) : {};
|
|
956
867
|
if (parserMode === "opencode") {
|
|
957
868
|
transcriptContent = (0, utils_1.normalizeOpenCodeTranscript)(transcriptContent);
|
|
958
869
|
}
|
|
959
870
|
else if (parserMode === "cursor") {
|
|
960
871
|
transcriptContent = (0, cursorHooks_1.normalizeCursorTranscript)(transcriptContent);
|
|
872
|
+
const observations = (0, cursorLifecycle_1.cursorLifecycleRecords)(session_id);
|
|
873
|
+
if (observations)
|
|
874
|
+
transcriptContent += `\n${observations}`;
|
|
875
|
+
}
|
|
876
|
+
else if (parserMode === "copilot") {
|
|
877
|
+
const normalized = (0, copilotHooks_1.normalizeCopilotTranscript)(transcriptContent, { clientName: copilotWorkspace.clientName });
|
|
878
|
+
transcriptContent = normalized.content;
|
|
879
|
+
copilotSubagents = normalized.subagents.count > 0 ? normalized.subagents : undefined;
|
|
880
|
+
}
|
|
881
|
+
if (mode === "cursor" || mode === "copilot") {
|
|
882
|
+
try {
|
|
883
|
+
await (0, limitEventReporter_1.reportTranscriptLimitEvent)({ config, source: mode, sessionId: session_id, transcript: transcriptContent });
|
|
884
|
+
}
|
|
885
|
+
catch { }
|
|
961
886
|
}
|
|
962
|
-
// Resolve repo URL. Codex embeds it in session_meta; CC and Cursor both
|
|
963
|
-
// need the git remote of the workspace.
|
|
964
887
|
let remoteUrl = null;
|
|
965
888
|
if (parserMode === "codex") {
|
|
966
889
|
remoteUrl = (0, utils_1.extractCodexRepoUrl)(transcriptContent);
|
|
967
890
|
}
|
|
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
891
|
const captureDirs = (0, utils_1.resolveCaptureDirs)(transcriptContent, workspaceRoot);
|
|
973
892
|
if (!remoteUrl) {
|
|
974
893
|
remoteUrl = (0, utils_1.resolveCaptureRemoteUrl)(workspaceRoot, captureDirs);
|
|
975
894
|
}
|
|
976
|
-
|
|
977
|
-
|
|
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.
|
|
895
|
+
if (remoteUrl)
|
|
896
|
+
remoteUrl = (0, utils_1.stripRepoUrlCredentials)(remoteUrl);
|
|
985
897
|
const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
|
|
986
898
|
const stats = parserMode === "codex"
|
|
987
899
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
988
900
|
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
901
|
+
const emptyReason = parserMode === "cursor" && (0, cursorHooks_1.isCursorTranscriptEmpty)(rawTranscriptContent)
|
|
902
|
+
? "empty-cursor-transcript"
|
|
903
|
+
: parserMode === "copilot" && (0, copilotHooks_1.isCopilotTranscriptEmpty)(rawTranscriptContent)
|
|
904
|
+
? "empty-copilot-transcript"
|
|
905
|
+
: null;
|
|
906
|
+
if (emptyReason) {
|
|
994
907
|
(0, captureLog_1.appendCaptureLog)({
|
|
995
908
|
stage: "capture-finish",
|
|
996
909
|
sessionId: session_id,
|
|
997
910
|
outcome: "skip",
|
|
998
|
-
detail:
|
|
911
|
+
detail: emptyReason,
|
|
999
912
|
});
|
|
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
913
|
if ((0, utils_1.shouldReportCaptureSkip)(input.final)) {
|
|
1004
|
-
await postSessionSkip(config, session_id,
|
|
914
|
+
await postSessionSkip(config, session_id, emptyReason);
|
|
1005
915
|
}
|
|
1006
916
|
return;
|
|
1007
917
|
}
|
|
1008
918
|
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
919
|
const keys = (0, utils_1.collectCaptureKeys)(captureDirs);
|
|
1017
920
|
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
921
|
const authoredBranches = (0, utils_1.parseAuthoredBranches)(transcriptContent);
|
|
1026
|
-
const
|
|
922
|
+
const recordedBranches = copilotWorkspace.branch ? [copilotWorkspace.branch] : [];
|
|
923
|
+
const branches = [...new Set([...keys.branches, ...authoredBranches, ...recordedBranches])];
|
|
1027
924
|
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
925
|
const authoredShas = (0, utils_1.parseAuthoredShas)(transcriptContent, workspaceRoot);
|
|
1033
926
|
const headShas = [...new Set([...keys.headShas, ...authoredShas])];
|
|
1034
927
|
const headSha = keys.headSha ?? headShas[0] ?? null;
|
|
1035
928
|
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
929
|
const usage = (0, utils_1.parseUsage)(mode === "desktop" ? rawTranscriptContent : transcriptContent, mode);
|
|
930
|
+
const subagents = mode === "claude-code"
|
|
931
|
+
? (0, subagents_1.readSubagentTranscripts)(transcript_path, session_id)
|
|
932
|
+
: { contents: [], count: 0, truncated: false };
|
|
933
|
+
const subagentUsage = (0, subagents_1.foldSubagentModelUsage)([], subagents, mode);
|
|
934
|
+
for (const u of subagentUsage) {
|
|
935
|
+
usage.inputTokens = (usage.inputTokens ?? 0) + u.inputTokens;
|
|
936
|
+
usage.outputTokens = (usage.outputTokens ?? 0) + u.outputTokens;
|
|
937
|
+
usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + u.cacheReadTokens;
|
|
938
|
+
usage.cacheCreationTokens = (usage.cacheCreationTokens ?? 0) + u.cacheCreationTokens;
|
|
939
|
+
usage.cacheTokens = (usage.cacheTokens ?? 0) + u.cacheReadTokens + u.cacheCreationTokens;
|
|
940
|
+
}
|
|
941
|
+
const hasUsage = mode === "cursor" ? (0, cursorHooks_1.cursorTranscriptHasUsage)(transcriptContent)
|
|
942
|
+
: mode === "copilot" ? (0, transcriptModels_1.parseTranscriptModels)(transcriptContent, mode).some(model => model.tokensAvailable !== false)
|
|
943
|
+
: true;
|
|
944
|
+
if (mode === "cursor" && !usage.model)
|
|
945
|
+
usage.model = (0, cursorHooks_1.cursorObservedModel)(transcriptContent) ?? null;
|
|
1042
946
|
const sessionStartedAt = (0, utils_1.parseSessionStart)(transcriptContent);
|
|
1043
947
|
const steering = (0, utils_1.parseSteering)(transcriptContent, parserMode);
|
|
1044
948
|
const payload = (0, utils_1.buildCapturePayload)({
|
|
@@ -1057,33 +961,26 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1057
961
|
headShas,
|
|
1058
962
|
gitRefs,
|
|
1059
963
|
pullRequestUrls,
|
|
1060
|
-
inputTokens: usage.inputTokens,
|
|
1061
|
-
outputTokens: usage.outputTokens,
|
|
1062
|
-
cacheTokens: usage.cacheTokens,
|
|
1063
|
-
cacheReadTokens: usage.cacheReadTokens,
|
|
1064
|
-
cacheCreationTokens: usage.cacheCreationTokens,
|
|
964
|
+
inputTokens: hasUsage ? usage.inputTokens : undefined,
|
|
965
|
+
outputTokens: hasUsage ? usage.outputTokens : undefined,
|
|
966
|
+
cacheTokens: hasUsage ? usage.cacheTokens : undefined,
|
|
967
|
+
cacheReadTokens: hasUsage ? usage.cacheReadTokens : undefined,
|
|
968
|
+
cacheCreationTokens: hasUsage ? usage.cacheCreationTokens : undefined,
|
|
1065
969
|
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),
|
|
970
|
+
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),
|
|
971
|
+
modelUsage: (0, subagents_1.foldSubagentModelUsage)((0, transcriptModels_1.parseTranscriptModels)(mode === "desktop" || mode === "opencode" ? rawTranscriptContent : transcriptContent, mode), subagents, mode),
|
|
1068
972
|
sessionStartedAt,
|
|
1069
973
|
interruptCount: steering.interrupts,
|
|
1070
974
|
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
975
|
final: input.final,
|
|
1074
|
-
// Desktop-only measured spend (undefined for every other source).
|
|
1075
976
|
costUsd: usage.costUsd,
|
|
1076
977
|
});
|
|
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
978
|
const uploadState = (0, utils_1.readSessionUploadState)(session_id ?? "");
|
|
1080
979
|
const decision = (0, utils_1.decideDispatch)({
|
|
1081
980
|
final: input.final === true,
|
|
1082
981
|
lastUploadAt: uploadState?.lastUploadAt ?? null,
|
|
1083
982
|
now: Date.now(),
|
|
1084
983
|
});
|
|
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
984
|
(0, captureLog_1.appendCaptureLog)({
|
|
1088
985
|
stage: "capture-finish",
|
|
1089
986
|
sessionId: session_id,
|
|
@@ -1092,11 +989,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1092
989
|
? `dispatch=deferred delayMs=${decision.delayMs} final=${input.final === true} repo=${remoteUrl ?? "-"} branch=${branch ?? "-"} msgs=${stats.messageCount}`
|
|
1093
990
|
: `dispatch=immediate final=${input.final === true} repo=${remoteUrl ?? "-"} branch=${branch ?? "-"} msgs=${stats.messageCount}`,
|
|
1094
991
|
});
|
|
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
992
|
try {
|
|
1101
993
|
const hookScriptPath = process.argv[1];
|
|
1102
994
|
const tmpFile = (0, utils_1.captureUploadTmpFile)(session_id, decision.mode);
|
|
@@ -1107,21 +999,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1107
999
|
body: payload,
|
|
1108
1000
|
scopePaths,
|
|
1109
1001
|
}, 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
1002
|
if (session_id)
|
|
1114
1003
|
(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
1004
|
const orphanPings = [];
|
|
1126
1005
|
(0, utils_1.sweepPendingCaptures)({
|
|
1127
1006
|
dir: os.tmpdir(),
|
|
@@ -1133,9 +1012,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1133
1012
|
exclude: tmpFile,
|
|
1134
1013
|
openSync: fs.openSync,
|
|
1135
1014
|
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
1015
|
unlinkSync: fs.unlinkSync,
|
|
1140
1016
|
reportOrphan: (sid) => {
|
|
1141
1017
|
orphanPings.push(postSessionSkip(config, sid, "finish-orphaned"));
|
|
@@ -1144,9 +1020,6 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1144
1020
|
await Promise.all(orphanPings);
|
|
1145
1021
|
}
|
|
1146
1022
|
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
1023
|
(0, utils_1.dispatchDeferredUpload)({
|
|
1151
1024
|
url: `${config.backendUrl}/api/captureSession`,
|
|
1152
1025
|
token: config.token,
|
|
@@ -1159,6 +1032,10 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
1159
1032
|
process.stderr.write(`[codiedev-hook] Warning: Failed to dispatch session capture — ${err.message}\n`);
|
|
1160
1033
|
}
|
|
1161
1034
|
}
|
|
1035
|
+
const COPILOT_SHUTDOWN_WAIT_MS = 5000;
|
|
1036
|
+
function withCopilotSubagents(signals, subagents) {
|
|
1037
|
+
return subagents ? { ...signals, subagents } : signals;
|
|
1038
|
+
}
|
|
1162
1039
|
main().catch(() => {
|
|
1163
1040
|
process.exit(0);
|
|
1164
1041
|
});
|