@brainervirus/workit-core 2.3.0 → 2.5.0

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/src/git/rev.ts CHANGED
@@ -130,8 +130,8 @@ child.on("exit", (code) => {
130
130
 
131
131
  const WATCHDOG_TIMEOUT_EXIT = 124;
132
132
 
133
- /** A network git call under the process-group watchdog (see WATCHDOG). */
134
- const gitNetwork = (cwd: string, args: string[], timeoutMs: number): GitRun => {
133
+ /** A network git call (push, fetch, ls-remote) under the process-group watchdog (see WATCHDOG). */
134
+ export const gitNetwork = (cwd: string, args: string[], timeoutMs: number): GitRun => {
135
135
  const invocation = networkGitInvocation(cwd, args, timeoutMs);
136
136
  const result = spawnSync(process.execPath, ["-e", WATCHDOG], {
137
137
  cwd,
@@ -351,11 +351,25 @@ export function remoteTip(
351
351
  branch: string,
352
352
  options: { timeoutMs?: number } = {},
353
353
  ): RemoteTipResult {
354
- if (!safeArg(remote) || !safeArg(branch))
354
+ if (!safeArg(branch))
355
355
  return { ok: false, code: "invalid_input", error: "remote and branch must not start with -" };
356
+ return remoteRefTip(cwd, remote, `refs/heads/${branch}`, options);
357
+ }
358
+
359
+ /**
360
+ * The commit a full remote ref points at (`refs/heads/x`, `refs/tags/v1`); an
361
+ * annotated tag resolves to the commit it peels to. Same contract as remoteTip.
362
+ */
363
+ export function remoteRefTip(
364
+ cwd: string,
365
+ remote: string,
366
+ ref: string,
367
+ options: { timeoutMs?: number } = {},
368
+ ): RemoteTipResult {
369
+ if (!safeArg(remote) || !safeArg(ref))
370
+ return { ok: false, code: "invalid_input", error: "remote and ref must not start with -" };
356
371
  const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.network;
357
- const ref = `refs/heads/${branch}`;
358
- const listed = gitNetwork(cwd, ["ls-remote", remote, ref], timeoutMs);
372
+ const listed = gitNetwork(cwd, ["ls-remote", remote, ref, `${ref}^{}`], timeoutMs);
359
373
  if (!listed.ok)
360
374
  return {
361
375
  ok: false,
@@ -364,11 +378,109 @@ export function remoteTip(
364
378
  ? `git ls-remote ${redactRemote(remote)} timed out after ${timeoutMs} ms`
365
379
  : `git ls-remote ${redactRemote(remote)} failed (unreachable or not authorized)`,
366
380
  };
381
+ let direct: string | null = null;
367
382
  for (const row of listed.stdout.split(/\r?\n/u)) {
368
383
  const [sha, name] = row.split(/\s+/u);
369
- if (name === ref && sha) return { ok: true, sha };
384
+ if (!sha) continue;
385
+ if (name === `${ref}^{}`) return { ok: true, sha };
386
+ if (name === ref) direct = sha;
370
387
  }
371
- return { ok: true, sha: null };
388
+ return { ok: true, sha: direct };
389
+ }
390
+
391
+ export type FetchResult =
392
+ | { ok: true }
393
+ | { ok: false; code: "invalid_input" | "unavailable" | "busy"; error: string };
394
+
395
+ /**
396
+ * `git fetch --no-tags --no-recurse-submodules --no-write-fetch-head
397
+ * <remote> <refspec…>` under the watchdog (no prompts, bounded). Pass bare
398
+ * object ids (no `:dst`) for a pure read: the objects arrive, no ref moves.
399
+ * Contention on a repository lock is `busy`.
400
+ */
401
+ export function fetchRefs(
402
+ cwd: string,
403
+ remote: string,
404
+ refspecs: readonly string[],
405
+ options: { timeoutMs?: number } = {},
406
+ ): FetchResult {
407
+ if (
408
+ !safeArg(remote) ||
409
+ refspecs.length === 0 ||
410
+ !refspecs.every((spec) => safeArg(spec.replace(/^\+/u, "")))
411
+ )
412
+ return { ok: false, code: "invalid_input", error: "remote and refspecs must not start with -" };
413
+ const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.network;
414
+ const fetched = gitNetwork(
415
+ cwd,
416
+ [
417
+ "fetch",
418
+ "--no-tags",
419
+ "--no-recurse-submodules",
420
+ "--no-write-fetch-head",
421
+ "--quiet",
422
+ remote,
423
+ ...refspecs,
424
+ ],
425
+ timeoutMs,
426
+ );
427
+ if (fetched.ok) return { ok: true };
428
+ if (/\.lock'?:? File exists|unable to lock|another git process/iu.test(fetched.stderr))
429
+ return {
430
+ ok: false,
431
+ code: "busy",
432
+ error: `git fetch ${redactRemote(remote)}: the repository is locked by another git process`,
433
+ };
434
+ return {
435
+ ok: false,
436
+ code: "unavailable",
437
+ error: fetched.timedOut
438
+ ? `git fetch ${redactRemote(remote)} timed out after ${timeoutMs} ms`
439
+ : `git fetch ${redactRemote(remote)} failed (unreachable, not authorized, or ref missing)`,
440
+ };
441
+ }
442
+
443
+ /** The configured remote names. */
444
+ export function remoteNames(cwd: string): string[] {
445
+ return (git(cwd, ["remote"]).stdout ?? "")
446
+ .split(/\r?\n/u)
447
+ .map((value) => value.trim())
448
+ .filter(Boolean);
449
+ }
450
+
451
+ /** The absolute git common dir (shared by every worktree), or null outside a repository. */
452
+ export function gitCommonDir(cwd: string): string | null {
453
+ return line(git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]));
454
+ }
455
+
456
+ /** True when `rev` names a commit present in the local object store. */
457
+ export function hasCommit(cwd: string, rev: string): boolean {
458
+ if (!safeArg(rev)) return false;
459
+ return git(cwd, ["cat-file", "-e", `${rev}^{commit}`]).ok;
460
+ }
461
+
462
+ /** The commit a local ref (e.g. refs/remotes/origin/main) points at, or null. */
463
+ export function resolveRef(cwd: string, ref: string): string | null {
464
+ if (!safeArg(ref)) return null;
465
+ return line(git(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]));
466
+ }
467
+
468
+ /**
469
+ * Commits `head` lacks from `base` (behind) and has on top of it (ahead), and
470
+ * whether `base` is already an ancestor of `head`. Null when either side
471
+ * does not resolve locally.
472
+ */
473
+ export function aheadBehind(
474
+ cwd: string,
475
+ base: string,
476
+ head: string,
477
+ ): { ahead: number; behind: number; upToDate: boolean } | null {
478
+ if (!safeArg(base) || !safeArg(head)) return null;
479
+ const counted = line(git(cwd, ["rev-list", "--left-right", "--count", `${base}...${head}`]));
480
+ const match = counted ? /^(\d+)\s+(\d+)$/u.exec(counted) : null;
481
+ if (!match) return null;
482
+ const upToDate = git(cwd, ["merge-base", "--is-ancestor", base, head]).ok;
483
+ return { behind: Number(match[1]), ahead: Number(match[2]), upToDate };
372
484
  }
373
485
 
374
486
  const config = (cwd: string, key: string): string | null =>
@@ -386,10 +498,7 @@ export function currentBranch(cwd: string): string | null {
386
498
  */
387
499
  export function pushRemoteName(cwd: string, branch?: string | null): string | null {
388
500
  const name = branch ?? currentBranch(cwd);
389
- const remotes = (git(cwd, ["remote"]).stdout ?? "")
390
- .split(/\r?\n/u)
391
- .map((value) => value.trim())
392
- .filter(Boolean);
501
+ const remotes = remoteNames(cwd);
393
502
  const candidates = [
394
503
  name ? config(cwd, `branch.${name}.pushRemote`) : null,
395
504
  config(cwd, "remote.pushDefault"),
package/src/ledger.ts CHANGED
@@ -62,7 +62,7 @@ export type VerdictSurface = (typeof VERDICT_SURFACES)[number];
62
62
  export type Observer = "agent_asserted" | "workit_cli";
63
63
 
64
64
  /** Row types written by the CLI's own PR verbs (S10/S11); the only PR→branch source. */
65
- export const PR_ROW_TYPES: ReadonlySet<string> = new Set(["pr.created", "pr.status"]);
65
+ export const PR_ROW_TYPES: ReadonlySet<string> = new Set(["pr.created", "pr.status", "pr.merged"]);
66
66
 
67
67
  /** Who wrote a row. Unverified: the environment says so (D18). */
68
68
  export type LedgerActor = { host: string; session: string | null; agentId: string | null };
@@ -1156,6 +1156,22 @@ export function summarizeRow(row: ReadRow): RowSummary {
1156
1156
  case "handoff":
1157
1157
  summary = `next: ${text("next")}${text("note") ? ` (note: ${text("note")})` : ""}`;
1158
1158
  break;
1159
+ case "commit.recorded":
1160
+ summary = `${(row.head ?? "?").slice(0, 12)} ${text("subject")}${row.actor.session ? "" : " (no session)"}`;
1161
+ break;
1162
+ case "push.verified":
1163
+ case "push.noop":
1164
+ summary = `${text("remote")} ${(str(row.previous) ?? "(new)").slice(0, 12)} -> ${(row.head ?? "?").slice(0, 12)}${row.forced === true ? " (force-with-lease)" : ""}`;
1165
+ break;
1166
+ case "pr.created":
1167
+ summary = `#${row.pr ?? "?"} -> ${text("base")} at ${(row.head ?? "?").slice(0, 12)}${row.created === false ? " (existing)" : ""}`;
1168
+ break;
1169
+ case "pr.merged":
1170
+ summary = `#${row.pr ?? "?"} ${text("method")} at ${(row.head ?? "?").slice(0, 12)}${text("mergeSha") ? ` -> ${text("mergeSha").slice(0, 12)}` : ""}`;
1171
+ break;
1172
+ case "delivery.verified":
1173
+ summary = `${text("expect")} delivered at ${(row.head ?? "?").slice(0, 12)}`;
1174
+ break;
1159
1175
  default:
1160
1176
  summary = text("what") || text("name") || row.type;
1161
1177
  }