codiedev 0.9.10 → 0.9.12
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/dist/hook.js +9 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +126 -11
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -263,7 +263,15 @@ async function main() {
|
|
|
263
263
|
// worktree's branch+sha so all the resulting PRs attribute. branch/headSha
|
|
264
264
|
// remain the primary (most-recent) values for back-compat.
|
|
265
265
|
const captureDirs = (0, utils_1.resolveCaptureDirs)(transcriptContent, workspaceRoot);
|
|
266
|
-
const
|
|
266
|
+
const keys = (0, utils_1.collectCaptureKeys)(captureDirs);
|
|
267
|
+
const { branch, branches } = keys;
|
|
268
|
+
// Union the worktree HEAD shas with the SHAs the agent actually committed/
|
|
269
|
+
// pushed (parsed from the transcript's git output, resolved via the shared
|
|
270
|
+
// object store). The authored shas are independent of end-of-session
|
|
271
|
+
// filesystem state, so they rescue attribution when a worktree is stale/gone.
|
|
272
|
+
const authoredShas = (0, utils_1.parseAuthoredShas)(transcriptContent, workspaceRoot);
|
|
273
|
+
const headShas = [...new Set([...keys.headShas, ...authoredShas])];
|
|
274
|
+
const headSha = keys.headSha ?? headShas[0] ?? null;
|
|
267
275
|
// Cost/velocity/steering signals parsed straight from the transcript we're
|
|
268
276
|
// already holding — no extra hooks, no extra git/process calls.
|
|
269
277
|
const usage = (0, utils_1.parseUsage)(transcriptContent, mode);
|
package/dist/utils.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export declare function collectCaptureKeys(dirs: string[], getBranch?: (d: strin
|
|
|
35
35
|
branches: string[];
|
|
36
36
|
headShas: string[];
|
|
37
37
|
};
|
|
38
|
+
export declare function extractShaTokens(transcriptContent: string): string[];
|
|
39
|
+
export declare function parseAuthoredShas(transcriptContent: string, repoRoot: string, resolveFull?: (sha: string) => string | null): string[];
|
|
38
40
|
export declare function getGitBranch(cwd: string): string | null;
|
|
39
41
|
export declare function getGitHeadSha(cwd: string): string | null;
|
|
40
42
|
export interface CapturePayloadInput {
|
package/dist/utils.js
CHANGED
|
@@ -44,6 +44,8 @@ exports.matchRepo = matchRepo;
|
|
|
44
44
|
exports.resolveCaptureDir = resolveCaptureDir;
|
|
45
45
|
exports.resolveCaptureDirs = resolveCaptureDirs;
|
|
46
46
|
exports.collectCaptureKeys = collectCaptureKeys;
|
|
47
|
+
exports.extractShaTokens = extractShaTokens;
|
|
48
|
+
exports.parseAuthoredShas = parseAuthoredShas;
|
|
47
49
|
exports.getGitBranch = getGitBranch;
|
|
48
50
|
exports.getGitHeadSha = getGitHeadSha;
|
|
49
51
|
exports.buildCapturePayload = buildCapturePayload;
|
|
@@ -283,35 +285,96 @@ function resolveCaptureDir(transcriptContent, workspaceRoot) {
|
|
|
283
285
|
// used (work done directly in the repo).
|
|
284
286
|
function resolveCaptureDirs(transcriptContent, workspaceRoot) {
|
|
285
287
|
const rootGitDir = resolveGitDir(workspaceRoot);
|
|
286
|
-
|
|
288
|
+
// Collect the folders the session WROTE to (not merely referenced), in order:
|
|
289
|
+
// - the session cwd
|
|
290
|
+
// - `git -C <path>` of MUTATING git commands (commit/push/add/...) — NOT
|
|
291
|
+
// read-only ones (log/status/diff), which would falsely credit a worktree
|
|
292
|
+
// the session only inspected
|
|
293
|
+
// - the dir of files edited via Edit/Write/NotebookEdit — NOT Read/Grep/Glob
|
|
294
|
+
// Reading/inspecting another worktree must not attribute its PRs to this
|
|
295
|
+
// session (that also corrupts the even cost-split).
|
|
296
|
+
const candidates = [];
|
|
297
|
+
const add = (p) => {
|
|
298
|
+
if (!p)
|
|
299
|
+
return;
|
|
300
|
+
candidates.push(path.isAbsolute(p) ? p : path.resolve(workspaceRoot, p));
|
|
301
|
+
};
|
|
287
302
|
for (const line of transcriptContent.split("\n")) {
|
|
288
303
|
if (!line.trim())
|
|
289
304
|
continue;
|
|
305
|
+
let entry;
|
|
290
306
|
try {
|
|
291
|
-
|
|
292
|
-
if (typeof entry?.cwd === "string" && entry.cwd)
|
|
293
|
-
cwds.push(entry.cwd);
|
|
307
|
+
entry = JSON.parse(line);
|
|
294
308
|
}
|
|
295
309
|
catch {
|
|
296
|
-
// non-JSON
|
|
310
|
+
// partial/non-JSON line — best-effort: only mutating git -C, never files.
|
|
311
|
+
for (const d of mutatingGitCDirs(line))
|
|
312
|
+
add(d);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (typeof entry?.cwd === "string" && entry.cwd)
|
|
316
|
+
add(entry.cwd);
|
|
317
|
+
const content = entry?.message?.content;
|
|
318
|
+
if (!Array.isArray(content))
|
|
319
|
+
continue;
|
|
320
|
+
for (const b of content) {
|
|
321
|
+
if (b?.type !== "tool_use")
|
|
322
|
+
continue;
|
|
323
|
+
if (b.name === "Bash" && typeof b.input?.command === "string") {
|
|
324
|
+
for (const d of mutatingGitCDirs(b.input.command))
|
|
325
|
+
add(d);
|
|
326
|
+
}
|
|
327
|
+
else if ((b.name === "Edit" || b.name === "Write" || b.name === "NotebookEdit") &&
|
|
328
|
+
typeof (b.input?.file_path ?? b.input?.notebook_path) === "string") {
|
|
329
|
+
add(path.dirname(b.input.file_path ?? b.input.notebook_path));
|
|
330
|
+
}
|
|
297
331
|
}
|
|
298
332
|
}
|
|
299
|
-
// Walk most-recent first;
|
|
300
|
-
//
|
|
301
|
-
//
|
|
333
|
+
// Walk most-recent first; one entry per distinct worktree gitdir (a worktree's
|
|
334
|
+
// gitdir is .git/worktrees/<name>; the root and its subdirs share .git, so
|
|
335
|
+
// they're excluded as not-separate). Normalize each to its canonical worktree
|
|
336
|
+
// root so a file path / subdir resolves to the worktree dir itself.
|
|
302
337
|
const out = [];
|
|
303
338
|
const seenGitDirs = new Set();
|
|
304
|
-
for (let i =
|
|
305
|
-
const gitDir = resolveGitDir(
|
|
339
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
340
|
+
const gitDir = resolveGitDir(candidates[i]);
|
|
306
341
|
if (!gitDir || (rootGitDir && gitDir === rootGitDir))
|
|
307
342
|
continue;
|
|
308
343
|
if (seenGitDirs.has(gitDir))
|
|
309
344
|
continue;
|
|
310
345
|
seenGitDirs.add(gitDir);
|
|
311
|
-
out.push(
|
|
346
|
+
out.push(worktreeRootFromGitDir(gitDir) ?? candidates[i]);
|
|
312
347
|
}
|
|
313
348
|
return out.length > 0 ? out : [workspaceRoot];
|
|
314
349
|
}
|
|
350
|
+
// Extract the `-C <path>` targets of MUTATING git commands in a shell string.
|
|
351
|
+
// Splits on command separators and only takes a sub-command's -C path when that
|
|
352
|
+
// sub-command runs a state-changing git verb — so `git -C x log` is ignored but
|
|
353
|
+
// `git -C x commit`/`push`/`add` count.
|
|
354
|
+
const MUTATING_GIT = /\b(?:commit|push|add|checkout|switch|merge|rebase|reset|restore|rm|mv|stash|apply|cherry-pick|am|tag|worktree)\b/;
|
|
355
|
+
function mutatingGitCDirs(command) {
|
|
356
|
+
const out = [];
|
|
357
|
+
for (const sub of command.split(/&&|\|\||;|\n/)) {
|
|
358
|
+
if (!/\bgit\b/.test(sub) || !MUTATING_GIT.test(sub))
|
|
359
|
+
continue;
|
|
360
|
+
const re = /-C\s+(?:"([^"]+)"|'([^']+)'|(\S+))/g;
|
|
361
|
+
let m;
|
|
362
|
+
while ((m = re.exec(sub)))
|
|
363
|
+
out.push(m[1] ?? m[2] ?? m[3]);
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
// A linked worktree's gitdir (`.git/worktrees/<name>`) holds a `gitdir` file
|
|
368
|
+
// pointing at the worktree's own `.git` file; its parent is the worktree root.
|
|
369
|
+
function worktreeRootFromGitDir(gitDir) {
|
|
370
|
+
try {
|
|
371
|
+
const p = fs.readFileSync(path.join(gitDir, "gitdir"), "utf8").trim();
|
|
372
|
+
return p ? path.dirname(p) : null;
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
315
378
|
// Turn the worktrees a session touched into the upload join keys: dedup'd
|
|
316
379
|
// branch + headSha arrays (so one session → many PRs all attribute), plus the
|
|
317
380
|
// primary single values (first entry) for back-compat with servers that read
|
|
@@ -329,6 +392,58 @@ function collectCaptureKeys(dirs, getBranch = getGitBranch, getSha = getGitHeadS
|
|
|
329
392
|
}
|
|
330
393
|
return { branch: branches[0] ?? null, headSha: headShas[0] ?? null, branches, headShas };
|
|
331
394
|
}
|
|
395
|
+
// Pull the commit SHAs the agent actually authored/pushed straight out of the
|
|
396
|
+
// transcript's git command OUTPUT — independent of end-of-session filesystem
|
|
397
|
+
// state. Catches: commit output "[branch 4c4e790]", push ranges
|
|
398
|
+
// "abc1234..def5678", and bare 40-hex (rev-parse). This is the signal that
|
|
399
|
+
// rescues attribution when the worktree's HEAD is stale or the worktree is gone
|
|
400
|
+
// at SessionEnd (pruned worktree, submodule pointer bump).
|
|
401
|
+
function extractShaTokens(transcriptContent) {
|
|
402
|
+
const out = new Set();
|
|
403
|
+
const commitRe = /\[[^\]\n]*?\b([0-9a-f]{7,40})\]/g; // [branch <sha>]
|
|
404
|
+
const rangeRe = /\b([0-9a-f]{7,40})\.\.([0-9a-f]{7,40})\b/g; // <sha>..<sha>
|
|
405
|
+
const fullRe = /\b([0-9a-f]{40})\b/g; // bare full sha (rev-parse)
|
|
406
|
+
let m;
|
|
407
|
+
while ((m = commitRe.exec(transcriptContent)))
|
|
408
|
+
out.add(m[1].toLowerCase());
|
|
409
|
+
while ((m = rangeRe.exec(transcriptContent))) {
|
|
410
|
+
out.add(m[1].toLowerCase());
|
|
411
|
+
out.add(m[2].toLowerCase());
|
|
412
|
+
}
|
|
413
|
+
while ((m = fullRe.exec(transcriptContent)))
|
|
414
|
+
out.add(m[1].toLowerCase());
|
|
415
|
+
return [...out];
|
|
416
|
+
}
|
|
417
|
+
// Resolve the extracted tokens to FULL 40-char SHAs via the repo's shared object
|
|
418
|
+
// store (a worktree's commits live in the main repo's .git/objects, so they
|
|
419
|
+
// resolve even after the worktree dir is removed). Full SHAs pass through; short
|
|
420
|
+
// ones are resolved; unresolvable ones are dropped (a short sha can't exact-
|
|
421
|
+
// match the worker's full commitShas). Keeps the server-side match exact.
|
|
422
|
+
function parseAuthoredShas(transcriptContent, repoRoot, resolveFull = (sha) => defaultResolveFullSha(sha, repoRoot)) {
|
|
423
|
+
const out = new Set();
|
|
424
|
+
for (const tok of extractShaTokens(transcriptContent)) {
|
|
425
|
+
if (tok.length === 40) {
|
|
426
|
+
out.add(tok);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const full = resolveFull(tok);
|
|
430
|
+
if (full && /^[0-9a-f]{40}$/.test(full))
|
|
431
|
+
out.add(full.toLowerCase());
|
|
432
|
+
}
|
|
433
|
+
return [...out];
|
|
434
|
+
}
|
|
435
|
+
function defaultResolveFullSha(sha, repoRoot) {
|
|
436
|
+
try {
|
|
437
|
+
const r = (0, child_process_1.execSync)(`git rev-parse ${sha}^{commit}`, {
|
|
438
|
+
cwd: repoRoot,
|
|
439
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
440
|
+
}).toString().trim();
|
|
441
|
+
return /^[0-9a-f]{40}$/.test(r) ? r : null;
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
332
447
|
// Current branch via direct .git/HEAD read (no git binary required, same
|
|
333
448
|
// robustness as getGitRemoteUrl). Returns null on detached HEAD or when
|
|
334
449
|
// unresolvable. This is the session→merge join key: a captured session's
|
package/package.json
CHANGED