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 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 { branch, headSha, branches, headShas } = (0, utils_1.collectCaptureKeys)(captureDirs);
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
- const cwds = [];
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
- cwds.push(entry.cwd);
307
+ add(entry.cwd);
294
308
  }
295
309
  catch {
296
- // non-JSON / partial line — skip
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; keep one entry per distinct worktree gitdir (a
300
- // worktree's gitdir is .git/worktrees/<name>; the root and its subdirs share
301
- // .git, so they're excluded as not-separate).
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 = cwds.length - 1; i >= 0; i--) {
305
- const gitDir = resolveGitDir(cwds[i]);
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(cwds[i]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "Connect Claude Code, Codex, Cursor, or VS Code Copilot to Codiedev for org-wide session capture and artifact collaboration",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",