@brainervirus/workit-cli 2.3.0 → 2.4.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.
Files changed (2) hide show
  1. package/dist/index.js +2072 -182
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -73016,15 +73016,1851 @@ var init_check = __esm(() => {
73016
73016
  run12 = notImplemented("check", "S9b");
73017
73017
  });
73018
73018
 
73019
+ // packages/workit-core/src/git/rev.ts
73020
+ import { spawnSync as spawnSync13 } from "node:child_process";
73021
+ import { createHash as createHash12 } from "node:crypto";
73022
+ import fs15 from "node:fs";
73023
+ import os12 from "node:os";
73024
+ import path34 from "node:path";
73025
+ function networkGitInvocation(cwd, args, timeoutMs) {
73026
+ const seconds = Math.max(1, Math.ceil(timeoutMs / 1000));
73027
+ const env = {
73028
+ ...process.env,
73029
+ GIT_TERMINAL_PROMPT: "0",
73030
+ GIT_ASKPASS: "",
73031
+ SSH_ASKPASS: "",
73032
+ SSH_ASKPASS_REQUIRE: "never",
73033
+ GCM_INTERACTIVE: "never"
73034
+ };
73035
+ const base = process.env.GIT_SSH_COMMAND || line(git(cwd, ["config", "--get", "core.sshCommand"])) || (process.env.GIT_SSH ? null : "ssh");
73036
+ if (base)
73037
+ env.GIT_SSH_COMMAND = `${base} -o BatchMode=yes -o ConnectTimeout=${Math.min(10, seconds)}`;
73038
+ return {
73039
+ args: ["-c", "http.lowSpeedLimit=1", "-c", `http.lowSpeedTime=${seconds}`, ...args],
73040
+ env
73041
+ };
73042
+ }
73043
+ function redactRemote(raw) {
73044
+ const value = raw.trim();
73045
+ if (!value)
73046
+ return UNPARSEABLE;
73047
+ if (/^[A-Za-z][A-Za-z0-9+.-]*::/u.test(value))
73048
+ return UNPARSEABLE;
73049
+ if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith("."))
73050
+ return value;
73051
+ if (!/[:@]/u.test(value))
73052
+ return value;
73053
+ if (!value.includes("://")) {
73054
+ if (/^[^/@]*:[^/]*@/u.test(value))
73055
+ return UNPARSEABLE;
73056
+ const scp = /^(?:([^@\s/:]+)@)?([^:/\s]+):([^?#]+)/u.exec(value);
73057
+ return scp ? `${scp[1] ? `${scp[1]}@` : ""}${scp[2]}:${scp[3]}` : UNPARSEABLE;
73058
+ }
73059
+ try {
73060
+ const url = new URL(value);
73061
+ const ssh = /^(?:ssh|git\+ssh|ssh\+git):$/u.test(url.protocol);
73062
+ url.password = "";
73063
+ if (!ssh)
73064
+ url.username = "";
73065
+ url.search = "";
73066
+ url.hash = "";
73067
+ return url.toString();
73068
+ } catch {
73069
+ return UNPARSEABLE;
73070
+ }
73071
+ }
73072
+ function headSha(cwd) {
73073
+ return line(git(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
73074
+ }
73075
+ function worktreeTree(cwd, options = {}) {
73076
+ const top = line(git(cwd, ["rev-parse", "--show-toplevel"]));
73077
+ const indexPath = line(git(cwd, ["rev-parse", "--path-format=absolute", "--git-path", "index"]));
73078
+ if (!top || !indexPath)
73079
+ return null;
73080
+ const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
73081
+ const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
73082
+ const scratch = fs15.mkdtempSync(path34.join(os12.tmpdir(), "workit-tree-"));
73083
+ try {
73084
+ const tempIndex = path34.join(scratch, "index");
73085
+ if (fs15.existsSync(indexPath))
73086
+ seedIndex(indexPath, tempIndex);
73087
+ const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
73088
+ const untracked = git(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
73089
+ env,
73090
+ timeoutMs
73091
+ });
73092
+ if (!untracked.ok)
73093
+ return null;
73094
+ const skipped = [];
73095
+ for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
73096
+ try {
73097
+ const stat = fs15.statSync(path34.join(top, file));
73098
+ if (stat.isFile() && stat.size > cap)
73099
+ skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
73100
+ } catch {}
73101
+ }
73102
+ skipped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
73103
+ const added = skipped.length ? git(top, ["add", "-A", "--pathspec-from-file=-", "--pathspec-file-nul"], {
73104
+ env,
73105
+ timeoutMs,
73106
+ input: [".", ...skipped.map((file) => `:(exclude,literal)${file.path}`)].join("\x00")
73107
+ }) : git(top, ["add", "-A"], { env, timeoutMs });
73108
+ if (!added.ok)
73109
+ return null;
73110
+ const tree = line(git(top, ["write-tree"], { env, timeoutMs }));
73111
+ if (!tree)
73112
+ return null;
73113
+ const base = line(git(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
73114
+ const key = skipped.length ? `sha256:${createHash12("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
73115
+ return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
73116
+ } finally {
73117
+ fs15.rmSync(scratch, { recursive: true, force: true });
73118
+ }
73119
+ }
73120
+ function patchId(cwd, base, head) {
73121
+ if (!safeArg(base) || !safeArg(head))
73122
+ return null;
73123
+ const diff = git(cwd, [
73124
+ "diff",
73125
+ "-U3",
73126
+ "--diff-algorithm=myers",
73127
+ "--indent-heuristic",
73128
+ "--no-color",
73129
+ "--no-ext-diff",
73130
+ "--no-textconv",
73131
+ "--no-renames",
73132
+ "--full-index",
73133
+ "--src-prefix=a/",
73134
+ "--dst-prefix=b/",
73135
+ `${base}...${head}`,
73136
+ "--"
73137
+ ]);
73138
+ if (!diff.ok || !diff.stdout.trim())
73139
+ return null;
73140
+ const id = git(cwd, ["patch-id", "--stable"], { input: diff.stdout });
73141
+ return line(id)?.split(/\s+/u)[0] ?? null;
73142
+ }
73143
+ function fetchRefs(cwd, remote, refspecs, options = {}) {
73144
+ if (!safeArg(remote) || refspecs.length === 0 || !refspecs.every((spec) => safeArg(spec.replace(/^\+/u, ""))))
73145
+ return { ok: false, code: "invalid_input", error: "remote and refspecs must not start with -" };
73146
+ const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.network;
73147
+ const fetched = gitNetwork(cwd, [
73148
+ "fetch",
73149
+ "--no-tags",
73150
+ "--no-recurse-submodules",
73151
+ "--no-write-fetch-head",
73152
+ "--quiet",
73153
+ remote,
73154
+ ...refspecs
73155
+ ], timeoutMs);
73156
+ if (fetched.ok)
73157
+ return { ok: true };
73158
+ if (/\.lock'?:? File exists|unable to lock|another git process/iu.test(fetched.stderr))
73159
+ return {
73160
+ ok: false,
73161
+ code: "busy",
73162
+ error: `git fetch ${redactRemote(remote)}: the repository is locked by another git process`
73163
+ };
73164
+ return {
73165
+ ok: false,
73166
+ code: "unavailable",
73167
+ error: fetched.timedOut ? `git fetch ${redactRemote(remote)} timed out after ${timeoutMs} ms` : `git fetch ${redactRemote(remote)} failed (unreachable, not authorized, or ref missing)`
73168
+ };
73169
+ }
73170
+ function remoteNames(cwd) {
73171
+ return (git(cwd, ["remote"]).stdout ?? "").split(/\r?\n/u).map((value) => value.trim()).filter(Boolean);
73172
+ }
73173
+ function gitCommonDir(cwd) {
73174
+ return line(git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]));
73175
+ }
73176
+ function hasCommit(cwd, rev) {
73177
+ if (!safeArg(rev))
73178
+ return false;
73179
+ return git(cwd, ["cat-file", "-e", `${rev}^{commit}`]).ok;
73180
+ }
73181
+ function resolveRef(cwd, ref) {
73182
+ if (!safeArg(ref))
73183
+ return null;
73184
+ return line(git(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]));
73185
+ }
73186
+ function aheadBehind(cwd, base, head) {
73187
+ if (!safeArg(base) || !safeArg(head))
73188
+ return null;
73189
+ const counted = line(git(cwd, ["rev-list", "--left-right", "--count", `${base}...${head}`]));
73190
+ const match = counted ? /^(\d+)\s+(\d+)$/u.exec(counted) : null;
73191
+ if (!match)
73192
+ return null;
73193
+ const upToDate = git(cwd, ["merge-base", "--is-ancestor", base, head]).ok;
73194
+ return { behind: Number(match[1]), ahead: Number(match[2]), upToDate };
73195
+ }
73196
+ function currentBranch2(cwd) {
73197
+ return line(git(cwd, ["symbolic-ref", "--short", "-q", "HEAD"]));
73198
+ }
73199
+ function pushRemoteName(cwd, branch) {
73200
+ const name = branch ?? currentBranch2(cwd);
73201
+ const remotes = remoteNames(cwd);
73202
+ const candidates = [
73203
+ name ? config2(cwd, `branch.${name}.pushRemote`) : null,
73204
+ config2(cwd, "remote.pushDefault"),
73205
+ name ? config2(cwd, `branch.${name}.remote`) : null
73206
+ ];
73207
+ for (const candidate of candidates)
73208
+ if (candidate && candidate !== "." && remotes.includes(candidate))
73209
+ return candidate;
73210
+ if (remotes.includes("origin"))
73211
+ return "origin";
73212
+ return remotes.length === 1 ? remotes[0] : null;
73213
+ }
73214
+ function pushUrl(cwd, remote) {
73215
+ if (!safeArg(remote))
73216
+ return null;
73217
+ const urls = git(cwd, ["remote", "get-url", "--push", "--all", remote]).stdout.split(/\r?\n/u).map((value) => value.trim()).filter(Boolean);
73218
+ return urls.length === 1 ? urls[0] : null;
73219
+ }
73220
+ function parseRemoteUrl(raw) {
73221
+ const value = raw.trim();
73222
+ if (!value || /^[A-Za-z][A-Za-z0-9+.-]*::/u.test(value))
73223
+ return null;
73224
+ if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith(".")) {
73225
+ return { protocol: "file", user: null, host: "", port: null, path: value };
73226
+ }
73227
+ if (!value.includes("://")) {
73228
+ if (/^[^/@]*:[^/]*@/u.test(value))
73229
+ return null;
73230
+ const scp = /^(?:([^@\s/:]+)@)?([^:/\s]+):(.+)$/u.exec(value);
73231
+ if (!scp)
73232
+ return null;
73233
+ return {
73234
+ protocol: "ssh",
73235
+ user: scp[1] ?? null,
73236
+ host: scp[2],
73237
+ port: null,
73238
+ path: cleanPath(scp[3])
73239
+ };
73240
+ }
73241
+ let url;
73242
+ try {
73243
+ url = new URL(value);
73244
+ } catch {
73245
+ return null;
73246
+ }
73247
+ const scheme = url.protocol.replace(/:$/u, "");
73248
+ const protocol = scheme === "ssh" || scheme === "git+ssh" || scheme === "ssh+git" ? "ssh" : scheme === "https" || scheme === "http" || scheme === "git" || scheme === "file" ? scheme : null;
73249
+ if (!protocol)
73250
+ return null;
73251
+ return {
73252
+ protocol,
73253
+ user: protocol === "ssh" && url.username ? decodeURIComponent(url.username) : null,
73254
+ host: url.hostname.replace(/\.$/u, ""),
73255
+ port: url.port || null,
73256
+ path: cleanPath(decodeURIComponent(url.pathname))
73257
+ };
73258
+ }
73259
+ function readSshConfig(home = os12.homedir()) {
73260
+ try {
73261
+ return { text: fs15.readFileSync(path34.join(home, ".ssh", "config"), "utf8"), home };
73262
+ } catch {
73263
+ return { text: "", home };
73264
+ }
73265
+ }
73266
+ function resolveSshHost(alias, sshConfig) {
73267
+ const source = typeof sshConfig === "string" ? { text: sshConfig, home: os12.homedir() } : sshConfig;
73268
+ let hostname = null;
73269
+ let port = null;
73270
+ const evaluate = (text, initiallyActive, neverMatch, depth) => {
73271
+ let active = initiallyActive;
73272
+ for (const { key, args } of directives(text)) {
73273
+ if (key === "host") {
73274
+ active = !neverMatch && hostMatches(args, alias);
73275
+ continue;
73276
+ }
73277
+ if (key === "match") {
73278
+ active = false;
73279
+ continue;
73280
+ }
73281
+ if (key === "include") {
73282
+ if (depth >= 16)
73283
+ continue;
73284
+ for (const spec of args)
73285
+ for (const file of includeFiles(spec, source.home)) {
73286
+ let included;
73287
+ try {
73288
+ included = fs15.readFileSync(file, "utf8");
73289
+ } catch {
73290
+ continue;
73291
+ }
73292
+ evaluate(included, active, neverMatch || !active, depth + 1);
73293
+ }
73294
+ continue;
73295
+ }
73296
+ if (!active || args.length === 0)
73297
+ continue;
73298
+ if (key === "hostname" && hostname === null)
73299
+ hostname = args[0].replace(/%(%|h)/gu, (_, token) => token === "h" ? alias : "%");
73300
+ if (key === "port" && port === null)
73301
+ port = args[0];
73302
+ }
73303
+ };
73304
+ evaluate(source.text, true, false, 0);
73305
+ return { hostname: (hostname ?? alias).replace(/\.$/u, ""), port };
73306
+ }
73307
+ function deriveForge(url, options = {}) {
73308
+ const parsed = parseRemoteUrl(url);
73309
+ if (!parsed || parsed.protocol === "file" || !parsed.host)
73310
+ return null;
73311
+ const segments = parsed.path.split("/").filter(Boolean);
73312
+ if (segments.length < 2)
73313
+ return null;
73314
+ const hostname = (parsed.protocol === "ssh" ? resolveSshHost(parsed.host, options.sshConfig ?? readSshConfig()).hostname : parsed.host).toLowerCase();
73315
+ const withPort = parsed.protocol !== "ssh" && parsed.port ? `${hostname}:${parsed.port}` : hostname;
73316
+ const base = { host: parsed.host, hostname, repo: segments.join("/") };
73317
+ const known = KNOWN_HOSTS[hostname];
73318
+ if (known) {
73319
+ if (known.kind === "github" && segments.length !== 2)
73320
+ return null;
73321
+ return { ...base, kind: known.kind, apiHost: known.apiHost, via: "known_host" };
73322
+ }
73323
+ for (const kind of ["github", "gitlab"]) {
73324
+ const hosts = new Set((options.hosts?.[kind] ?? []).map(normalizeHost));
73325
+ if (hosts.has(withPort) || hosts.has(hostname)) {
73326
+ const apiHost = hosts.has(withPort) ? withPort : hostname;
73327
+ return { ...base, kind, apiHost, via: "configured_host" };
73328
+ }
73329
+ }
73330
+ const label = hostname.split(".")[0];
73331
+ if ((label === "github" || label === "gitlab") && hostname.includes("."))
73332
+ return { ...base, kind: label, apiHost: withPort, via: "host_name" };
73333
+ return null;
73334
+ }
73335
+ function pushForge(cwd, options = {}) {
73336
+ const remote = pushRemoteName(cwd, options.branch);
73337
+ if (!remote)
73338
+ return {
73339
+ ok: false,
73340
+ code: "not_found",
73341
+ error: "no push remote is configured for this branch",
73342
+ unblock: "git remote add origin <url>"
73343
+ };
73344
+ const url = pushUrl(cwd, remote);
73345
+ if (!url)
73346
+ return {
73347
+ ok: false,
73348
+ code: "unavailable",
73349
+ error: `remote "${remote}" has no single push URL`,
73350
+ unblock: `git remote get-url --push --all ${remote} # keep exactly one push URL`
73351
+ };
73352
+ const forge = deriveForge(url, options);
73353
+ if (!forge) {
73354
+ const host = parseRemoteUrl(url)?.host || null;
73355
+ return {
73356
+ ok: false,
73357
+ code: "unavailable",
73358
+ error: host ? `cannot tell whether push host "${host}" is GitHub or GitLab` : `push URL ${redactRemote(url)} of remote "${remote}" is not a GitHub/GitLab URL`,
73359
+ unblock: host ? `map the alias in ~/.ssh/config (Host ${host} / HostName github.com|gitlab.com) or set github.host / gitlab.host in ~/.config/workit/vcs.json` : `git remote set-url --push ${remote} <ssh-or-https-url>`
73360
+ };
73361
+ }
73362
+ return { ok: true, remote, url: redactRemote(url), forge };
73363
+ }
73364
+ function forgeConflict(derived, configuredProvider) {
73365
+ const configured = configuredProvider?.trim().toLowerCase();
73366
+ if (!configured || configured === derived.kind)
73367
+ return null;
73368
+ return {
73369
+ code: "blocked",
73370
+ error: `forge_mismatch: push remote ${derived.host} is ${derived.kind} but the workspace vcs.provider is ${configured}`,
73371
+ unblock: `set vcs.provider to "${derived.kind}" for this repo in ~/.config/workit/workspaces.json (workit-github-override), or push to a ${configured} remote`
73372
+ };
73373
+ }
73374
+ var MAX_BUFFER, GIT_TIMEOUTS, git = (cwd, args, options = {}) => {
73375
+ const result = spawnSync13("git", args, {
73376
+ cwd,
73377
+ encoding: "utf8",
73378
+ env: options.env ?? process.env,
73379
+ input: options.input,
73380
+ maxBuffer: MAX_BUFFER,
73381
+ timeout: options.timeoutMs ?? GIT_TIMEOUTS.local,
73382
+ killSignal: "SIGKILL",
73383
+ windowsHide: true
73384
+ });
73385
+ const error = result.error;
73386
+ return {
73387
+ ok: result.status === 0,
73388
+ stdout: result.stdout ?? "",
73389
+ stderr: result.stderr ?? "",
73390
+ timedOut: error?.code === "ETIMEDOUT"
73391
+ };
73392
+ }, line = (run) => {
73393
+ if (!run.ok)
73394
+ return null;
73395
+ const value = run.stdout.trim();
73396
+ return value ? value : null;
73397
+ }, safeArg = (value) => value.length > 0 && !value.startsWith("-"), WATCHDOG = `
73398
+ const cp = require("node:child_process");
73399
+ const { ms, args } = JSON.parse(process.env.WORKIT_GIT_WATCHDOG);
73400
+ delete process.env.WORKIT_GIT_WATCHDOG;
73401
+ const win = process.platform === "win32";
73402
+ const child = cp.spawn("git", args, { stdio: "inherit", detached: !win, windowsHide: true });
73403
+ const killTree = () => {
73404
+ if (!child.pid) return;
73405
+ if (win) cp.spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
73406
+ else try { process.kill(-child.pid, "SIGKILL"); } catch {}
73407
+ };
73408
+ let timedOut = false;
73409
+ const timer = setTimeout(() => { timedOut = true; killTree(); }, ms);
73410
+ child.on("error", () => { clearTimeout(timer); process.exit(127); });
73411
+ child.on("exit", (code) => {
73412
+ clearTimeout(timer);
73413
+ if (!win) killTree();
73414
+ process.exit(timedOut ? 124 : code ?? 1);
73415
+ });
73416
+ `, WATCHDOG_TIMEOUT_EXIT = 124, gitNetwork = (cwd, args, timeoutMs) => {
73417
+ const invocation = networkGitInvocation(cwd, args, timeoutMs);
73418
+ const result = spawnSync13(process.execPath, ["-e", WATCHDOG], {
73419
+ cwd,
73420
+ encoding: "utf8",
73421
+ env: {
73422
+ ...invocation.env,
73423
+ WORKIT_GIT_WATCHDOG: JSON.stringify({ ms: timeoutMs, args: invocation.args })
73424
+ },
73425
+ maxBuffer: MAX_BUFFER,
73426
+ timeout: timeoutMs + 1e4,
73427
+ killSignal: "SIGKILL",
73428
+ windowsHide: true
73429
+ });
73430
+ const error = result.error;
73431
+ return {
73432
+ ok: result.status === 0,
73433
+ stdout: result.stdout ?? "",
73434
+ stderr: result.stderr ?? "",
73435
+ timedOut: result.status === WATCHDOG_TIMEOUT_EXIT || error?.code === "ETIMEDOUT"
73436
+ };
73437
+ }, UNPARSEABLE = "<unparseable remote>", emptyTree = (cwd) => line(git(cwd, ["mktree"], { input: "" })), MAX_UNTRACKED_BYTES, seedIndex = (from, to) => {
73438
+ fs15.copyFileSync(from, to);
73439
+ const { atimeNs, mtimeNs } = fs15.statSync(from, { bigint: true });
73440
+ const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
73441
+ fs15.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
73442
+ }, config2 = (cwd, key) => line(git(cwd, ["config", "--get", key])), cleanPath = (value) => value.replace(/^\/+/u, "").replace(/\/+$/u, "").replace(/\.git$/u, ""), globToRegExp2 = (pattern) => new RegExp(`^${pattern.split("").map((char) => char === "*" ? ".*" : char === "?" ? "." : char.replace(/[.+^${}()|[\]\\]/gu, "\\$&")).join("")}$`, "iu"), hostMatches = (patterns, host) => {
73443
+ let matched = false;
73444
+ for (const pattern of patterns) {
73445
+ const negated = pattern.startsWith("!");
73446
+ if (globToRegExp2(negated ? pattern.slice(1) : pattern).test(host)) {
73447
+ if (negated)
73448
+ return false;
73449
+ matched = true;
73450
+ }
73451
+ }
73452
+ return matched;
73453
+ }, tokens = (rest) => [...rest.matchAll(/"([^"]*)"|(\S+)/gu)].map((match) => match[1] ?? match[2]), directives = (text) => text.split(/\r?\n/u).flatMap((raw) => {
73454
+ const trimmed = raw.trim();
73455
+ if (!trimmed || trimmed.startsWith("#"))
73456
+ return [];
73457
+ const match = /^(\S+?)(?:\s*=\s*|\s+)(.*)$/u.exec(trimmed);
73458
+ if (!match)
73459
+ return [];
73460
+ return [{ key: match[1].toLowerCase(), args: tokens(match[2]) }];
73461
+ }), includeFiles = (spec, home) => {
73462
+ const expanded = spec.startsWith("~") ? path34.join(home, spec.slice(1)) : spec;
73463
+ const absolute = path34.isAbsolute(expanded) ? expanded : path34.join(home, ".ssh", expanded);
73464
+ const dir = path34.dirname(absolute);
73465
+ const base = path34.basename(absolute);
73466
+ if (!/[*?]/u.test(base))
73467
+ return [absolute];
73468
+ try {
73469
+ const pattern = globToRegExp2(base);
73470
+ return fs15.readdirSync(dir).filter((name) => pattern.test(name)).toSorted().map((name) => path34.join(dir, name));
73471
+ } catch {
73472
+ return [];
73473
+ }
73474
+ }, KNOWN_HOSTS, normalizeHost = (value) => value.trim().toLowerCase().replace(/^https?:\/\//u, "").replace(/\/.*$/u, "").replace(/\.$/u, "");
73475
+ var init_rev = __esm(() => {
73476
+ MAX_BUFFER = 256 * 1024 * 1024;
73477
+ GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
73478
+ MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
73479
+ KNOWN_HOSTS = {
73480
+ "github.com": { kind: "github", apiHost: "github.com" },
73481
+ "ssh.github.com": { kind: "github", apiHost: "github.com" },
73482
+ "gitlab.com": { kind: "gitlab", apiHost: "gitlab.com" },
73483
+ "altssh.gitlab.com": { kind: "gitlab", apiHost: "gitlab.com" }
73484
+ };
73485
+ });
73486
+
73487
+ // packages/workit-core/src/forge/reruns.ts
73488
+ import fs16 from "node:fs";
73489
+ import path35 from "node:path";
73490
+ function rerunLogPath(cwd) {
73491
+ const common = gitCommonDir(cwd);
73492
+ return common ? path35.join(common, "workit", "ci-reruns.jsonl") : null;
73493
+ }
73494
+ function rerunCounts(cwd, key) {
73495
+ const counts = new Map;
73496
+ const file = rerunLogPath(cwd);
73497
+ if (!file)
73498
+ return counts;
73499
+ let text;
73500
+ try {
73501
+ text = fs16.readFileSync(file, "utf8");
73502
+ } catch {
73503
+ return counts;
73504
+ }
73505
+ for (const raw of text.split(`
73506
+ `)) {
73507
+ if (!raw.trim())
73508
+ continue;
73509
+ let row;
73510
+ try {
73511
+ row = JSON.parse(raw);
73512
+ } catch {
73513
+ continue;
73514
+ }
73515
+ if (isRow(row) && row.repo === key.repo && row.pr === key.pr && row.head === key.head)
73516
+ counts.set(row.check, (counts.get(row.check) ?? 0) + 1);
73517
+ }
73518
+ return counts;
73519
+ }
73520
+ function recordReruns(cwd, rows) {
73521
+ if (rows.length === 0)
73522
+ return true;
73523
+ const file = rerunLogPath(cwd);
73524
+ if (!file)
73525
+ return false;
73526
+ try {
73527
+ fs16.mkdirSync(path35.dirname(file), { recursive: true });
73528
+ fs16.appendFileSync(file, rows.map((row) => `${JSON.stringify(row)}
73529
+ `).join(""));
73530
+ return true;
73531
+ } catch {
73532
+ return false;
73533
+ }
73534
+ }
73535
+ function withRerunLock(cwd, fn) {
73536
+ const file = rerunLogPath(cwd);
73537
+ if (!file)
73538
+ return fn();
73539
+ const lock = `${file.slice(0, -".jsonl".length)}.lock`;
73540
+ fs16.mkdirSync(path35.dirname(lock), { recursive: true });
73541
+ const until = Date.now() + LOCK_WAIT_MS;
73542
+ for (;; ) {
73543
+ try {
73544
+ fs16.closeSync(fs16.openSync(lock, "wx"));
73545
+ break;
73546
+ } catch (error) {
73547
+ if (error.code !== "EEXIST")
73548
+ throw error;
73549
+ try {
73550
+ if (Date.now() - fs16.statSync(lock).mtimeMs > LOCK_STALE_MS) {
73551
+ fs16.rmSync(lock, { force: true });
73552
+ continue;
73553
+ }
73554
+ } catch {
73555
+ continue;
73556
+ }
73557
+ if (Date.now() >= until)
73558
+ return null;
73559
+ pause(50);
73560
+ }
73561
+ }
73562
+ try {
73563
+ return fn();
73564
+ } finally {
73565
+ fs16.rmSync(lock, { force: true });
73566
+ }
73567
+ }
73568
+ var isRow = (value) => {
73569
+ const row = value;
73570
+ return !!row && row.type === "ci.rerun" && typeof row.pr === "number" && typeof row.head === "string" && typeof row.check === "string" && typeof row.repo === "string";
73571
+ }, LOCK_WAIT_MS = 2000, LOCK_STALE_MS = 60000, pause = (ms) => {
73572
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
73573
+ };
73574
+ var init_reruns = __esm(() => {
73575
+ init_rev();
73576
+ });
73577
+
73578
+ // packages/workit-core/src/forge/types.ts
73579
+ var success2 = (data) => ({ ok: true, data }), failure3 = (code, error, unblock) => ({ ok: false, code, error, ...unblock ? { unblock } : {} });
73580
+
73581
+ // packages/workit-core/src/forge/report.ts
73582
+ function gatingChecks(status) {
73583
+ const required = status.checks.filter((check) => check.required === true);
73584
+ const protectionNamesChecks = (status.requiredContexts?.length ?? 0) > 0;
73585
+ return required.length > 0 || protectionNamesChecks ? { checks: required, protected: true } : { checks: status.checks, protected: false };
73586
+ }
73587
+ function missingRequired(status) {
73588
+ if (status.requiredContexts === null)
73589
+ return null;
73590
+ const seen = new Set(status.checks.map((check) => check.context));
73591
+ return status.requiredContexts.filter((context) => !seen.has(context));
73592
+ }
73593
+ function checksState(checks, missing = []) {
73594
+ if (checks.some((check) => check.state === "failing"))
73595
+ return "failing";
73596
+ if (checks.some((check) => check.state === "pending") || (missing?.length ?? 0) > 0)
73597
+ return "pending";
73598
+ return checks.length > 0 ? "passing" : "none";
73599
+ }
73600
+ function blockersOf(input) {
73601
+ if (input.state === "merged")
73602
+ return [{ next: "MERGED", reason: "merged" }];
73603
+ if (input.state === "closed")
73604
+ return [{ next: "CLOSED", reason: "closed" }];
73605
+ const out = [];
73606
+ const ms = input.mergeState ?? "";
73607
+ if (input.conflicts)
73608
+ out.push({ next: "RESOLVE_CONFLICTS", reason: "conflicts" });
73609
+ if (input.rebaseRequired)
73610
+ out.push({ next: "REBASE", reason: "behind_base_required" });
73611
+ if (input.threads > 0 || ms === "discussions_not_resolved")
73612
+ out.push({ next: "RESOLVE_THREADS", reason: "unresolved_threads" });
73613
+ if (input.checks === "failing")
73614
+ out.push({
73615
+ next: "FIX_CI",
73616
+ reason: input.checksRequired ? "required_checks_failing" : "checks_failing"
73617
+ });
73618
+ else if (input.checks === "pending" || ms === "ci_still_running" || ms === "ci_must_pass" && input.checks !== "passing")
73619
+ out.push({ next: "WAITING_CI", reason: "checks_pending" });
73620
+ if (input.reviewDecision === "changes_requested" || ms === "requested_changes")
73621
+ out.push({ next: "ADDRESS_REVIEW", reason: "changes_requested" });
73622
+ else if (input.reviewDecision === "review_required" || ms === "not_approved")
73623
+ out.push({ next: "REVIEW", reason: "review_required" });
73624
+ if (input.draft || ms === "draft" || ms === "draft_status")
73625
+ out.push({ next: "MARK_READY", reason: "draft" });
73626
+ if (input.inMergeQueue)
73627
+ out.push({ next: "IN_MERGE_QUEUE", reason: "in_merge_queue" });
73628
+ if (out.length === 0) {
73629
+ const allowed = input.forge === "github" ? GITHUB_MERGEABLE.has(ms) : GITLAB_MERGEABLE.has(ms);
73630
+ if (input.mergeable === "unknown")
73631
+ out.push({ next: "NOT_MERGEABLE", reason: "mergeability_unknown" });
73632
+ else if (!allowed)
73633
+ out.push({ next: "NOT_MERGEABLE", reason: `merge_state_${ms || "unknown"}` });
73634
+ }
73635
+ return out;
73636
+ }
73637
+ function computeBehindBase(cwd, remotes, baseSha, headSha_, options = {}) {
73638
+ const empty = { behind: null, ahead: null, upToDate: null };
73639
+ if (!baseSha)
73640
+ return success2({ ...empty, baseSha: null, error: "the forge reported no base tip" });
73641
+ for (const [sha, remote] of [
73642
+ [baseSha, remotes.base],
73643
+ [headSha_, remotes.head]
73644
+ ]) {
73645
+ if (hasCommit(cwd, sha))
73646
+ continue;
73647
+ const fetched = fetchRefs(cwd, remote, [sha], options);
73648
+ if (!fetched.ok && remote !== remotes.base)
73649
+ fetchRefs(cwd, remotes.base, [sha], options);
73650
+ if (!hasCommit(cwd, sha))
73651
+ return success2({
73652
+ ...empty,
73653
+ baseSha,
73654
+ error: fetched.ok ? `${sha.slice(0, 12)} is not available locally` : fetched.error
73655
+ });
73656
+ }
73657
+ const counts = aheadBehind(cwd, baseSha, headSha_);
73658
+ return success2(counts ? { ...counts, baseSha } : { ...empty, baseSha, error: "ahead/behind did not resolve" });
73659
+ }
73660
+ function selectPr(cwd, resolved, options) {
73661
+ if (options.pr)
73662
+ return success2(options.pr);
73663
+ const branch = options.branch ?? currentBranch2(cwd);
73664
+ if (!branch)
73665
+ return failure3("invalid_input", "HEAD is detached; pass --pr <n> or --branch <name>");
73666
+ const localSha = resolveRef(cwd, `refs/heads/${branch}`);
73667
+ const found = resolved.forge.findPr(branch, {
73668
+ owner: resolved.headRepo.split("/")[0] ?? null,
73669
+ projectId: resolved.headProjectId,
73670
+ sha: localSha
73671
+ });
73672
+ if (!found.ok)
73673
+ return found;
73674
+ if (!found.data) {
73675
+ const noun = resolved.forge.kind === "github" ? "pull request" : "merge request";
73676
+ return failure3("not_found", `no ${noun} for branch ${branch} from ${resolved.headRepo} in ${resolved.forge.repo}`, "push the branch and open one (workit pr create, S11), or pass --pr <n>");
73677
+ }
73678
+ return success2(found.data.number);
73679
+ }
73680
+ function buildStatusDoc(cwd, resolved, status, options) {
73681
+ const { forge } = resolved;
73682
+ const reruns = rerunCounts(cwd, { repo: forge.repo, pr: status.number, head: status.head.sha });
73683
+ const gating = gatingChecks(status);
73684
+ const missing = missingRequired(status);
73685
+ const failing = status.checks.filter((check) => check.state === "failing");
73686
+ const failingDocs = failing.map((check, index) => {
73687
+ const doc = {
73688
+ name: check.name,
73689
+ url: check.url,
73690
+ runId: check.runId,
73691
+ jobId: check.jobId,
73692
+ conclusion: check.conclusion,
73693
+ required: check.required,
73694
+ logTail: [],
73695
+ rerunsOnHead: reruns.get(check.name) ?? 0
73696
+ };
73697
+ if (check.jobId !== null && options.logLines > 0 && index < MAX_LOG_TAILS) {
73698
+ const tail = forge.jobLogTail({ jobId: check.jobId, scope: check.scope }, options.logLines);
73699
+ if (tail.ok)
73700
+ doc.logTail = tail.data;
73701
+ else
73702
+ doc.logError = tail.error;
73703
+ }
73704
+ return doc;
73705
+ });
73706
+ const local = currentBranch2(cwd) === status.head.branch ? headSha(cwd) : resolveRef(cwd, `refs/heads/${status.head.branch}`);
73707
+ let behindBase = null;
73708
+ if (options.behind && status.state === "open") {
73709
+ const counted = computeBehindBase(cwd, { base: resolved.baseRemote, head: resolved.remote }, status.baseSha, status.head.sha, options.behindTimeoutMs === undefined ? {} : { timeoutMs: options.behindTimeoutMs });
73710
+ if (!counted.ok)
73711
+ return counted;
73712
+ behindBase = counted.data;
73713
+ }
73714
+ const state = checksState(gating.checks, missing);
73715
+ const blockers = blockersOf({
73716
+ forge: forge.kind,
73717
+ state: status.state,
73718
+ draft: status.draft,
73719
+ conflicts: status.conflicts,
73720
+ rebaseRequired: status.rebaseRequired,
73721
+ mergeable: status.mergeable,
73722
+ mergeState: status.mergeState,
73723
+ inMergeQueue: status.inMergeQueue,
73724
+ reviewDecision: status.reviewDecision,
73725
+ threads: status.threads.length,
73726
+ checks: state,
73727
+ checksRequired: gating.protected
73728
+ });
73729
+ return success2({
73730
+ forge: forge.kind,
73731
+ repo: forge.repo,
73732
+ headRepo: resolved.fork ? resolved.headRepo : null,
73733
+ number: status.number,
73734
+ url: status.url,
73735
+ state: status.state,
73736
+ draft: status.draft,
73737
+ base: status.base,
73738
+ head: {
73739
+ branch: status.head.branch,
73740
+ sha: status.head.sha,
73741
+ localSha: local,
73742
+ pushed: local === null ? null : local === status.head.sha
73743
+ },
73744
+ mergeable: status.mergeable,
73745
+ mergeState: status.mergeState,
73746
+ conflicts: status.conflicts,
73747
+ rebaseRequired: status.rebaseRequired,
73748
+ inMergeQueue: status.inMergeQueue,
73749
+ behindBase,
73750
+ checks: {
73751
+ state,
73752
+ failing: failingDocs,
73753
+ pending: status.checks.filter((check) => check.state === "pending").map((check) => check.name),
73754
+ passing: status.checks.filter((check) => check.state === "passing").length,
73755
+ missingRequired: missing
73756
+ },
73757
+ reviews: { decision: status.reviewDecision, unresolvedThreads: status.threads },
73758
+ blockers: status.state === "open" ? blockers.map((blocker) => blocker.reason) : [],
73759
+ identity: options.identity ?? null,
73760
+ truncated: status.truncated,
73761
+ next: blockers[0]?.next ?? "READY"
73762
+ });
73763
+ }
73764
+ function prStatusReport(cwd, resolved, options = {}) {
73765
+ const number = selectPr(cwd, resolved, options);
73766
+ if (!number.ok)
73767
+ return number;
73768
+ const status = resolved.forge.prStatus(number.data);
73769
+ if (!status.ok)
73770
+ return status;
73771
+ const doc = buildStatusDoc(cwd, resolved, status.data, {
73772
+ logLines: options.logLines ?? 60,
73773
+ behind: options.behind ?? true,
73774
+ identity: options.identity
73775
+ });
73776
+ if (!doc.ok)
73777
+ return doc;
73778
+ return success2({ doc: doc.data, raw: status.data });
73779
+ }
73780
+ function waitVerdict(doc, options) {
73781
+ if (doc.state !== "open")
73782
+ return {
73783
+ state: "blocked",
73784
+ reason: `pr_${doc.state}`,
73785
+ unblock: `the ${doc.forge === "github" ? "PR" : "MR"} is ${doc.state}; nothing to wait for`
73786
+ };
73787
+ if (options.head && !doc.head.sha.startsWith(options.head))
73788
+ return { state: "waiting", reason: "head_mismatch" };
73789
+ const checks = doc.checks.state;
73790
+ if (checks === "failing")
73791
+ return { state: "failed", reason: "checks_failing" };
73792
+ if (checks === "pending")
73793
+ return { state: "waiting", reason: "checks_pending" };
73794
+ if (checks === "passing")
73795
+ return { state: "ready", reason: "checks_passing" };
73796
+ if (doc.conflicts)
73797
+ return {
73798
+ state: "blocked",
73799
+ reason: "conflicts",
73800
+ unblock: `rebase onto ${doc.base} and push; CI does not run on a conflicting PR`
73801
+ };
73802
+ const ciRequired = doc.checks.missingRequired === null || doc.mergeState === "ci_must_pass" || doc.mergeState === "ci_still_running";
73803
+ if (ciRequired)
73804
+ return { state: "waiting", reason: "no_checks_yet" };
73805
+ if (options.elapsedMs < (options.noChecksGraceMs ?? NO_CHECKS_GRACE_MS))
73806
+ return { state: "waiting", reason: "no_checks_yet" };
73807
+ return { state: "ready", reason: "no_checks" };
73808
+ }
73809
+ function pollDelay(intervalMs, poll) {
73810
+ const cap = Math.max(intervalMs, 120000);
73811
+ return Math.min(cap, Math.round(intervalMs * 1.5 ** poll));
73812
+ }
73813
+ function planRerun(status, selection) {
73814
+ let checks;
73815
+ if (selection.names.length > 0) {
73816
+ const unknown = selection.names.filter((name) => !status.checks.some((check) => check.name === name));
73817
+ if (unknown.length)
73818
+ return failure3("invalid_input", `unknown check ${unknown.map((name) => JSON.stringify(name)).join(", ")}; known: ${status.checks.map((check) => check.name).join(", ") || "none"}`);
73819
+ checks = status.checks.filter((check) => selection.names.includes(check.name));
73820
+ const running = checks.filter((check) => check.state === "pending");
73821
+ if (running.length)
73822
+ return failure3("busy", `still running: ${running.map((check) => check.name).join(", ")}`, "workit ci wait");
73823
+ } else {
73824
+ checks = status.checks.filter((check) => check.state === "failing");
73825
+ }
73826
+ if (checks.length === 0)
73827
+ return failure3("not_found", `no failing checks on head ${status.head.sha.slice(0, 12)}`);
73828
+ const rerunnable = checks.filter((check) => check.jobId !== null && check.runId !== null);
73829
+ const skipped = checks.filter((check) => !rerunnable.includes(check)).map((check) => check.name);
73830
+ const targets = [];
73831
+ if (selection.names.length === 0) {
73832
+ const busy = status.checks.filter((check) => check.state === "pending" && check.runId !== null && rerunnable.some((failing) => failing.runId === check.runId));
73833
+ if (busy.length)
73834
+ return failure3("busy", `the failed jobs' run is still in progress (${busy.map((check) => check.name).join(", ")})`, "workit ci wait, then rerun");
73835
+ const seen = new Set;
73836
+ for (const check of rerunnable)
73837
+ if (!seen.has(check.runId)) {
73838
+ seen.add(check.runId);
73839
+ targets.push({ kind: "failed_in_run", runId: check.runId, scope: check.scope });
73840
+ }
73841
+ } else {
73842
+ for (const check of rerunnable)
73843
+ targets.push({ kind: "job", jobId: check.jobId, scope: check.scope });
73844
+ }
73845
+ if (targets.length === 0)
73846
+ return failure3("unavailable", `${skipped.join(", ")} cannot be rerun from here (not a CI job); rerun it on the forge`);
73847
+ return success2({ checks: rerunnable, targets, skipped });
73848
+ }
73849
+ function executeRerun(cwd, resolved, status, options) {
73850
+ if (status.state !== "open")
73851
+ return failure3("blocked", `${status.state} PR: CI cannot be rerun`, "nothing to do");
73852
+ const plan = planRerun(status, options);
73853
+ if (!plan.ok)
73854
+ return plan;
73855
+ const locked = withRerunLock(cwd, () => rerunLocked(cwd, resolved, status, plan.data, options));
73856
+ return locked ?? failure3("busy", "another ci rerun is in progress for this repository", "retry in a moment");
73857
+ }
73858
+ function rerunLocked(cwd, resolved, status, plan, options) {
73859
+ const { forge } = resolved;
73860
+ const counts = rerunCounts(cwd, { repo: forge.repo, pr: status.number, head: status.head.sha });
73861
+ const repeated = plan.checks.filter((check) => (counts.get(check.name) ?? 0) > 0);
73862
+ if (repeated.length && !options.force)
73863
+ return failure3("blocked", `rerun_limit: ${repeated.map((check) => check.name).join(", ")} already rerun once on head ${status.head.sha.slice(0, 12)}`, "treat it as a real failure and fix it; if it is a confirmed flake, rerun with --force");
73864
+ const accepted = [];
73865
+ for (const target of plan.targets) {
73866
+ const done = forge.rerun(target);
73867
+ if (!done.ok) {
73868
+ recordAccepted(cwd, resolved, status, accepted, options);
73869
+ return failure3(done.code, accepted.length ? `${done.error} (already rerun: ${accepted.map((check) => check.name).join(", ")})` : done.error, done.unblock);
73870
+ }
73871
+ accepted.push(...plan.checks.filter((check) => target.kind === "failed_in_run" ? check.runId === target.runId : check.jobId === target.jobId));
73872
+ }
73873
+ const recorded = recordAccepted(cwd, resolved, status, accepted, options);
73874
+ return success2({
73875
+ pr: status.number,
73876
+ head: status.head.sha,
73877
+ reason: options.reason,
73878
+ forced: options.force,
73879
+ rerun: accepted.map((check) => ({ name: check.name, runId: check.runId, jobId: check.jobId })),
73880
+ skipped: plan.skipped,
73881
+ recorded
73882
+ });
73883
+ }
73884
+ function recordAccepted(cwd, resolved, status, accepted, options) {
73885
+ const at = new Date().toISOString();
73886
+ const rows = accepted.map((check) => ({
73887
+ v: 1,
73888
+ at,
73889
+ type: "ci.rerun",
73890
+ forge: resolved.forge.kind,
73891
+ repo: resolved.forge.repo,
73892
+ pr: status.number,
73893
+ head: status.head.sha,
73894
+ check: check.name,
73895
+ reason: options.reason,
73896
+ forced: options.force
73897
+ }));
73898
+ return recordReruns(cwd, rows);
73899
+ }
73900
+ var MAX_LOG_TAILS = 5, GITLAB_MERGEABLE, GITHUB_MERGEABLE, NO_CHECKS_GRACE_MS = 90000;
73901
+ var init_report = __esm(() => {
73902
+ init_rev();
73903
+ init_reruns();
73904
+ GITLAB_MERGEABLE = new Set(["mergeable"]);
73905
+ GITHUB_MERGEABLE = new Set(["clean", "unstable", "has_hooks"]);
73906
+ });
73907
+
73908
+ // packages/workit-core/src/forge/redact.ts
73909
+ function redactText(value) {
73910
+ let out = stripAnsi2(value).replace(PRIVATE_KEY, "[REDACTED PRIVATE KEY]");
73911
+ out = out.replace(WEBHOOK, `$1/${R}`);
73912
+ out = out.replace(URL_USERINFO, `$1${R}@`);
73913
+ for (const pattern of TOKEN_PATTERNS)
73914
+ out = out.replace(pattern, R);
73915
+ out = out.replace(CURL_USER, `$1$2${R}$2`);
73916
+ out = out.replace(BEARER, `$1${R}`);
73917
+ out = out.replace(QUOTED_ASSIGNMENT, (match, lead, quote, secret) => secret.startsWith(R) ? match : `${lead}${quote}${R}${quote}`);
73918
+ out = out.replace(NETRC, `$1${R}`);
73919
+ for (const pattern of FLAG_SECRETS)
73920
+ out = out.replace(pattern, `$1${R}`);
73921
+ out = out.replace(QUERY_SECRET, `$1${R}`);
73922
+ out = out.replace(ASSIGNMENT, (match, lead, scheme, secret) => secret.startsWith(R) ? match : `${lead}${scheme ?? ""}${R}`);
73923
+ return out.split(`
73924
+ `).map((line) => line.replace(BASE64, (blob) => /\bbase64\b/iu.test(line) || decodesToSecret(blob) ? R : blob)).join(`
73925
+ `);
73926
+ }
73927
+ function logTail(text, lines) {
73928
+ if (lines <= 0)
73929
+ return [];
73930
+ const cleaned = text.replace(GITLAB_SECTION, "").split(/\r?\n/u).map((raw) => stripAnsi2(raw).split("\r").findLast((part) => part.trim()) ?? "").map((raw) => raw.replace(LINE_NOISE, ""));
73931
+ const all = redactText(cleaned.join(`
73932
+ `)).split(`
73933
+ `);
73934
+ let end = all.length;
73935
+ for (let index = all.length - 1;index >= 0; index -= 1)
73936
+ if (FAILURE_END.some((pattern) => pattern.test(all[index]))) {
73937
+ end = index + 1;
73938
+ break;
73939
+ }
73940
+ const kept = all.slice(0, end);
73941
+ while (kept.length > 0 && !kept[kept.length - 1].trim())
73942
+ kept.pop();
73943
+ const cap = (value) => value.length > MAX_LINE ? `${value.slice(0, MAX_LINE)}…` : value;
73944
+ const tailStart = Math.max(0, kept.length - lines);
73945
+ const find = (pattern) => kept.findIndex((value) => pattern.test(value) && !NOT_FIRST_ERROR.test(value));
73946
+ const strong = find(STRONG_ERROR);
73947
+ const first = strong >= 0 ? strong : find(WEAK_ERROR);
73948
+ if (first < 0 || first >= tailStart || lines < 8)
73949
+ return kept.slice(tailStart).map(cap);
73950
+ const headCount = Math.ceil((lines - 1) * (2 / 3));
73951
+ const from = Math.max(0, first - LEAD);
73952
+ const head = kept.slice(from, from + headCount);
73953
+ const tailCount = lines - 1 - head.length;
73954
+ const tailFrom = Math.max(from + head.length, kept.length - tailCount);
73955
+ const tail = kept.slice(tailFrom);
73956
+ return [...head, ...tailFrom > from + head.length ? ["…"] : [], ...tail].map(cap);
73957
+ }
73958
+ function shortBody(value, max = 300) {
73959
+ const flat = redactText(value).replace(/\s+/gu, " ").trim();
73960
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
73961
+ }
73962
+ var R = "[REDACTED]", TOKEN_PATTERNS, WEBHOOK, URL_USERINFO, CURL_USER, KEY, ASSIGNMENT, BEARER, QUOTED_ASSIGNMENT, NETRC, FLAG_SECRETS, QUERY_SECRET, PRIVATE_KEY, BASE64, ANSI, stripAnsi2 = (value) => value.replace(ANSI, ""), decodesToSecret = (blob) => {
73963
+ if (/^[0-9a-f]+$/iu.test(blob))
73964
+ return false;
73965
+ let decoded;
73966
+ try {
73967
+ decoded = Buffer.from(blob, "base64").toString("utf8");
73968
+ } catch {
73969
+ return false;
73970
+ }
73971
+ if (!/^[\x20-\x7e\s]+$/u.test(decoded))
73972
+ return false;
73973
+ return TOKEN_PATTERNS.some((pattern) => new RegExp(pattern.source, "u").test(decoded)) || /^[^\s:]{1,64}:[^\s]{6,}$/u.test(decoded.trim());
73974
+ }, LINE_NOISE, GITLAB_SECTION, FAILURE_END, MAX_LINE = 400, STRONG_ERROR, WEAK_ERROR, NOT_FIRST_ERROR, LEAD = 3;
73975
+ var init_redact = __esm(() => {
73976
+ TOKEN_PATTERNS = [
73977
+ /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/gu,
73978
+ /\bgl[a-z]{1,8}-[A-Za-z0-9_-]{16,}/gu,
73979
+ /\bnpm_[A-Za-z0-9]{30,}/gu,
73980
+ /\bxox[abposr]-[A-Za-z0-9-]{10,}/gu,
73981
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu,
73982
+ /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{20,}/gu,
73983
+ /\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/gu,
73984
+ /\bAIza[0-9A-Za-z_-]{35}/gu,
73985
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/gu
73986
+ ];
73987
+ WEBHOOK = /\b(https?:\/\/(?:hooks\.slack\.com\/(?:services|workflows|triggers)|discord(?:app)?\.com\/api\/webhooks))\/[^\s"'<>]+/giu;
73988
+ URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/giu;
73989
+ CURL_USER = /(\s(?:-u|--user)(?:\s+|=))(["']?)[^\s"':]+:[^\s"']+\2/gu;
73990
+ KEY = String.raw`[A-Za-z0-9_.-]*?(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|account[_-]?key|private[_-]?key|client[_-]?secret|credentials?|authorization|cookie|signature)[A-Za-z0-9_.-]*`;
73991
+ ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*["']?)((?:bearer|basic|token|digest)\s+)?([^\s"',;&]+)`, "giu");
73992
+ BEARER = /\b(bearer\s+)[A-Za-z0-9._~+/-]{12,}=*/giu;
73993
+ QUOTED_ASSIGNMENT = new RegExp(String.raw`(?<![A-Za-z0-9])(["']?${KEY}["']?\s*[:=]\s*)(["'])([^"'\n]*[^\s"'][^"'\n]*)\2`, "giu");
73994
+ NETRC = /\b((?:machine|login)\s+\S+\s+(?:login\s+\S+\s+)?password\s+)\S+/giu;
73995
+ FLAG_SECRETS = [
73996
+ /\b(sshpass\s+-p\s*)\S+/gu,
73997
+ /\b(mysql(?:dump|admin|import|show)?\b[^\n]*?\s-p)(?!\s|$)\S+/gu
73998
+ ];
73999
+ QUERY_SECRET = /([?&;](?:sig|signature|token|access_token|api_key|apikey|key|password|secret|client_secret|X-Amz-Signature|X-Goog-Signature)=)[^&\s#"']+/giu;
74000
+ PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/gu;
74001
+ BASE64 = /[A-Za-z0-9+/]{32,}={0,2}/gu;
74002
+ ANSI = new RegExp(String.raw`\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007`, "gu");
74003
+ LINE_NOISE = /^(?:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s?)?(?:\d{2}[A-Z]\s)?/u;
74004
+ GITLAB_SECTION = /section_(?:start|end):\d+:[A-Za-z0-9_.-]+(?:\[[^\]]*\])?\r?/gu;
74005
+ FAILURE_END = [/##\[error\]Process completed with exit code/u, /^ERROR: Job failed\b/u];
74006
+ STRONG_ERROR = /^\s*(?:\(fail\)|FAIL\b|✗|✕|×|error(?:\[[A-Z0-9]+\])?:|[A-Za-z]*Error\b:|AssertionError|Traceback|panic:|Exception\b)/u;
74007
+ WEAK_ERROR = /\berror\b[:[]|\bERROR\b|\bFAIL(?:ED)?\b|\bfailed\b|\bassert/u;
74008
+ NOT_FIRST_ERROR = /^\s*(?:\(pass\)|\(skip\)|\(todo\)|✓|✔|ok\b|PASS\b)|^\s*\d+ fail\b|##\[error\]Process completed|^\s*0 (?:errors?|fail)/u;
74009
+ });
74010
+
74011
+ // packages/workit-core/src/forge/exec.ts
74012
+ import { spawnSync as spawnSync14 } from "node:child_process";
74013
+ import fs17 from "node:fs";
74014
+ import path36 from "node:path";
74015
+ function findExecutable(bin, env) {
74016
+ const dirs = (env.PATH ?? env.Path ?? "").split(path36.delimiter).filter(Boolean);
74017
+ const exts = process.platform === "win32" ? [".exe"] : [""];
74018
+ for (const dir of dirs)
74019
+ for (const ext of exts) {
74020
+ const candidate = path36.join(dir, `${bin}${ext}`);
74021
+ try {
74022
+ const stat = fs17.statSync(candidate);
74023
+ if (stat.isFile())
74024
+ return candidate;
74025
+ } catch {}
74026
+ }
74027
+ return null;
74028
+ }
74029
+ function systemRunner(kind, apiHost, env = process.env) {
74030
+ const childEnv = {
74031
+ ...env,
74032
+ ...kind === "github" ? { GH_HOST: apiHost } : { GITLAB_HOST: apiHost },
74033
+ GH_PROMPT_DISABLED: "1",
74034
+ GH_NO_UPDATE_NOTIFIER: "1",
74035
+ GLAB_CHECK_UPDATE: "false",
74036
+ NO_PROMPT: "1",
74037
+ NO_COLOR: "1",
74038
+ GH_PAGER: "",
74039
+ GLAB_PAGER: "",
74040
+ PAGER: ""
74041
+ };
74042
+ return (bin, args, options) => {
74043
+ const exe = findExecutable(bin, env);
74044
+ if (!exe)
74045
+ return { status: null, stdout: "", stderr: "", timedOut: false, missing: true };
74046
+ const token = options.token ? kind === "github" ? { GH_TOKEN: options.token, GH_ENTERPRISE_TOKEN: options.token } : { GITLAB_TOKEN: options.token } : {};
74047
+ const result = spawnSync14(exe, [...args], {
74048
+ encoding: "utf8",
74049
+ env: { ...childEnv, ...token },
74050
+ maxBuffer: MAX_BUFFER2,
74051
+ timeout: options.timeoutMs,
74052
+ killSignal: "SIGKILL",
74053
+ windowsHide: true,
74054
+ stdio: ["ignore", "pipe", "pipe"]
74055
+ });
74056
+ const error = result.error;
74057
+ return {
74058
+ status: result.status,
74059
+ stdout: result.stdout ?? "",
74060
+ stderr: result.stderr ?? "",
74061
+ timedOut: error?.code === "ETIMEDOUT",
74062
+ missing: error?.code === "ENOENT"
74063
+ };
74064
+ };
74065
+ }
74066
+ function cliFailure(bin, apiHost, run, what, timeoutMs) {
74067
+ if (run.missing)
74068
+ return failure3("unavailable", `${bin} is not installed`, installHint(bin));
74069
+ if (run.timedOut)
74070
+ return failure3("unavailable", `${bin} api ${what} timed out after ${timeoutMs} ms`);
74071
+ const stderr = redactText(run.stderr).trim();
74072
+ if (/auth login|not logged in|no token|authentication required|bad credentials|\b401\b|unauthorized/iu.test(stderr))
74073
+ return failure3("unavailable", `${bin} is not authenticated for ${apiHost}`, loginHint(bin, apiHost));
74074
+ if (/\b404\b|not found/iu.test(stderr))
74075
+ return failure3("not_found", `${what} was not found`);
74076
+ const first = stderr.split(/\r?\n/u).find((value) => value.trim()) ?? `exit ${run.status}`;
74077
+ return failure3("failed", `${bin} api ${what} failed: ${first.slice(0, 200)}`);
74078
+ }
74079
+ function apiJson(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.api, options = {}) {
74080
+ const run = runner(bin, args, { timeoutMs });
74081
+ if (run.status !== 0) {
74082
+ if (options.graphql && !run.missing && !run.timedOut)
74083
+ try {
74084
+ const body = JSON.parse(run.stdout);
74085
+ if (Array.isArray(body.errors))
74086
+ return { ok: true, data: body };
74087
+ } catch {}
74088
+ return cliFailure(bin, apiHost, run, what, timeoutMs);
74089
+ }
74090
+ try {
74091
+ return { ok: true, data: JSON.parse(run.stdout) };
74092
+ } catch {
74093
+ return failure3("failed", `${bin} api ${what} returned invalid JSON`);
74094
+ }
74095
+ }
74096
+ function apiText(runner, bin, apiHost, args, what, timeoutMs = FORGE_TIMEOUTS.log) {
74097
+ const run = runner(bin, args, { timeoutMs });
74098
+ if (run.status !== 0)
74099
+ return cliFailure(bin, apiHost, run, what, timeoutMs);
74100
+ return { ok: true, data: run.stdout };
74101
+ }
74102
+ var FORGE_TIMEOUTS, MAX_BUFFER2, installHint = (bin) => bin === "gh" ? "install the GitHub CLI (https://cli.github.com) and run: gh auth login" : "install glab (https://gitlab.com/gitlab-org/cli) and run: glab auth login", loginHint = (bin, apiHost) => `${bin} auth login --hostname ${apiHost}`;
74103
+ var init_exec = __esm(() => {
74104
+ init_redact();
74105
+ FORGE_TIMEOUTS = { api: 20000, log: 30000 };
74106
+ MAX_BUFFER2 = 64 * 1024 * 1024;
74107
+ });
74108
+
74109
+ // packages/workit-core/src/forge/github.ts
74110
+ function checkFromContext(node) {
74111
+ const required = node.isRequired ?? null;
74112
+ if (node.__typename === "StatusContext") {
74113
+ const state = node.state.toUpperCase();
74114
+ return {
74115
+ name: node.context,
74116
+ context: node.context,
74117
+ state: state === "SUCCESS" ? "passing" : state === "FAILURE" || state === "ERROR" ? "failing" : "pending",
74118
+ conclusion: state.toLowerCase(),
74119
+ url: safeUrl(node.targetUrl),
74120
+ runId: null,
74121
+ jobId: null,
74122
+ scope: null,
74123
+ required
74124
+ };
74125
+ }
74126
+ if (node.conclusion === "STALE")
74127
+ return null;
74128
+ const run = node.checkSuite?.workflowRun ?? null;
74129
+ const workflow = run?.workflow?.name;
74130
+ const completed = node.status === "COMPLETED";
74131
+ const conclusion = node.conclusion?.toUpperCase() ?? null;
74132
+ return {
74133
+ name: workflow ? `${workflow} / ${node.name}` : node.name,
74134
+ context: node.name,
74135
+ state: !completed ? "pending" : conclusion && FAILING.has(conclusion) ? "failing" : conclusion === "SKIPPED" || conclusion === "NEUTRAL" ? "skipped" : "passing",
74136
+ conclusion: completed ? conclusion?.toLowerCase() ?? null : node.status.toLowerCase(),
74137
+ url: safeUrl(node.detailsUrl),
74138
+ runId: run?.databaseId ?? null,
74139
+ jobId: run ? node.databaseId ?? null : null,
74140
+ scope: null,
74141
+ required
74142
+ };
74143
+ }
74144
+ function latestPerName(checks) {
74145
+ const byName = new Map;
74146
+ for (const check of checks) {
74147
+ const seen = byName.get(check.name);
74148
+ if (!seen || (check.jobId ?? 0) >= (seen.jobId ?? 0))
74149
+ byName.set(check.name, check);
74150
+ }
74151
+ return [...byName.values()];
74152
+ }
74153
+ function threadFrom(node) {
74154
+ const first = node.comments.nodes[0];
74155
+ return {
74156
+ id: node.id,
74157
+ path: node.path,
74158
+ line: node.line,
74159
+ author: first?.author?.login ?? null,
74160
+ isBot: first?.author?.__typename === "Bot",
74161
+ outdated: node.isOutdated,
74162
+ url: safeUrl(first?.url ?? null),
74163
+ body: shortBody(first?.body ?? "")
74164
+ };
74165
+ }
74166
+ function createGitHubForge(options) {
74167
+ const { apiHost, repo, runner } = options;
74168
+ const maxPages = options.maxPages ?? GITHUB_MAX_PAGES;
74169
+ const [owner, name] = repo.split("/");
74170
+ const rest = (endpoint, what) => apiJson(runner, "gh", apiHost, ["api", endpoint], what);
74171
+ const graphql = (query, variables, what) => {
74172
+ const args = ["api", "graphql", "-f", `query=${query}`];
74173
+ for (const [key, value] of Object.entries(variables)) {
74174
+ if (value === null)
74175
+ continue;
74176
+ args.push(typeof value === "number" ? "-F" : "-f", `${key}=${value}`);
74177
+ }
74178
+ const response = apiJson(runner, "gh", apiHost, args, what, FORGE_TIMEOUTS.api, { graphql: true });
74179
+ if (!response.ok)
74180
+ return response;
74181
+ if (response.data.errors?.length) {
74182
+ const message = redactText(response.data.errors[0]?.message ?? "GraphQL error").slice(0, 300);
74183
+ return /could not resolve to a (pullrequest|repository)/iu.test(message) ? failure3("not_found", `${what}: ${message}`) : failure3("failed", `${what}: ${message}`);
74184
+ }
74185
+ if (!response.data.data)
74186
+ return failure3("failed", `${what}: empty GraphQL response`);
74187
+ return success2(response.data.data);
74188
+ };
74189
+ const fetchPr = (number, cursors) => {
74190
+ const data = graphql(PR_STATUS_QUERY, { owner, name, number, threads: cursors.threads, contexts: cursors.contexts }, `pull request #${number}`);
74191
+ if (!data.ok)
74192
+ return data;
74193
+ const pr = data.data.repository?.pullRequest;
74194
+ return pr ? success2(pr) : failure3("not_found", `pull request #${number} was not found`);
74195
+ };
74196
+ const requiredContexts = (base) => {
74197
+ const branch = rest(`repos/${repo}/branches/${encodeRef(base)}`, `branch ${base}`);
74198
+ const rules = rest(`repos/${repo}/rules/branches/${encodeRef(base)}`, `rules for ${base}`);
74199
+ if (!branch.ok || !rules.ok || !Array.isArray(rules.data))
74200
+ return null;
74201
+ const contexts = new Set;
74202
+ const classic = branch.data.protection?.required_status_checks;
74203
+ for (const context of classic?.contexts ?? [])
74204
+ contexts.add(context);
74205
+ for (const check of classic?.checks ?? [])
74206
+ if (check.context)
74207
+ contexts.add(check.context);
74208
+ for (const rule of rules.data)
74209
+ if (rule.type === "required_status_checks") {
74210
+ const listed = rule.parameters?.required_status_checks;
74211
+ if (Array.isArray(listed)) {
74212
+ for (const item of listed)
74213
+ if (typeof item.context === "string")
74214
+ contexts.add(item.context);
74215
+ }
74216
+ }
74217
+ return [...contexts];
74218
+ };
74219
+ return {
74220
+ kind: "github",
74221
+ apiHost,
74222
+ repo,
74223
+ identity(expected) {
74224
+ const run = runner("gh", ["api", "user"], { timeoutMs: FORGE_TIMEOUTS.api });
74225
+ if (run.status !== 0) {
74226
+ if (/HTTP 403|resource not accessible by integration/iu.test(run.stderr))
74227
+ return success2({
74228
+ login: null,
74229
+ expected,
74230
+ matches: null,
74231
+ note: "the credential cannot read /user (app or Actions token); identity not checked"
74232
+ });
74233
+ return cliFailure("gh", apiHost, run, "user", FORGE_TIMEOUTS.api);
74234
+ }
74235
+ let login = "";
74236
+ try {
74237
+ const user = JSON.parse(run.stdout);
74238
+ login = typeof user.login === "string" ? user.login : "";
74239
+ } catch {}
74240
+ if (!login)
74241
+ return failure3("failed", "gh api user returned no login");
74242
+ return success2({
74243
+ login,
74244
+ expected,
74245
+ matches: expected ? login.toLowerCase() === expected.toLowerCase() : null
74246
+ });
74247
+ },
74248
+ repoInfo(target) {
74249
+ const info = rest(`repos/${target}`, `repository ${target}`);
74250
+ if (!info.ok)
74251
+ return info;
74252
+ return success2({
74253
+ id: typeof info.data.id === "number" ? info.data.id : null,
74254
+ parent: info.data.fork && info.data.parent?.full_name ? info.data.parent.full_name : null
74255
+ });
74256
+ },
74257
+ findPr(head, match) {
74258
+ const data = graphql(FIND_PR_QUERY, { owner, name, head }, `pull requests for ${head}`);
74259
+ if (!data.ok)
74260
+ return data;
74261
+ const nodes = data.data.repository?.pullRequests.nodes ?? [];
74262
+ const ours = nodes.filter((node) => node.headRefName === head && (match.owner !== null && node.headRepositoryOwner?.login.toLowerCase() === match.owner.toLowerCase() || match.sha !== null && node.headRefOid === match.sha));
74263
+ const pick = ours.find((node) => node.state === "OPEN") ?? ours[0];
74264
+ if (!pick)
74265
+ return success2(null);
74266
+ const ref = {
74267
+ number: pick.number,
74268
+ url: pick.url,
74269
+ state: prState(pick.state),
74270
+ headBranch: pick.headRefName,
74271
+ headSha: pick.headRefOid
74272
+ };
74273
+ return success2(ref);
74274
+ },
74275
+ prStatus(number) {
74276
+ const first = fetchPr(number, { threads: null, contexts: null });
74277
+ if (!first.ok)
74278
+ return first;
74279
+ const pr = first.data;
74280
+ const threads = [...pr.reviewThreads.nodes];
74281
+ const rollup = pr.commits.nodes[0]?.commit.statusCheckRollup ?? null;
74282
+ const contexts = [...rollup?.contexts.nodes ?? []];
74283
+ let threadPage = pr.reviewThreads.pageInfo;
74284
+ let contextPage = rollup?.contexts.pageInfo ?? { hasNextPage: false, endCursor: null };
74285
+ let pages = 1;
74286
+ while ((threadPage.hasNextPage || contextPage.hasNextPage) && pages < maxPages) {
74287
+ pages += 1;
74288
+ const next = fetchPr(number, {
74289
+ threads: threadPage.hasNextPage ? threadPage.endCursor : null,
74290
+ contexts: contextPage.hasNextPage ? contextPage.endCursor : null
74291
+ });
74292
+ if (!next.ok)
74293
+ return next;
74294
+ if (next.data.headRefOid !== pr.headRefOid)
74295
+ return failure3("failed", `pull request #${number} head moved while paging; retry`);
74296
+ if (threadPage.hasNextPage) {
74297
+ threads.push(...next.data.reviewThreads.nodes);
74298
+ threadPage = next.data.reviewThreads.pageInfo;
74299
+ }
74300
+ const nextRollup = next.data.commits.nodes[0]?.commit.statusCheckRollup;
74301
+ if (contextPage.hasNextPage && nextRollup) {
74302
+ contexts.push(...nextRollup.contexts.nodes);
74303
+ contextPage = nextRollup.contexts.pageInfo;
74304
+ }
74305
+ }
74306
+ const checks = latestPerName(contexts.map(checkFromContext).filter((check) => check !== null));
74307
+ const open = pr.state === "OPEN";
74308
+ const status = {
74309
+ number: pr.number,
74310
+ url: pr.url,
74311
+ state: prState(pr.state),
74312
+ draft: pr.isDraft,
74313
+ base: pr.baseRefName,
74314
+ baseSha: pr.baseRef?.target?.oid ?? pr.baseRefOid,
74315
+ head: { branch: pr.headRefName, sha: pr.headRefOid },
74316
+ mergeable: pr.mergeable === "MERGEABLE" ? "yes" : pr.mergeable === "CONFLICTING" ? "no" : "unknown",
74317
+ conflicts: pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY",
74318
+ rebaseRequired: pr.mergeStateStatus === "BEHIND",
74319
+ reviewDecision: pr.reviewDecision?.toLowerCase() ?? null,
74320
+ mergeState: pr.mergeStateStatus?.toLowerCase() ?? null,
74321
+ inMergeQueue: pr.isInMergeQueue === true,
74322
+ checks,
74323
+ requiredContexts: open ? requiredContexts(pr.baseRefName) : null,
74324
+ threads: threads.filter((node) => !node.isResolved).map(threadFrom),
74325
+ truncated: threadPage.hasNextPage || contextPage.hasNextPage
74326
+ };
74327
+ return success2(status);
74328
+ },
74329
+ jobLogTail(job, lines) {
74330
+ const text = apiText(runner, "gh", apiHost, ["api", `repos/${repo}/actions/jobs/${job.jobId}/logs`], `job ${job.jobId} log`, FORGE_TIMEOUTS.log);
74331
+ return text.ok ? success2(logTail(text.data, lines)) : text;
74332
+ },
74333
+ rerun(target) {
74334
+ const endpoint = target.kind === "failed_in_run" ? `repos/${repo}/actions/runs/${target.runId}/rerun-failed-jobs` : `repos/${repo}/actions/jobs/${target.jobId}/rerun`;
74335
+ const what = target.kind === "failed_in_run" ? `rerun of run ${target.runId}` : `rerun of job ${target.jobId}`;
74336
+ const run = apiText(runner, "gh", apiHost, ["api", "-X", "POST", endpoint], what, FORGE_TIMEOUTS.api);
74337
+ return run.ok ? success2(undefined) : run;
74338
+ }
74339
+ };
74340
+ }
74341
+ var GITHUB_MAX_PAGES = 10, PR_FIELDS = `number url state isDraft isInMergeQueue mergeable mergeStateStatus baseRefName baseRefOid baseRef { target { oid } } headRefName headRefOid reviewDecision`, THREADS = `reviewThreads(first: 100, after: $threads) { pageInfo { hasNextPage endCursor } nodes { id isResolved isOutdated path line comments(first: 1) { nodes { author { login __typename } body url } } } }`, CONTEXTS = `contexts(first: 100, after: $contexts) { pageInfo { hasNextPage endCursor } nodes { __typename ... on CheckRun { name status conclusion detailsUrl databaseId isRequired(pullRequestNumber: $number) checkSuite { workflowRun { databaseId workflow { name } } } } ... on StatusContext { context state targetUrl isRequired(pullRequestNumber: $number) } } }`, PR_STATUS_QUERY, FIND_PR_QUERY = `query($owner: String!, $name: String!, $head: String!) { repository(owner: $owner, name: $name) { pullRequests(headRefName: $head, first: 20, orderBy: { field: CREATED_AT, direction: DESC }) { nodes { number url state headRefName headRefOid headRepositoryOwner { login } } } } }`, prState = (value) => value === "MERGED" ? "merged" : value === "CLOSED" ? "closed" : "open", FAILING, safeUrl = (value) => value ? redactText(value) : null, encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
74342
+ var init_github = __esm(() => {
74343
+ init_exec();
74344
+ init_redact();
74345
+ PR_STATUS_QUERY = `query($owner: String!, $name: String!, $number: Int!, $threads: String, $contexts: String) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { ${PR_FIELDS} ${THREADS} commits(last: 1) { nodes { commit { oid statusCheckRollup { state ${CONTEXTS} } } } } } } }`;
74346
+ FAILING = new Set([
74347
+ "FAILURE",
74348
+ "TIMED_OUT",
74349
+ "CANCELLED",
74350
+ "ACTION_REQUIRED",
74351
+ "STARTUP_FAILURE"
74352
+ ]);
74353
+ });
74354
+
74355
+ // packages/workit-core/src/forge/gitlab.ts
74356
+ function checkFromBridge(bridge, pipelineId, scope) {
74357
+ const status = bridge.downstream_pipeline?.status ?? bridge.status;
74358
+ const allow = bridge.allow_failure === true;
74359
+ const failed = status === "failed" || status === "canceled";
74360
+ return {
74361
+ name: `${bridge.stage} / ${bridge.name}`,
74362
+ context: bridge.name,
74363
+ state: stateOf(status, allow),
74364
+ conclusion: failed && allow ? "allowed_failure" : `downstream_${status}`,
74365
+ url: safeUrl2(bridge.downstream_pipeline?.web_url ?? bridge.web_url ?? null),
74366
+ runId: pipelineId,
74367
+ jobId: null,
74368
+ scope,
74369
+ required: !allow
74370
+ };
74371
+ }
74372
+ function checkFromJob(job, pipelineId, scope) {
74373
+ const failed = job.status === "failed" || job.status === "canceled";
74374
+ return {
74375
+ name: `${job.stage} / ${job.name}`,
74376
+ context: job.name,
74377
+ state: stateOf(job.status, job.allow_failure === true),
74378
+ conclusion: failed && job.allow_failure ? "allowed_failure" : job.status,
74379
+ url: safeUrl2(job.web_url ?? null),
74380
+ runId: pipelineId,
74381
+ jobId: job.id,
74382
+ scope,
74383
+ required: job.allow_failure !== true
74384
+ };
74385
+ }
74386
+ function createGitLabForge(options) {
74387
+ const { apiHost, repo, runner } = options;
74388
+ const maxPages = options.maxPages ?? GITLAB_MAX_PAGES;
74389
+ const project = `projects/${encodeURIComponent(repo)}`;
74390
+ const api = (endpoint) => ["api", endpoint];
74391
+ const pages = (endpoint, what) => {
74392
+ const items = [];
74393
+ const sep = endpoint.includes("?") ? "&" : "?";
74394
+ for (let page = 1;page <= maxPages; page += 1) {
74395
+ const chunk = apiJson(runner, "glab", apiHost, api(`${endpoint}${sep}per_page=${PER_PAGE}&page=${page}`), what);
74396
+ if (!chunk.ok)
74397
+ return chunk;
74398
+ if (!Array.isArray(chunk.data))
74399
+ return failure3("failed", `${what}: expected a list`);
74400
+ items.push(...chunk.data);
74401
+ if (chunk.data.length < PER_PAGE)
74402
+ return success2({ items, truncated: false });
74403
+ }
74404
+ return success2({ items, truncated: true });
74405
+ };
74406
+ const mergeCommitTests = (scope, sha, head) => {
74407
+ const commit = apiJson(runner, "glab", apiHost, api(`projects/${scope}/repository/commits/${sha}`), `commit ${sha.slice(0, 12)}`);
74408
+ return commit.ok && Array.isArray(commit.data.parent_ids) && commit.data.parent_ids.includes(head);
74409
+ };
74410
+ const baseTip = (branch) => {
74411
+ const tip = apiJson(runner, "glab", apiHost, api(`${project}/repository/branches/${encodeURIComponent(branch)}`), `branch ${branch}`);
74412
+ return tip.ok && typeof tip.data.commit?.id === "string" ? tip.data.commit.id : null;
74413
+ };
74414
+ return {
74415
+ kind: "gitlab",
74416
+ apiHost,
74417
+ repo,
74418
+ identity(expected) {
74419
+ const user = apiJson(runner, "glab", apiHost, api("user"), "user");
74420
+ if (!user.ok && user.code === "failed" && /\b403\b|forbidden/iu.test(user.error))
74421
+ return success2({
74422
+ login: null,
74423
+ expected,
74424
+ matches: null,
74425
+ note: "the credential cannot read /user (job or project token); identity not checked"
74426
+ });
74427
+ if (!user.ok)
74428
+ return user;
74429
+ const login = typeof user.data.username === "string" ? user.data.username : "";
74430
+ if (!login)
74431
+ return failure3("failed", "glab api user returned no username");
74432
+ return success2({
74433
+ login,
74434
+ expected,
74435
+ matches: expected ? login.toLowerCase() === expected.toLowerCase() : null
74436
+ });
74437
+ },
74438
+ repoInfo(target) {
74439
+ const info = apiJson(runner, "glab", apiHost, api(`projects/${encodeURIComponent(target)}`), `project ${target}`);
74440
+ if (!info.ok)
74441
+ return info;
74442
+ return success2({
74443
+ id: typeof info.data.id === "number" ? info.data.id : null,
74444
+ parent: info.data.forked_from_project?.path_with_namespace ?? null
74445
+ });
74446
+ },
74447
+ findPr(head, match) {
74448
+ const list = apiJson(runner, "glab", apiHost, api(`${project}/merge_requests?source_branch=${encodeURIComponent(head)}&order_by=created_at&sort=desc&per_page=20`), `merge requests for ${head}`);
74449
+ if (!list.ok)
74450
+ return list;
74451
+ if (!Array.isArray(list.data))
74452
+ return failure3("failed", "merge request list is not a list");
74453
+ const matches = list.data.filter((mr) => mr.source_branch === head && (match.projectId !== null && mr.source_project_id === match.projectId || match.sha !== null && mr.sha === match.sha));
74454
+ const pick = matches.find((mr) => mr.state === "opened") ?? matches[0];
74455
+ return success2(pick ? {
74456
+ number: pick.iid,
74457
+ url: pick.web_url,
74458
+ state: mrState(pick.state),
74459
+ headBranch: pick.source_branch,
74460
+ headSha: pick.sha ?? null
74461
+ } : null);
74462
+ },
74463
+ prStatus(iid) {
74464
+ const mr = apiJson(runner, "glab", apiHost, api(`${project}/merge_requests/${iid}`), `merge request !${iid}`);
74465
+ if (!mr.ok)
74466
+ return mr;
74467
+ const value = mr.data;
74468
+ let truncated = false;
74469
+ let checks = [];
74470
+ const pipeline = value.head_pipeline ?? null;
74471
+ const open = value.state === "opened";
74472
+ const current = pipeline !== null && (pipeline.sha === value.sha || pipeline.source_sha === value.sha || !pipeline.source_sha && new RegExp(`^refs/merge-requests/${value.iid}/(?:merge|train)$`, "u").test(pipeline.ref ?? "") && mergeCommitTests(pipeline.project_id, pipeline.sha, value.sha));
74473
+ if (pipeline && current) {
74474
+ const scope = String(pipeline.project_id);
74475
+ const jobs = pages(`projects/${scope}/pipelines/${pipeline.id}/jobs`, `pipeline ${pipeline.id} jobs`);
74476
+ if (!jobs.ok)
74477
+ return jobs;
74478
+ const bridges = pages(`projects/${scope}/pipelines/${pipeline.id}/bridges`, `pipeline ${pipeline.id} bridges`);
74479
+ if (!bridges.ok)
74480
+ return bridges;
74481
+ truncated ||= jobs.data.truncated || bridges.data.truncated;
74482
+ checks = [
74483
+ ...jobs.data.items.map((job) => checkFromJob(job, pipeline.id, scope)),
74484
+ ...bridges.data.items.map((bridge) => checkFromBridge(bridge, pipeline.id, scope))
74485
+ ];
74486
+ const pipelineState = stateOf(pipeline.status, false);
74487
+ if (pipelineState === "failing" && !checks.some((check) => check.state === "failing"))
74488
+ checks.push(pipelineCheck("failing", pipeline.status, pipeline));
74489
+ if (pipelineState === "pending" && !checks.some((check) => check.state === "pending"))
74490
+ checks.push(pipelineCheck("pending", pipeline.status, pipeline));
74491
+ } else if (open && pipeline) {
74492
+ checks = [pipelineCheck("pending", "awaiting_pipeline_for_head", null)];
74493
+ }
74494
+ const discussions = pages(`${project}/merge_requests/${iid}/discussions`, `merge request !${iid} discussions`);
74495
+ if (!discussions.ok)
74496
+ return discussions;
74497
+ truncated ||= discussions.data.truncated;
74498
+ const threads = discussions.data.items.filter((discussion) => discussion.notes.some((note) => note.resolvable === true && note.resolved !== true)).map((discussion) => {
74499
+ const note = discussion.notes[0];
74500
+ return {
74501
+ id: discussion.id,
74502
+ path: note.position?.new_path ?? note.position?.old_path ?? null,
74503
+ line: note.position?.new_line ?? null,
74504
+ author: note.author?.username ?? null,
74505
+ isBot: isBot(note.author),
74506
+ outdated: false,
74507
+ url: safeUrl2(`${value.web_url}#note_${note.id}`),
74508
+ body: shortBody(note.body ?? "")
74509
+ };
74510
+ });
74511
+ const detailed = value.detailed_merge_status ?? "unchecked";
74512
+ const conflicts = value.has_conflicts === true || detailed === "conflict";
74513
+ const status = {
74514
+ number: value.iid,
74515
+ url: value.web_url,
74516
+ state: mrState(value.state),
74517
+ draft: value.draft ?? value.work_in_progress ?? false,
74518
+ base: value.target_branch,
74519
+ baseSha: open ? baseTip(value.target_branch) : null,
74520
+ head: { branch: value.source_branch, sha: value.sha },
74521
+ mergeable: conflicts ? "no" : UNSETTLED.has(detailed) ? "unknown" : "yes",
74522
+ conflicts,
74523
+ rebaseRequired: detailed === "need_rebase",
74524
+ reviewDecision: detailed === "not_approved" ? "review_required" : detailed === "requested_changes" ? "changes_requested" : null,
74525
+ mergeState: detailed,
74526
+ inMergeQueue: false,
74527
+ checks,
74528
+ requiredContexts: [],
74529
+ threads,
74530
+ truncated
74531
+ };
74532
+ return success2(status);
74533
+ },
74534
+ jobLogTail(job, lines) {
74535
+ const scope = job.scope ?? encodeURIComponent(repo);
74536
+ const text = apiText(runner, "glab", apiHost, ["api", `projects/${scope}/jobs/${job.jobId}/trace`], `job ${job.jobId} trace`, FORGE_TIMEOUTS.log);
74537
+ return text.ok ? success2(logTail(text.data, lines)) : text;
74538
+ },
74539
+ rerun(target) {
74540
+ const scope = target.scope ?? encodeURIComponent(repo);
74541
+ const endpoint = target.kind === "failed_in_run" ? `projects/${scope}/pipelines/${target.runId}/retry` : `projects/${scope}/jobs/${target.jobId}/retry`;
74542
+ const what = target.kind === "failed_in_run" ? `retry of pipeline ${target.runId}` : `retry of job ${target.jobId}`;
74543
+ const run = apiText(runner, "glab", apiHost, ["api", "-X", "POST", endpoint], what, FORGE_TIMEOUTS.api);
74544
+ return run.ok ? success2(undefined) : run;
74545
+ }
74546
+ };
74547
+ }
74548
+ var GITLAB_MAX_PAGES = 10, PER_PAGE = 100, mrState = (value) => value === "merged" ? "merged" : value === "opened" ? "open" : "closed", PENDING, stateOf = (status, allowFailure) => {
74549
+ const failed = status === "failed" || status === "canceled";
74550
+ return PENDING.has(status) ? "pending" : failed ? allowFailure ? "passing" : "failing" : status === "success" ? "passing" : "skipped";
74551
+ }, pipelineCheck = (state, conclusion, pipeline) => ({
74552
+ name: "pipeline",
74553
+ context: "pipeline",
74554
+ state,
74555
+ conclusion,
74556
+ url: safeUrl2(pipeline?.web_url ?? null),
74557
+ runId: pipeline?.id ?? null,
74558
+ jobId: null,
74559
+ scope: pipeline ? String(pipeline.project_id) : null,
74560
+ required: true
74561
+ }), safeUrl2 = (value) => value ? redactText(value) : null, isBot = (author) => author?.bot === true || /(?:^project_\d+_bot|bot$|\[bot\]$)/iu.test(author?.username ?? ""), UNSETTLED;
74562
+ var init_gitlab = __esm(() => {
74563
+ init_exec();
74564
+ init_redact();
74565
+ PENDING = new Set([
74566
+ "created",
74567
+ "pending",
74568
+ "running",
74569
+ "waiting_for_resource",
74570
+ "waiting_for_callback",
74571
+ "preparing",
74572
+ "scheduled"
74573
+ ]);
74574
+ UNSETTLED = new Set(["checking", "unchecked", "preparing", "approvals_syncing"]);
74575
+ });
74576
+
74577
+ // packages/workit-core/src/forge/resolve.ts
74578
+ import fs18 from "node:fs";
74579
+ function resolveForge(cwd, options = {}) {
74580
+ let workspace;
74581
+ try {
74582
+ workspace = resolveRuntimeWorkspaceVcs(cwd);
74583
+ } catch (error) {
74584
+ return failure3("blocked", error instanceof Error ? error.message : String(error), "fix ~/.config/workit/workspaces.json (workit init → Workspaces)");
74585
+ }
74586
+ const hosts = configuredHosts();
74587
+ const pushed = pushForge(cwd, { branch: options.branch, hosts, sshConfig: options.sshConfig });
74588
+ if (!pushed.ok)
74589
+ return failure3(pushed.code, pushed.error, pushed.unblock);
74590
+ const conflict = forgeConflict(pushed.forge, workspace?.vcs?.provider);
74591
+ if (conflict)
74592
+ return failure3("blocked", conflict.error, conflict.unblock);
74593
+ const derived = pushed.forge;
74594
+ const { kind, apiHost } = derived;
74595
+ const base = options.runner ?? systemRunner(kind, apiHost, options.env ?? process.env);
74596
+ const now = options.now ?? Date.now;
74597
+ const expectedAccount = workspace?.vcs?.account ?? null;
74598
+ let token;
74599
+ let credential = "cli_login";
74600
+ const tokenFile = workspace?.vcs?.tokenFile;
74601
+ if (typeof tokenFile === "string" && tokenFile.trim()) {
74602
+ const workspaceHost = workspace?.vcs?.host;
74603
+ const explicit = typeof workspaceHost === "string" && workspaceHost.trim().toLowerCase().replace(/^https?:\/\//u, "").replace(/\/.*$/u, "") === apiHost;
74604
+ if (derived.via === "host_name" && !explicit)
74605
+ return failure3("blocked", `token_host_unverified: push host ${derived.host} only looks like ${kind}; workspace ${workspace?.name ?? "?"} vcs.tokenFile is not sent to it`, `if ${apiHost} is your ${kind} server, set ${kind}.host in ~/.config/workit/vcs.json (or vcs.host for the workspace); otherwise fix the push remote`);
74606
+ try {
74607
+ token = fs18.readFileSync(tokenFile.trim(), "utf8").trim();
74608
+ } catch {
74609
+ return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile cannot be read`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
74610
+ }
74611
+ if (!token)
74612
+ return failure3("blocked", `workspace ${workspace?.name ?? "?"} vcs.tokenFile is empty`, "fix vcs.tokenFile in ~/.config/workit/workspaces.json");
74613
+ credential = "workspace_token_file";
74614
+ } else if (expectedAccount && kind === "github") {
74615
+ const issued = base("gh", ["auth", "token", "--hostname", apiHost, "--user", expectedAccount], {
74616
+ timeoutMs: FORGE_TIMEOUTS.api
74617
+ });
74618
+ if (issued.missing)
74619
+ return failure3("unavailable", "gh is not installed", "install the GitHub CLI (https://cli.github.com) and run: gh auth login");
74620
+ token = issued.status === 0 ? issued.stdout.trim() : "";
74621
+ if (!token)
74622
+ return failure3("blocked", `identity_unavailable: gh has no login for ${expectedAccount} on ${apiHost} (workspace ${workspace?.name ?? "?"})`, `gh auth login --hostname ${apiHost} # sign in as ${expectedAccount}; workit passes that account's token per call and never switches the active account`);
74623
+ credential = "gh_account_token";
74624
+ }
74625
+ const limits = { deadline: null };
74626
+ const runner = (bin, args, callOptions) => {
74627
+ let timeoutMs = callOptions.timeoutMs;
74628
+ if (limits.deadline !== null) {
74629
+ const remaining = limits.deadline - now();
74630
+ if (remaining <= 0)
74631
+ return { status: null, stdout: "", stderr: "", timedOut: true, missing: false };
74632
+ timeoutMs = Math.min(timeoutMs, remaining);
74633
+ }
74634
+ return base(bin, args, { ...callOptions, timeoutMs, ...token ? { token } : {} });
74635
+ };
74636
+ const headRepo = derived.repo;
74637
+ const probe = createForge(derived, headRepo, runner);
74638
+ const info = probe.repoInfo(headRepo);
74639
+ if (!info.ok && info.code !== "not_found")
74640
+ return info;
74641
+ const headProjectId = info.ok ? info.data.id : null;
74642
+ const remotes = remoteNames(cwd);
74643
+ const repoOfRemote = (name) => {
74644
+ const url = pushUrl(cwd, name);
74645
+ const other = url ? deriveForge(url, { hosts, sshConfig: options.sshConfig }) : null;
74646
+ return other && other.kind === kind && other.apiHost === apiHost ? other.repo : null;
74647
+ };
74648
+ const upstream = pushed.remote !== "upstream" && remotes.includes("upstream") ? repoOfRemote("upstream") : null;
74649
+ const baseRepo = upstream && upstream !== headRepo ? upstream : info.ok && info.data.parent ? info.data.parent : headRepo;
74650
+ const baseRemote = baseRepo === headRepo ? pushed.remote : remotes.find((name) => repoOfRemote(name) === baseRepo) ?? pushed.remote;
74651
+ return success2({
74652
+ forge: baseRepo === headRepo ? probe : createForge(derived, baseRepo, runner),
74653
+ remote: pushed.remote,
74654
+ url: pushed.url,
74655
+ derived,
74656
+ workspace: workspace?.name ?? null,
74657
+ expectedAccount,
74658
+ credential,
74659
+ headRepo,
74660
+ headProjectId,
74661
+ fork: baseRepo !== headRepo,
74662
+ baseRemote,
74663
+ limits
74664
+ });
74665
+ }
74666
+ function checkIdentity(resolved) {
74667
+ const { forge, expectedAccount } = resolved;
74668
+ const identity = forge.identity(expectedAccount);
74669
+ if (!identity.ok)
74670
+ return identity;
74671
+ if (identity.data.matches === false) {
74672
+ const bin = forge.kind === "github" ? "gh" : "glab";
74673
+ const where = `workspace ${resolved.workspace ?? "?"}`;
74674
+ return failure3("blocked", `identity_mismatch: the ${resolved.credential === "workspace_token_file" ? "vcs.tokenFile token" : `${bin} login`} is ${identity.data.login} but ${where} expects ${expectedAccount}`, resolved.credential === "workspace_token_file" ? `put a token for ${expectedAccount} in the ${where} vcs.tokenFile` : `${bin} auth login --hostname ${forge.apiHost} # sign in as ${expectedAccount}`);
74675
+ }
74676
+ return identity;
74677
+ }
74678
+ var configuredHosts = () => {
74679
+ const vcs = readVcsConfig();
74680
+ if (vcs.status !== "valid")
74681
+ return {};
74682
+ const host = (kind) => {
74683
+ const value = vcs.config[kind]?.host;
74684
+ return typeof value === "string" && value.trim() ? [value.trim()] : [];
74685
+ };
74686
+ return { github: host("github"), gitlab: host("gitlab") };
74687
+ }, createForge = (derived, repo, runner) => derived.kind === "github" ? createGitHubForge({ apiHost: derived.apiHost, repo, runner }) : createGitLabForge({ apiHost: derived.apiHost, repo, runner });
74688
+ var init_resolve = __esm(() => {
74689
+ init_vcs_config();
74690
+ init_workspaces();
74691
+ init_rev();
74692
+ init_exec();
74693
+ init_github();
74694
+ init_gitlab();
74695
+ });
74696
+
74697
+ // packages/workit-cli/src/verbs/forge-common.ts
74698
+ function parseFlags(argv, spec) {
74699
+ const parsed = { values: {}, booleans: new Set, lists: {}, positionals: [] };
74700
+ for (let index = 0;index < argv.length; index += 1) {
74701
+ const arg = argv[index];
74702
+ if (arg === "--json")
74703
+ continue;
74704
+ if (!arg.startsWith("--")) {
74705
+ parsed.positionals.push(arg);
74706
+ continue;
74707
+ }
74708
+ const eq = arg.indexOf("=");
74709
+ const name = arg.slice(2, eq < 0 ? undefined : eq);
74710
+ const kind = spec[name];
74711
+ if (!kind)
74712
+ return `unknown option --${name}`;
74713
+ if (kind === "boolean") {
74714
+ if (eq >= 0)
74715
+ return `--${name} takes no value`;
74716
+ parsed.booleans.add(name);
74717
+ continue;
74718
+ }
74719
+ const value = eq >= 0 ? arg.slice(eq + 1) : argv[++index];
74720
+ if (value === undefined || value === "" || eq < 0 && value.startsWith("--"))
74721
+ return `--${name} requires a value`;
74722
+ if (kind === "list")
74723
+ (parsed.lists[name] ??= []).push(value);
74724
+ else
74725
+ parsed.values[name] = value;
74726
+ }
74727
+ return parsed;
74728
+ }
74729
+ function positiveInt(value, flag) {
74730
+ if (value === undefined)
74731
+ return null;
74732
+ return /^[1-9]\d{0,8}$/u.test(value) ? Number(value) : `${flag} must be a positive integer`;
74733
+ }
74734
+ function parseDuration2(value) {
74735
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/u.exec(value.trim());
74736
+ if (!match)
74737
+ return null;
74738
+ const unit = match[2] ?? "s";
74739
+ const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
74740
+ return Math.round(Number(match[1]) * factor);
74741
+ }
74742
+ function connect(io, branch) {
74743
+ const resolved = resolveForge(io.cwd, {
74744
+ branch,
74745
+ env: io.env,
74746
+ runner: forgeDeps.runner,
74747
+ now: forgeDeps.now
74748
+ });
74749
+ if (!resolved.ok)
74750
+ return resolved;
74751
+ const identity = checkIdentity(resolved.data);
74752
+ if (!identity.ok)
74753
+ return identity;
74754
+ return {
74755
+ ok: true,
74756
+ data: {
74757
+ ...resolved.data,
74758
+ identity: {
74759
+ login: identity.data.login,
74760
+ credential: resolved.data.credential,
74761
+ ...identity.data.note ? { note: identity.data.note } : {}
74762
+ }
74763
+ }
74764
+ };
74765
+ }
74766
+ function renderStatus(doc) {
74767
+ const noun = doc.forge === "github" ? `PR #${doc.number}` : `MR !${doc.number}`;
74768
+ const lines = [
74769
+ `${noun} ${doc.state}${doc.draft ? " (draft)" : ""}: ${doc.headRepo ? `${doc.headRepo}:` : ""}${doc.head.branch} -> ${doc.base} ${doc.url}`
74770
+ ];
74771
+ const local = doc.head.localSha === null ? "not checked out" : doc.head.pushed ? "local matches" : `local ${short(doc.head.localSha)} differs`;
74772
+ lines.push(`head ${short(doc.head.sha)} (${local})`);
74773
+ const behind = doc.behindBase;
74774
+ const behindText = !behind ? "" : behind.behind === null ? ` · behind ${doc.base}: unknown (${behind.error ?? "unavailable"})` : ` · behind ${doc.base}: ${behind.behind}, ahead ${behind.ahead}`;
74775
+ lines.push(`mergeable ${doc.mergeable} · conflicts ${doc.conflicts ? "yes" : "no"}${doc.rebaseRequired ? " · rebase required" : ""}${behindText}`);
74776
+ const checks = doc.checks;
74777
+ lines.push(`checks ${checks.state}: ${checks.failing.length} failing, ${checks.pending.length} pending, ${checks.passing} passing`);
74778
+ for (const check of checks.failing) {
74779
+ lines.push(` x ${check.name} (${check.conclusion ?? "failed"}${check.required === false ? ", optional" : ""})${check.rerunsOnHead ? ` [rerun ${check.rerunsOnHead}x on this head]` : ""}${check.url ? ` ${check.url}` : ""}`);
74780
+ for (const value of check.logTail)
74781
+ lines.push(` ${value}`);
74782
+ if (check.logError)
74783
+ lines.push(` (log unavailable: ${check.logError})`);
74784
+ }
74785
+ if (checks.pending.length)
74786
+ lines.push(` pending: ${checks.pending.join(", ")}`);
74787
+ if (checks.missingRequired === null)
74788
+ lines.push(" required checks: unknown (branch protection not readable)");
74789
+ else if (checks.missingRequired.length)
74790
+ lines.push(` required, not reported yet: ${checks.missingRequired.join(", ")}`);
74791
+ const threads = doc.reviews.unresolvedThreads;
74792
+ lines.push(`reviews ${doc.reviews.decision ?? "none"} · ${threads.length} unresolved thread${threads.length === 1 ? "" : "s"}`);
74793
+ for (const thread of threads)
74794
+ lines.push(` - ${thread.path ? `${thread.path}${thread.line ? `:${thread.line}` : ""} ` : ""}@${thread.author ?? "?"}${thread.isBot ? " (bot)" : ""}: ${thread.body}`);
74795
+ if (doc.truncated)
74796
+ lines.push("(lists truncated at the page cap)");
74797
+ if (doc.blockers.length)
74798
+ lines.push(`blockers: ${doc.blockers.join(", ")}`);
74799
+ if (doc.identity?.note)
74800
+ lines.push(`identity: ${doc.identity.note}`);
74801
+ lines.push(`next: ${doc.next}`);
74802
+ return lines;
74803
+ }
74804
+ var forgeDeps, usage3 = (io, message, line) => emit(io, fail("invalid_input", message, { unblock: line })), forgeFail = (io, result, data) => emit(io, fail(result.code, result.error, { data, unblock: result.unblock })), short = (sha) => sha ? sha.slice(0, 7) : "?";
74805
+ var init_forge_common = __esm(() => {
74806
+ init_resolve();
74807
+ init_output();
74808
+ forgeDeps = {
74809
+ runner: undefined,
74810
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
74811
+ now: () => Date.now()
74812
+ };
74813
+ });
74814
+
73019
74815
  // packages/workit-cli/src/verbs/pr.ts
73020
74816
  var exports_pr = {};
73021
74817
  __export(exports_pr, {
73022
74818
  run: () => run13
73023
74819
  });
73024
- var run13;
74820
+ async function status(argv, io) {
74821
+ const flags = parseFlags(argv, { pr: "value", branch: "value", "log-lines": "value" });
74822
+ if (typeof flags === "string")
74823
+ return usage3(io, flags, STATUS_USAGE);
74824
+ if (flags.positionals.length)
74825
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, STATUS_USAGE);
74826
+ const pr = positiveInt(flags.values.pr, "--pr");
74827
+ if (typeof pr === "string")
74828
+ return usage3(io, pr, STATUS_USAGE);
74829
+ if (pr && flags.values.branch)
74830
+ return usage3(io, "pass --pr or --branch, not both", STATUS_USAGE);
74831
+ const logLines = flags.values["log-lines"] === "0" ? 0 : positiveInt(flags.values["log-lines"], "--log-lines");
74832
+ if (typeof logLines === "string")
74833
+ return usage3(io, logLines, STATUS_USAGE);
74834
+ if (logLines !== null && logLines > 500)
74835
+ return usage3(io, "--log-lines is capped at 500", STATUS_USAGE);
74836
+ const connected = connect(io, flags.values.branch);
74837
+ if (!connected.ok)
74838
+ return forgeFail(io, connected);
74839
+ const report = prStatusReport(io.cwd, connected.data, {
74840
+ pr,
74841
+ branch: flags.values.branch ?? null,
74842
+ logLines: logLines ?? 60,
74843
+ identity: connected.data.identity
74844
+ });
74845
+ if (!report.ok)
74846
+ return forgeFail(io, report);
74847
+ return emit(io, ok(report.data.doc), renderStatus);
74848
+ }
74849
+ async function run13(argv, io) {
74850
+ const [sub, ...rest] = argv;
74851
+ if (sub === "status")
74852
+ return status(rest, io);
74853
+ if (sub === "create" || sub === "merge")
74854
+ return later(argv, io);
74855
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown pr subcommand "${sub}"` : "missing pr subcommand", STATUS_USAGE);
74856
+ }
74857
+ var STATUS_USAGE = "workit pr status [--pr <n> | --branch <b>] [--log-lines 60] [--json]", later;
73025
74858
  var init_pr = __esm(() => {
74859
+ init_report();
74860
+ init_output();
74861
+ init_forge_common();
73026
74862
  init_stub();
73027
- run13 = notImplemented("pr", "S10/S11");
74863
+ later = notImplemented("pr", "S11");
73028
74864
  });
73029
74865
 
73030
74866
  // packages/workit-cli/src/verbs/ci.ts
@@ -73032,10 +74868,181 @@ var exports_ci = {};
73032
74868
  __export(exports_ci, {
73033
74869
  run: () => run14
73034
74870
  });
73035
- var run14;
74871
+ async function wait(argv, io) {
74872
+ const flags = parseFlags(argv, {
74873
+ pr: "value",
74874
+ branch: "value",
74875
+ head: "value",
74876
+ timeout: "value",
74877
+ interval: "value"
74878
+ });
74879
+ if (typeof flags === "string")
74880
+ return usage3(io, flags, WAIT_USAGE);
74881
+ if (flags.positionals.length)
74882
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, WAIT_USAGE);
74883
+ const pr = positiveInt(flags.values.pr, "--pr");
74884
+ if (typeof pr === "string")
74885
+ return usage3(io, pr, WAIT_USAGE);
74886
+ const timeoutMs = parseDuration2(flags.values.timeout ?? "20m");
74887
+ const intervalMs = parseDuration2(flags.values.interval ?? "30s");
74888
+ if (timeoutMs === null || timeoutMs > 6 * 3600000)
74889
+ return usage3(io, "--timeout must be a duration up to 6h (20m, 90s)", WAIT_USAGE);
74890
+ if (intervalMs === null || intervalMs < 1000 || intervalMs > 600000)
74891
+ return usage3(io, "--interval must be between 1s and 10m", WAIT_USAGE);
74892
+ if (pr && flags.values.branch)
74893
+ return usage3(io, "pass --pr or --branch, not both", WAIT_USAGE);
74894
+ const head = flags.values.head ?? null;
74895
+ if (head !== null && !/^[0-9a-f]{7,64}$/u.test(head))
74896
+ return usage3(io, "--head must be a commit sha (7-64 hex chars)", WAIT_USAGE);
74897
+ const connected = connect(io, flags.values.branch);
74898
+ if (!connected.ok)
74899
+ return forgeFail(io, connected);
74900
+ const resolved = connected.data;
74901
+ const number = selectPr(io.cwd, resolved, { pr, branch: flags.values.branch ?? null });
74902
+ if (!number.ok)
74903
+ return forgeFail(io, number);
74904
+ const started = forgeDeps.now();
74905
+ const deadline = started + timeoutMs;
74906
+ resolved.limits.deadline = started + Math.max(timeoutMs, ONE_POLL_MS);
74907
+ let polls = 0;
74908
+ let errors = 0;
74909
+ let status = null;
74910
+ let verdict = { state: "waiting", reason: "not_polled" };
74911
+ for (;; ) {
74912
+ const read = resolved.forge.prStatus(number.data);
74913
+ polls += 1;
74914
+ const elapsedMs = forgeDeps.now() - started;
74915
+ if (read.ok) {
74916
+ errors = 0;
74917
+ status = read.data;
74918
+ const doc = buildStatusDoc(io.cwd, resolved, status, { logLines: 0, behind: false });
74919
+ if (!doc.ok)
74920
+ return forgeFail(io, doc);
74921
+ verdict = waitVerdict(doc.data, { head, elapsedMs });
74922
+ if (verdict.state !== "waiting")
74923
+ break;
74924
+ } else {
74925
+ errors += 1;
74926
+ if (read.code !== "unavailable" && read.code !== "failed")
74927
+ return forgeFail(io, read);
74928
+ if (errors >= MAX_POLL_ERRORS)
74929
+ return forgeFail(io, read, { polls });
74930
+ }
74931
+ const remaining = deadline - forgeDeps.now();
74932
+ if (remaining <= 0)
74933
+ break;
74934
+ await forgeDeps.sleep(Math.min(pollDelay(intervalMs, polls - 1), remaining));
74935
+ }
74936
+ const elapsedMs = forgeDeps.now() - started;
74937
+ if (!status)
74938
+ return emit(io, fail("unavailable", "no successful poll before the timeout", { data: { polls } }));
74939
+ const left = deadline - forgeDeps.now();
74940
+ const built = buildStatusDoc(io.cwd, resolved, status, {
74941
+ logLines: verdict.state === "failed" && left > 0 ? 60 : 0,
74942
+ behind: left >= 1000,
74943
+ behindTimeoutMs: Math.max(1000, Math.min(15000, left)),
74944
+ identity: resolved.identity
74945
+ });
74946
+ if (!built.ok)
74947
+ return forgeFail(io, built);
74948
+ const doc = built.data;
74949
+ const data = {
74950
+ state: verdict.state,
74951
+ reason: verdict.reason,
74952
+ polls,
74953
+ elapsedMs,
74954
+ timeoutMs,
74955
+ status: doc
74956
+ };
74957
+ if (!io.json && verdict.state !== "ready")
74958
+ for (const value of renderStatus(doc))
74959
+ io.stdout(`${value}
74960
+ `);
74961
+ if (verdict.state === "ready")
74962
+ return emit(io, ok(data), () => [
74963
+ `CI ready (${verdict.reason}) after ${polls} poll${polls === 1 ? "" : "s"}, ${seconds(elapsedMs)}`,
74964
+ ...renderStatus(doc)
74965
+ ]);
74966
+ if (verdict.state === "failed")
74967
+ return emit(io, fail("failed", `CI failed: ${doc.checks.failing.map((check) => check.name).join(", ")}`, {
74968
+ data,
74969
+ unblock: "fix the failure, or if it is a flake: workit ci rerun --failed --reason flake"
74970
+ }));
74971
+ if (verdict.state === "blocked")
74972
+ return emit(io, fail("blocked", `CI wait blocked: ${verdict.reason}`, { data, unblock: verdict.unblock }));
74973
+ return emit(io, fail("pending", `CI still ${verdict.reason.replaceAll("_", " ")} after ${seconds(elapsedMs)}`, {
74974
+ data,
74975
+ unblock: `workit ci wait --pr ${doc.number}${head ? ` --head ${head}` : ""}`
74976
+ }));
74977
+ }
74978
+ async function rerun(argv, io) {
74979
+ const flags = parseFlags(argv, {
74980
+ pr: "value",
74981
+ branch: "value",
74982
+ check: "list",
74983
+ failed: "boolean",
74984
+ reason: "value",
74985
+ force: "boolean"
74986
+ });
74987
+ if (typeof flags === "string")
74988
+ return usage3(io, flags, RERUN_USAGE);
74989
+ if (flags.positionals.length)
74990
+ return usage3(io, `unexpected argument ${flags.positionals[0]}`, RERUN_USAGE);
74991
+ const pr = positiveInt(flags.values.pr, "--pr");
74992
+ if (typeof pr === "string")
74993
+ return usage3(io, pr, RERUN_USAGE);
74994
+ const names = flags.lists.check ?? [];
74995
+ if (pr && flags.values.branch)
74996
+ return usage3(io, "pass --pr or --branch, not both", RERUN_USAGE);
74997
+ if (names.length && flags.booleans.has("failed"))
74998
+ return usage3(io, "pass --check or --failed, not both", RERUN_USAGE);
74999
+ const reason = flags.values.reason;
75000
+ if (reason !== "flake" && reason !== "infra")
75001
+ return usage3(io, "--reason flake|infra is required: rerun only what you judged a flake or an infrastructure failure", RERUN_USAGE);
75002
+ const connected = connect(io, flags.values.branch);
75003
+ if (!connected.ok)
75004
+ return forgeFail(io, connected);
75005
+ const resolved = connected.data;
75006
+ if (resolved.expectedAccount && resolved.identity.login === null)
75007
+ return emit(io, fail("blocked", `identity_unverified: the credential cannot prove it is ${resolved.expectedAccount} (${resolved.identity.note ?? "no /user access"})`, {
75008
+ unblock: `use a credential for ${resolved.expectedAccount} that can read /user (vcs.tokenFile or ${resolved.forge.kind === "github" ? "gh" : "glab"} auth login --hostname ${resolved.forge.apiHost})`
75009
+ }));
75010
+ const number = selectPr(io.cwd, resolved, { pr, branch: flags.values.branch ?? null });
75011
+ if (!number.ok)
75012
+ return forgeFail(io, number);
75013
+ const status = resolved.forge.prStatus(number.data);
75014
+ if (!status.ok)
75015
+ return forgeFail(io, status);
75016
+ const done = executeRerun(io.cwd, resolved, status.data, {
75017
+ failed: names.length === 0,
75018
+ names,
75019
+ reason,
75020
+ force: flags.booleans.has("force")
75021
+ });
75022
+ if (!done.ok)
75023
+ return forgeFail(io, done);
75024
+ const outcome = done.data;
75025
+ return emit(io, ok(outcome), () => [
75026
+ `rerun requested on #${outcome.pr} head ${outcome.head.slice(0, 7)} (${outcome.reason}${outcome.forced ? ", forced" : ""}): ${outcome.rerun.map((check) => check.name).join(", ")}`,
75027
+ ...outcome.skipped.length ? [`not rerunnable here: ${outcome.skipped.join(", ")}`] : [],
75028
+ ...outcome.recorded ? [] : ["warning: the rerun could not be recorded; the once-per-head guard will not see it"],
75029
+ "next: workit ci wait"
75030
+ ]);
75031
+ }
75032
+ async function run14(argv, io) {
75033
+ const [sub, ...rest] = argv;
75034
+ if (sub === "wait")
75035
+ return wait(rest, io);
75036
+ if (sub === "rerun")
75037
+ return rerun(rest, io);
75038
+ return usage3(io, sub && !sub.startsWith("-") ? `unknown ci subcommand "${sub}"` : "missing ci subcommand", `${WAIT_USAGE}
75039
+ ${RERUN_USAGE}`);
75040
+ }
75041
+ var WAIT_USAGE = "workit ci wait [--pr <n> | --branch <b>] [--head <sha>] [--timeout 20m] [--interval 30s] [--json]", RERUN_USAGE = "workit ci rerun [--pr <n> | --branch <b>] [--check <name>… | --failed] --reason flake|infra [--force] [--json]", ONE_POLL_MS = 20000, MAX_POLL_ERRORS = 3, seconds = (ms) => `${Math.round(ms / 1000)}s`;
73036
75042
  var init_ci = __esm(() => {
73037
- init_stub();
73038
- run14 = notImplemented("ci", "S10");
75043
+ init_report();
75044
+ init_output();
75045
+ init_forge_common();
73039
75046
  });
73040
75047
 
73041
75048
  // packages/workit-cli/src/verbs/git.ts
@@ -73071,126 +75078,11 @@ var init_stack = __esm(() => {
73071
75078
  run17 = notImplemented("stack", "S12");
73072
75079
  });
73073
75080
 
73074
- // packages/workit-core/src/git/rev.ts
73075
- import { spawnSync as spawnSync13 } from "node:child_process";
73076
- import { createHash as createHash12 } from "node:crypto";
73077
- import fs15 from "node:fs";
73078
- import os12 from "node:os";
73079
- import path34 from "node:path";
73080
- function headSha(cwd) {
73081
- return line(git(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
73082
- }
73083
- function worktreeTree(cwd, options = {}) {
73084
- const top = line(git(cwd, ["rev-parse", "--show-toplevel"]));
73085
- const indexPath = line(git(cwd, ["rev-parse", "--path-format=absolute", "--git-path", "index"]));
73086
- if (!top || !indexPath)
73087
- return null;
73088
- const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
73089
- const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
73090
- const scratch = fs15.mkdtempSync(path34.join(os12.tmpdir(), "workit-tree-"));
73091
- try {
73092
- const tempIndex = path34.join(scratch, "index");
73093
- if (fs15.existsSync(indexPath))
73094
- seedIndex(indexPath, tempIndex);
73095
- const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
73096
- const untracked = git(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
73097
- env,
73098
- timeoutMs
73099
- });
73100
- if (!untracked.ok)
73101
- return null;
73102
- const skipped = [];
73103
- for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
73104
- try {
73105
- const stat = fs15.statSync(path34.join(top, file));
73106
- if (stat.isFile() && stat.size > cap)
73107
- skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
73108
- } catch {}
73109
- }
73110
- skipped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
73111
- const added = skipped.length ? git(top, ["add", "-A", "--pathspec-from-file=-", "--pathspec-file-nul"], {
73112
- env,
73113
- timeoutMs,
73114
- input: [".", ...skipped.map((file) => `:(exclude,literal)${file.path}`)].join("\x00")
73115
- }) : git(top, ["add", "-A"], { env, timeoutMs });
73116
- if (!added.ok)
73117
- return null;
73118
- const tree = line(git(top, ["write-tree"], { env, timeoutMs }));
73119
- if (!tree)
73120
- return null;
73121
- const base = line(git(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
73122
- const key = skipped.length ? `sha256:${createHash12("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
73123
- return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
73124
- } finally {
73125
- fs15.rmSync(scratch, { recursive: true, force: true });
73126
- }
73127
- }
73128
- function patchId(cwd, base, head) {
73129
- if (!safeArg(base) || !safeArg(head))
73130
- return null;
73131
- const diff = git(cwd, [
73132
- "diff",
73133
- "-U3",
73134
- "--diff-algorithm=myers",
73135
- "--indent-heuristic",
73136
- "--no-color",
73137
- "--no-ext-diff",
73138
- "--no-textconv",
73139
- "--no-renames",
73140
- "--full-index",
73141
- "--src-prefix=a/",
73142
- "--dst-prefix=b/",
73143
- `${base}...${head}`,
73144
- "--"
73145
- ]);
73146
- if (!diff.ok || !diff.stdout.trim())
73147
- return null;
73148
- const id = git(cwd, ["patch-id", "--stable"], { input: diff.stdout });
73149
- return line(id)?.split(/\s+/u)[0] ?? null;
73150
- }
73151
- function currentBranch2(cwd) {
73152
- return line(git(cwd, ["symbolic-ref", "--short", "-q", "HEAD"]));
73153
- }
73154
- var MAX_BUFFER, GIT_TIMEOUTS, git = (cwd, args, options = {}) => {
73155
- const result = spawnSync13("git", args, {
73156
- cwd,
73157
- encoding: "utf8",
73158
- env: options.env ?? process.env,
73159
- input: options.input,
73160
- maxBuffer: MAX_BUFFER,
73161
- timeout: options.timeoutMs ?? GIT_TIMEOUTS.local,
73162
- killSignal: "SIGKILL",
73163
- windowsHide: true
73164
- });
73165
- const error = result.error;
73166
- return {
73167
- ok: result.status === 0,
73168
- stdout: result.stdout ?? "",
73169
- stderr: result.stderr ?? "",
73170
- timedOut: error?.code === "ETIMEDOUT"
73171
- };
73172
- }, line = (run) => {
73173
- if (!run.ok)
73174
- return null;
73175
- const value = run.stdout.trim();
73176
- return value ? value : null;
73177
- }, safeArg = (value) => value.length > 0 && !value.startsWith("-"), emptyTree = (cwd) => line(git(cwd, ["mktree"], { input: "" })), MAX_UNTRACKED_BYTES, seedIndex = (from, to) => {
73178
- fs15.copyFileSync(from, to);
73179
- const { atimeNs, mtimeNs } = fs15.statSync(from, { bigint: true });
73180
- const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
73181
- fs15.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
73182
- };
73183
- var init_rev = __esm(() => {
73184
- MAX_BUFFER = 256 * 1024 * 1024;
73185
- GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
73186
- MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
73187
- });
73188
-
73189
75081
  // packages/workit-core/src/ledger.ts
73190
- import { spawnSync as spawnSync14 } from "node:child_process";
75082
+ import { spawnSync as spawnSync15 } from "node:child_process";
73191
75083
  import { createHash as createHash13, randomBytes as randomBytes2 } from "node:crypto";
73192
- import fs16 from "node:fs";
73193
- import path35 from "node:path";
75084
+ import fs19 from "node:fs";
75085
+ import path37 from "node:path";
73194
75086
  function resolveCommit(cwd, ref) {
73195
75087
  if (!safeRef(ref))
73196
75088
  return null;
@@ -73248,16 +75140,16 @@ function storeRoot(cwd) {
73248
75140
  if (run.ok && common)
73249
75141
  return {
73250
75142
  ok: true,
73251
- value: { root: path35.join(path35.resolve(cwd, common), "workit"), shared: true }
75143
+ value: { root: path37.join(path37.resolve(cwd, common), "workit"), shared: true }
73252
75144
  };
73253
75145
  if (!run.ok && NOT_A_REPO.test(run.stderr))
73254
- return { ok: true, value: { root: path35.join(path35.resolve(cwd), ".workit"), shared: false } };
75146
+ return { ok: true, value: { root: path37.join(path37.resolve(cwd), ".workit"), shared: false } };
73255
75147
  return err("unavailable", `cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
73256
75148
  `)[0] || "no output"})`, "fix the repository (git status should work here) or run from a non-git directory");
73257
75149
  }
73258
75150
  function ledgerPath(cwd) {
73259
75151
  const root = storeRoot(cwd);
73260
- return root.ok ? { ok: true, value: path35.join(root.value.root, "ledger", "ledger.jsonl") } : root;
75152
+ return root.ok ? { ok: true, value: path37.join(root.value.root, "ledger", "ledger.jsonl") } : root;
73261
75153
  }
73262
75154
  function actorFromEnv(env) {
73263
75155
  const value = (key) => {
@@ -73270,7 +75162,7 @@ function actorFromEnv(env) {
73270
75162
  agentId: value("WORKIT_AGENT_ID")
73271
75163
  };
73272
75164
  }
73273
- function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
75165
+ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS2) {
73274
75166
  const lock = `${file}.lock`;
73275
75167
  const deadline = Date.now() + waitMs;
73276
75168
  let token = null;
@@ -73280,7 +75172,7 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
73280
75172
  break;
73281
75173
  const observed = lockToken(lock);
73282
75174
  const age = lockAgeMs(lock);
73283
- if (age !== null && age > LOCK_STALE_MS && ledgerLock.reap(lock, observed))
75175
+ if (age !== null && age > LOCK_STALE_MS2 && ledgerLock.reap(lock, observed))
73284
75176
  continue;
73285
75177
  if (Date.now() >= deadline)
73286
75178
  return null;
@@ -73294,29 +75186,29 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
73294
75186
  }
73295
75187
  function appendLine(file, line, options) {
73296
75188
  const write = () => {
73297
- const fd = fs16.openSync(file, "a+");
75189
+ const fd = fs19.openSync(file, "a+");
73298
75190
  try {
73299
- const size = fs16.fstatSync(fd).size;
75191
+ const size = fs19.fstatSync(fd).size;
73300
75192
  let fence = "";
73301
75193
  if (size > 0) {
73302
75194
  const last = Buffer.alloc(1);
73303
- fs16.readSync(fd, last, 0, 1, size - 1);
75195
+ fs19.readSync(fd, last, 0, 1, size - 1);
73304
75196
  if (last[0] !== 10)
73305
75197
  fence = `
73306
75198
  `;
73307
75199
  }
73308
75200
  const bytes = Buffer.from(fence + line);
73309
- const written = fs16.writeSync(fd, bytes);
75201
+ const written = fs19.writeSync(fd, bytes);
73310
75202
  if (written !== bytes.length)
73311
75203
  return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
73312
75204
  return { ok: true, value: undefined };
73313
75205
  } finally {
73314
- fs16.closeSync(fd);
75206
+ fs19.closeSync(fd);
73315
75207
  }
73316
75208
  };
73317
75209
  try {
73318
- fs16.mkdirSync(path35.dirname(file), { recursive: true });
73319
- if (!options.forceLock && !onNetworkFs(path35.dirname(file)))
75210
+ fs19.mkdirSync(path37.dirname(file), { recursive: true });
75211
+ if (!options.forceLock && !onNetworkFs(path37.dirname(file)))
73320
75212
  return write();
73321
75213
  const locked = withLedgerLock(file, write);
73322
75214
  return locked ?? err("busy", `ledger lock ${file}.lock is held`, "retry; remove the .lock directory if no workit process is running");
@@ -73355,7 +75247,7 @@ function readLedger(cwd) {
73355
75247
  return file;
73356
75248
  let text = "";
73357
75249
  try {
73358
- text = fs16.readFileSync(file.value, "utf8");
75250
+ text = fs19.readFileSync(file.value, "utf8");
73359
75251
  } catch (error) {
73360
75252
  if (error.code === "ENOENT")
73361
75253
  return { ok: true, value: { path: file.value, rows: [], skipped: 0 } };
@@ -73504,7 +75396,7 @@ function effectiveOf(rows, authors) {
73504
75396
  const effIndependent = independenceReasons(effective, authors).length === 0;
73505
75397
  if (effIndependent && !rowIndependent)
73506
75398
  continue;
73507
- if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session)
75399
+ if (effIndependent && FAILING2.has(String(effective.result)) && row.actor.session === effective.actor.session)
73508
75400
  continue;
73509
75401
  effective = row;
73510
75402
  }
@@ -73556,7 +75448,7 @@ function checkVerdicts(cwd, branch, rows) {
73556
75448
  }
73557
75449
  verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
73558
75450
  const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
73559
- const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
75451
+ const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING2.has(String(entry.verdict.result)));
73560
75452
  const newestAccepted = verdicts.findLast((entry) => entry.accepted) ?? null;
73561
75453
  const accepted = newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
73562
75454
  accepted: false,
@@ -73713,17 +75605,17 @@ function summarizeRow(row) {
73713
75605
  };
73714
75606
  }
73715
75607
  function stackPosition(root, branch) {
73716
- const dir = path35.join(root, "stacks");
75608
+ const dir = path37.join(root, "stacks");
73717
75609
  let names = [];
73718
75610
  try {
73719
- names = fs16.readdirSync(dir).filter((name) => name.endsWith(".json"));
75611
+ names = fs19.readdirSync(dir).filter((name) => name.endsWith(".json"));
73720
75612
  } catch {
73721
75613
  return null;
73722
75614
  }
73723
75615
  for (const name of names.toSorted()) {
73724
75616
  let parsed;
73725
75617
  try {
73726
- parsed = JSON.parse(fs16.readFileSync(path35.join(dir, name), "utf8"));
75618
+ parsed = JSON.parse(fs19.readFileSync(path37.join(dir, name), "utf8"));
73727
75619
  } catch {
73728
75620
  continue;
73729
75621
  }
@@ -73818,13 +75710,13 @@ function buildHandoff(cwd, options = {}) {
73818
75710
  value: { ...partial, nextCommand, next: options.next?.trim() || nextCommand }
73819
75711
  };
73820
75712
  }
73821
- var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING, FAILING, VERDICT_KINDS, VERDICT_SURFACES, PR_ROW_TYPES, err = (code, error, unblock) => ({
75713
+ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING, FAILING2, VERDICT_KINDS, VERDICT_SURFACES, PR_ROW_TYPES, err = (code, error, unblock) => ({
73822
75714
  ok: false,
73823
75715
  code,
73824
75716
  error,
73825
75717
  ...unblock ? { unblock } : {}
73826
75718
  }), gitRun = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
73827
- const run = spawnSync14("git", args, {
75719
+ const run = spawnSync15("git", args, {
73828
75720
  cwd,
73829
75721
  encoding: "utf8",
73830
75722
  timeout: timeoutMs,
@@ -73852,20 +75744,20 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, VERDICT_RESULTS, PASSING, FAILING
73852
75744
  if (process.platform !== "linux")
73853
75745
  return false;
73854
75746
  try {
73855
- return isNetworkFsType(fs16.statfsSync(dir).type);
75747
+ return isNetworkFsType(fs19.statfsSync(dir).type);
73856
75748
  } catch {
73857
75749
  return false;
73858
75750
  }
73859
- }, LOCK_WAIT_MS = 5000, LOCK_STALE_MS = 30000, LOCK_OWNER = "owner", lockToken = (lock) => {
75751
+ }, LOCK_WAIT_MS2 = 5000, LOCK_STALE_MS2 = 30000, LOCK_OWNER = "owner", lockToken = (lock) => {
73860
75752
  try {
73861
- return fs16.readFileSync(path35.join(lock, LOCK_OWNER), "utf8");
75753
+ return fs19.readFileSync(path37.join(lock, LOCK_OWNER), "utf8");
73862
75754
  } catch {
73863
75755
  return null;
73864
75756
  }
73865
75757
  }, lockAgeMs = (lock) => {
73866
- for (const target of [path35.join(lock, LOCK_OWNER), lock]) {
75758
+ for (const target of [path37.join(lock, LOCK_OWNER), lock]) {
73867
75759
  try {
73868
- return Date.now() - fs16.statSync(target).mtimeMs;
75760
+ return Date.now() - fs19.statSync(target).mtimeMs;
73869
75761
  } catch {}
73870
75762
  }
73871
75763
  return null;
@@ -73951,7 +75843,7 @@ var init_ledger = __esm(() => {
73951
75843
  "failed"
73952
75844
  ];
73953
75845
  PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
73954
- FAILING = new Set(["failed", "blocked"]);
75846
+ FAILING2 = new Set(["failed", "blocked"]);
73955
75847
  VERDICT_KINDS = ["unit", "live", "perf", "review"];
73956
75848
  VERDICT_SURFACES = ["ui", "cli", "api"];
73957
75849
  PR_ROW_TYPES = new Set(["pr.created", "pr.status"]);
@@ -73971,14 +75863,14 @@ var init_ledger = __esm(() => {
73971
75863
  ledgerLock = {
73972
75864
  acquire(lock) {
73973
75865
  try {
73974
- fs16.mkdirSync(lock);
75866
+ fs19.mkdirSync(lock);
73975
75867
  } catch (error) {
73976
75868
  if (error.code === "EEXIST")
73977
75869
  return null;
73978
75870
  throw error;
73979
75871
  }
73980
75872
  const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
73981
- fs16.writeFileSync(path35.join(lock, LOCK_OWNER), token);
75873
+ fs19.writeFileSync(path37.join(lock, LOCK_OWNER), token);
73982
75874
  return token;
73983
75875
  },
73984
75876
  release(lock, token) {
@@ -73986,35 +75878,35 @@ var init_ledger = __esm(() => {
73986
75878
  return false;
73987
75879
  const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
73988
75880
  try {
73989
- fs16.renameSync(lock, tomb);
75881
+ fs19.renameSync(lock, tomb);
73990
75882
  } catch {
73991
75883
  return false;
73992
75884
  }
73993
75885
  if (lockToken(tomb) !== token) {
73994
75886
  try {
73995
- fs16.renameSync(tomb, lock);
75887
+ fs19.renameSync(tomb, lock);
73996
75888
  } catch {}
73997
75889
  return false;
73998
75890
  }
73999
- fs16.rmSync(tomb, { recursive: true, force: true });
75891
+ fs19.rmSync(tomb, { recursive: true, force: true });
74000
75892
  return true;
74001
75893
  },
74002
75894
  token: lockToken,
74003
75895
  reap(lock, observed) {
74004
75896
  const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
74005
75897
  try {
74006
- fs16.renameSync(lock, tomb);
75898
+ fs19.renameSync(lock, tomb);
74007
75899
  } catch {
74008
75900
  return false;
74009
75901
  }
74010
75902
  if (lockToken(tomb) === observed) {
74011
- fs16.rmSync(tomb, { recursive: true, force: true });
75903
+ fs19.rmSync(tomb, { recursive: true, force: true });
74012
75904
  return true;
74013
75905
  }
74014
75906
  try {
74015
- fs16.renameSync(tomb, lock);
75907
+ fs19.renameSync(tomb, lock);
74016
75908
  } catch {
74017
- fs16.rmSync(tomb, { recursive: true, force: true });
75909
+ fs19.rmSync(tomb, { recursive: true, force: true });
74018
75910
  }
74019
75911
  return false;
74020
75912
  }
@@ -74053,34 +75945,34 @@ async function run18(argv, io) {
74053
75945
  try {
74054
75946
  parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
74055
75947
  } catch (error) {
74056
- return usage3(io, error.message);
75948
+ return usage4(io, error.message);
74057
75949
  }
74058
75950
  const { values } = parsed;
74059
75951
  let positionals = parsed.positionals;
74060
75952
  if (positionals[0] === "add") {
74061
75953
  positionals = positionals.slice(1);
74062
75954
  if (!["decision", "ruling", "verdict"].includes(positionals[0] ?? ""))
74063
- return usage3(io, "ledger add takes decision|ruling|verdict");
75955
+ return usage4(io, "ledger add takes decision|ruling|verdict");
74064
75956
  }
74065
75957
  const [sub, ...rest] = positionals;
74066
- const pr = positiveInt(values.pr, "--pr");
75958
+ const pr = positiveInt2(values.pr, "--pr");
74067
75959
  if (pr instanceof Error)
74068
- return usage3(io, pr.message);
74069
- const last = positiveInt(values.last, "--last");
75960
+ return usage4(io, pr.message);
75961
+ const last = positiveInt2(values.last, "--last");
74070
75962
  if (last instanceof Error)
74071
- return usage3(io, last.message);
75963
+ return usage4(io, last.message);
74072
75964
  const text = rest.join(" ");
74073
75965
  if (sub === undefined)
74074
- return usage3(io, "missing subcommand");
75966
+ return usage4(io, "missing subcommand");
74075
75967
  if (!["decision", "ruling", "verdict", "list", "show", "check"].includes(sub))
74076
- return usage3(io, `unknown ledger subcommand "${sub}"`);
75968
+ return usage4(io, `unknown ledger subcommand "${sub}"`);
74077
75969
  const ledger = readLedger(io.cwd);
74078
75970
  if (!ledger.ok)
74079
75971
  return failed(io, ledger);
74080
75972
  const { rows } = ledger.value;
74081
75973
  if (sub === "list" || sub === "show") {
74082
75974
  if (rest.length)
74083
- return usage3(io, `unexpected argument: ${rest[0]}`);
75975
+ return usage4(io, `unexpected argument: ${rest[0]}`);
74084
75976
  const listed = filterRows(rows, {
74085
75977
  branch: values.branch,
74086
75978
  pr,
@@ -74095,7 +75987,7 @@ async function run18(argv, io) {
74095
75987
  const isRead = sub === "check" || sub === "verdict" && values.how === undefined && !VERDICT_RESULTS.includes(rest[0] ?? "");
74096
75988
  if (isRead) {
74097
75989
  if (rest.length > (sub === "verdict" ? 1 : 0))
74098
- return usage3(io, `unexpected argument: ${rest.at(-1)}`);
75990
+ return usage4(io, `unexpected argument: ${rest.at(-1)}`);
74099
75991
  const target = targetBranch(io, rows, rest[0] ?? values.branch, pr);
74100
75992
  if (!target.ok)
74101
75993
  return failed(io, target);
@@ -74127,7 +76019,7 @@ async function run18(argv, io) {
74127
76019
  refs: values.ref
74128
76020
  }), (row) => `recorded ruling ${row.id}`);
74129
76021
  if (rest.length !== 1)
74130
- return usage3(io, `ledger verdict takes one result: ${VERDICT_RESULTS.join("|")}`);
76022
+ return usage4(io, `ledger verdict takes one result: ${VERDICT_RESULTS.join("|")}`);
74131
76023
  return fromResult(io, recordVerdict(context, {
74132
76024
  result: rest[0],
74133
76025
  kind: values.kind,
@@ -74137,12 +76029,12 @@ async function run18(argv, io) {
74137
76029
  evidenceRefs: values.evidence
74138
76030
  }), (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
74139
76031
  }
74140
- var USAGE = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, positiveInt = (value, flag) => {
76032
+ var USAGE = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, positiveInt2 = (value, flag) => {
74141
76033
  if (value === undefined)
74142
76034
  return;
74143
76035
  const parsed = Number(value);
74144
76036
  return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
74145
- }, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result), usage3 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE })), verdictLines = (check) => {
76037
+ }, failed = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE })), verdictLines = (check) => {
74146
76038
  const lines = [
74147
76039
  `${check.branch} @ ${(check.head ?? "no commit").slice(0, 12)}: current: ${check.current.basis}${typeof check.current.verdict?.result === "string" ? ` (${check.current.verdict.result})` : ""}; ${check.accepted.accepted ? "accepted" : `not accepted (${check.accepted.reasons.join(", ")})`}`
74148
76040
  ];
@@ -74181,7 +76073,7 @@ __export(exports_handoff, {
74181
76073
  import { parseArgs as parseArgs2 } from "node:util";
74182
76074
  function human(brief) {
74183
76075
  const lines = [
74184
- `branch: ${brief.branch ?? "(detached)"} @ ${short(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
76076
+ `branch: ${brief.branch ?? "(detached)"} @ ${short2(brief.head)}${brief.subject ? ` ${brief.subject}` : ""}`
74185
76077
  ];
74186
76078
  if (brief.base)
74187
76079
  lines.push(`base: ${brief.base}${brief.ahead === null ? "" : ` (ahead ${brief.ahead}, behind ${brief.behind})`}`);
@@ -74252,18 +76144,18 @@ async function run19(argv, io) {
74252
76144
  await Promise.resolve().then(() => init_task());
74253
76145
  return runTaskCommand(["handoff", ...argv]);
74254
76146
  }
74255
- var USAGE2 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short = (sha) => sha ? sha.slice(0, 12) : "no commit";
76147
+ var USAGE2 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short2 = (sha) => sha ? sha.slice(0, 12) : "no commit";
74256
76148
  var init_handoff = __esm(() => {
74257
76149
  init_ledger();
74258
76150
  init_output();
74259
76151
  });
74260
76152
 
74261
76153
  // packages/workit-cli/src/main.ts
74262
- import path36 from "node:path";
76154
+ import path38 from "node:path";
74263
76155
  // packages/workit-cli/package.json
74264
76156
  var package_default = {
74265
76157
  name: "@brainervirus/workit-cli",
74266
- version: "2.2.2",
76158
+ version: "2.3.0",
74267
76159
  private: false,
74268
76160
  description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
74269
76161
  keywords: [
@@ -74402,17 +76294,15 @@ var VERBS = [
74402
76294
  {
74403
76295
  name: "pr",
74404
76296
  group: "delivery",
74405
- usage: "workit pr status|create|merge [--pr <n>] [--json]",
74406
- summary: "Read PR/MR state (checks, threads, behind-base, next) or create/merge it",
74407
- planned: "S10/S11",
76297
+ usage: "workit pr status [--pr <n> | --branch <b>] [--log-lines 60] [--json]",
76298
+ summary: "Read PR/MR state: checks with failing log tails, unresolved threads, behind-base, next action",
74408
76299
  load: () => Promise.resolve().then(() => (init_pr(), exports_pr))
74409
76300
  },
74410
76301
  {
74411
76302
  name: "ci",
74412
76303
  group: "delivery",
74413
- usage: "workit ci wait|rerun [--pr <n>] [--json]",
74414
- summary: "Wait for CI on the current head or rerun failed jobs once",
74415
- planned: "S10",
76304
+ usage: "workit ci wait [--pr <n>] [--head <sha>] [--timeout 20m] | ci rerun [--failed | --check <name>…] --reason flake|infra [--force]",
76305
+ summary: "Wait for CI on the PR head (exit 0/1/3/4), or rerun failed jobs once per head",
74416
76306
  load: () => Promise.resolve().then(() => (init_ci(), exports_ci))
74417
76307
  },
74418
76308
  {
@@ -74536,7 +76426,7 @@ var defaultIo = (json) => ({
74536
76426
  });
74537
76427
  async function main(argv, overrides = {}, options = {}) {
74538
76428
  const parsed = parseGlobals(argv);
74539
- const cwd = path36.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
76429
+ const cwd = path38.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
74540
76430
  const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
74541
76431
  if (parsed.error)
74542
76432
  return emit(io, fail("invalid_input", parsed.error));