@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/package.json +1 -1
- package/src/autonomy.ts +135 -0
- package/src/core/external-action-effects.ts +4 -9
- package/src/core/pr-create.ts +2 -71
- package/src/forge/exec.ts +221 -0
- package/src/forge/github.ts +529 -0
- package/src/forge/gitlab.ts +556 -0
- package/src/forge/pr-body.ts +100 -0
- package/src/forge/pr-ops.ts +450 -0
- package/src/forge/redact.ts +191 -0
- package/src/forge/report.ts +618 -0
- package/src/forge/reruns.ts +127 -0
- package/src/forge/resolve.ts +240 -0
- package/src/forge/types.ts +166 -0
- package/src/forge/verify.ts +398 -0
- package/src/git/ops.ts +626 -0
- package/src/git/rev.ts +120 -11
- package/src/ledger.ts +17 -1
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(
|
|
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
|
|
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 (
|
|
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:
|
|
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 = (
|
|
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
|
}
|