codiedev 0.9.10 → 0.9.11
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 +98 -9
- 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,70 @@ 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 EVERY filesystem path the session referenced, in order — not just
|
|
289
|
+
// the session cwd. An agent can touch a worktree without cd-ing into it:
|
|
290
|
+
// - `git -C <path> ...` (operate on a worktree by path)
|
|
291
|
+
// - editing a file inside it ("file_path": "<worktree>/src/x.ts")
|
|
292
|
+
// Catching all of these is what makes worktree detection robust.
|
|
293
|
+
const candidates = [];
|
|
294
|
+
const add = (p) => {
|
|
295
|
+
if (!p)
|
|
296
|
+
return;
|
|
297
|
+
candidates.push(path.isAbsolute(p) ? p : path.resolve(workspaceRoot, p));
|
|
298
|
+
};
|
|
299
|
+
const gitCRe = /git\s+(?:[^\s]+\s+)*?-C\s+(?:"([^"]+)"|'([^']+)'|(\S+))/g;
|
|
300
|
+
const filePathRe = /"file_path"\s*:\s*"((?:[^"\\]|\\.)+)"/g;
|
|
287
301
|
for (const line of transcriptContent.split("\n")) {
|
|
288
302
|
if (!line.trim())
|
|
289
303
|
continue;
|
|
290
304
|
try {
|
|
291
305
|
const entry = JSON.parse(line);
|
|
292
306
|
if (typeof entry?.cwd === "string" && entry.cwd)
|
|
293
|
-
|
|
307
|
+
add(entry.cwd);
|
|
294
308
|
}
|
|
295
309
|
catch {
|
|
296
|
-
// non-JSON / partial line —
|
|
310
|
+
// non-JSON / partial line — still scan the raw text below
|
|
311
|
+
}
|
|
312
|
+
let m;
|
|
313
|
+
gitCRe.lastIndex = 0;
|
|
314
|
+
while ((m = gitCRe.exec(line)))
|
|
315
|
+
add(m[1] ?? m[2] ?? m[3]);
|
|
316
|
+
filePathRe.lastIndex = 0;
|
|
317
|
+
while ((m = filePathRe.exec(line))) {
|
|
318
|
+
try {
|
|
319
|
+
add(path.dirname(JSON.parse(`"${m[1]}"`)));
|
|
320
|
+
}
|
|
321
|
+
catch { /* skip */ }
|
|
297
322
|
}
|
|
298
323
|
}
|
|
299
|
-
// Walk most-recent first;
|
|
300
|
-
//
|
|
301
|
-
//
|
|
324
|
+
// Walk most-recent first; one entry per distinct worktree gitdir (a worktree's
|
|
325
|
+
// gitdir is .git/worktrees/<name>; the root and its subdirs share .git, so
|
|
326
|
+
// they're excluded as not-separate). Normalize each to its canonical worktree
|
|
327
|
+
// root so a file path / subdir resolves to the worktree dir itself.
|
|
302
328
|
const out = [];
|
|
303
329
|
const seenGitDirs = new Set();
|
|
304
|
-
for (let i =
|
|
305
|
-
const gitDir = resolveGitDir(
|
|
330
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
331
|
+
const gitDir = resolveGitDir(candidates[i]);
|
|
306
332
|
if (!gitDir || (rootGitDir && gitDir === rootGitDir))
|
|
307
333
|
continue;
|
|
308
334
|
if (seenGitDirs.has(gitDir))
|
|
309
335
|
continue;
|
|
310
336
|
seenGitDirs.add(gitDir);
|
|
311
|
-
out.push(
|
|
337
|
+
out.push(worktreeRootFromGitDir(gitDir) ?? candidates[i]);
|
|
312
338
|
}
|
|
313
339
|
return out.length > 0 ? out : [workspaceRoot];
|
|
314
340
|
}
|
|
341
|
+
// A linked worktree's gitdir (`.git/worktrees/<name>`) holds a `gitdir` file
|
|
342
|
+
// pointing at the worktree's own `.git` file; its parent is the worktree root.
|
|
343
|
+
function worktreeRootFromGitDir(gitDir) {
|
|
344
|
+
try {
|
|
345
|
+
const p = fs.readFileSync(path.join(gitDir, "gitdir"), "utf8").trim();
|
|
346
|
+
return p ? path.dirname(p) : null;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
315
352
|
// Turn the worktrees a session touched into the upload join keys: dedup'd
|
|
316
353
|
// branch + headSha arrays (so one session → many PRs all attribute), plus the
|
|
317
354
|
// primary single values (first entry) for back-compat with servers that read
|
|
@@ -329,6 +366,58 @@ function collectCaptureKeys(dirs, getBranch = getGitBranch, getSha = getGitHeadS
|
|
|
329
366
|
}
|
|
330
367
|
return { branch: branches[0] ?? null, headSha: headShas[0] ?? null, branches, headShas };
|
|
331
368
|
}
|
|
369
|
+
// Pull the commit SHAs the agent actually authored/pushed straight out of the
|
|
370
|
+
// transcript's git command OUTPUT — independent of end-of-session filesystem
|
|
371
|
+
// state. Catches: commit output "[branch 4c4e790]", push ranges
|
|
372
|
+
// "abc1234..def5678", and bare 40-hex (rev-parse). This is the signal that
|
|
373
|
+
// rescues attribution when the worktree's HEAD is stale or the worktree is gone
|
|
374
|
+
// at SessionEnd (pruned worktree, submodule pointer bump).
|
|
375
|
+
function extractShaTokens(transcriptContent) {
|
|
376
|
+
const out = new Set();
|
|
377
|
+
const commitRe = /\[[^\]\n]*?\b([0-9a-f]{7,40})\]/g; // [branch <sha>]
|
|
378
|
+
const rangeRe = /\b([0-9a-f]{7,40})\.\.([0-9a-f]{7,40})\b/g; // <sha>..<sha>
|
|
379
|
+
const fullRe = /\b([0-9a-f]{40})\b/g; // bare full sha (rev-parse)
|
|
380
|
+
let m;
|
|
381
|
+
while ((m = commitRe.exec(transcriptContent)))
|
|
382
|
+
out.add(m[1].toLowerCase());
|
|
383
|
+
while ((m = rangeRe.exec(transcriptContent))) {
|
|
384
|
+
out.add(m[1].toLowerCase());
|
|
385
|
+
out.add(m[2].toLowerCase());
|
|
386
|
+
}
|
|
387
|
+
while ((m = fullRe.exec(transcriptContent)))
|
|
388
|
+
out.add(m[1].toLowerCase());
|
|
389
|
+
return [...out];
|
|
390
|
+
}
|
|
391
|
+
// Resolve the extracted tokens to FULL 40-char SHAs via the repo's shared object
|
|
392
|
+
// store (a worktree's commits live in the main repo's .git/objects, so they
|
|
393
|
+
// resolve even after the worktree dir is removed). Full SHAs pass through; short
|
|
394
|
+
// ones are resolved; unresolvable ones are dropped (a short sha can't exact-
|
|
395
|
+
// match the worker's full commitShas). Keeps the server-side match exact.
|
|
396
|
+
function parseAuthoredShas(transcriptContent, repoRoot, resolveFull = (sha) => defaultResolveFullSha(sha, repoRoot)) {
|
|
397
|
+
const out = new Set();
|
|
398
|
+
for (const tok of extractShaTokens(transcriptContent)) {
|
|
399
|
+
if (tok.length === 40) {
|
|
400
|
+
out.add(tok);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const full = resolveFull(tok);
|
|
404
|
+
if (full && /^[0-9a-f]{40}$/.test(full))
|
|
405
|
+
out.add(full.toLowerCase());
|
|
406
|
+
}
|
|
407
|
+
return [...out];
|
|
408
|
+
}
|
|
409
|
+
function defaultResolveFullSha(sha, repoRoot) {
|
|
410
|
+
try {
|
|
411
|
+
const r = (0, child_process_1.execSync)(`git rev-parse ${sha}^{commit}`, {
|
|
412
|
+
cwd: repoRoot,
|
|
413
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
414
|
+
}).toString().trim();
|
|
415
|
+
return /^[0-9a-f]{40}$/.test(r) ? r : null;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
332
421
|
// Current branch via direct .git/HEAD read (no git binary required, same
|
|
333
422
|
// robustness as getGitRemoteUrl). Returns null on detached HEAD or when
|
|
334
423
|
// unresolvable. This is the session→merge join key: a captured session's
|
package/package.json
CHANGED