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 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,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
- const cwds = [];
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
- const entry = JSON.parse(line);
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 / partial line — skip
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; 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).
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 = cwds.length - 1; i >= 0; i--) {
305
- const gitDir = resolveGitDir(cwds[i]);
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(cwds[i]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
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",