@great-arrow/cli 0.1.7 → 0.1.10

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 (3) hide show
  1. package/README.md +23 -0
  2. package/dist/index.js +1336 -65
  3. package/package.json +5 -3
package/dist/index.js CHANGED
@@ -5253,7 +5253,7 @@ var require_minimist = __commonJS({
5253
5253
  var require_rc = __commonJS({
5254
5254
  "node_modules/.pnpm/rc@1.2.8/node_modules/rc/index.js"(exports, module) {
5255
5255
  var cc = require_utils();
5256
- var join3 = __require("path").join;
5256
+ var join5 = __require("path").join;
5257
5257
  var deepExtend = require_deep_extend();
5258
5258
  var etc = "/etc";
5259
5259
  var win = process.platform === "win32";
@@ -5278,15 +5278,15 @@ var require_rc = __commonJS({
5278
5278
  }
5279
5279
  if (!win)
5280
5280
  [
5281
- join3(etc, name, "config"),
5282
- join3(etc, name + "rc")
5281
+ join5(etc, name, "config"),
5282
+ join5(etc, name + "rc")
5283
5283
  ].forEach(addConfigFile);
5284
5284
  if (home)
5285
5285
  [
5286
- join3(home, ".config", name, "config"),
5287
- join3(home, ".config", name),
5288
- join3(home, "." + name, "config"),
5289
- join3(home, "." + name + "rc")
5286
+ join5(home, ".config", name, "config"),
5287
+ join5(home, ".config", name),
5288
+ join5(home, "." + name, "config"),
5289
+ join5(home, "." + name + "rc")
5290
5290
  ].forEach(addConfigFile);
5291
5291
  addConfigFile(cc.find("." + name + "rc"));
5292
5292
  if (env4.config) addConfigFile(env4.config);
@@ -6624,11 +6624,11 @@ var require_util = __commonJS({
6624
6624
  if (files.includes("node_modules") || files.includes("package.json") || files.includes("package.json5") || files.includes("package.yaml") || files.includes("pnpm-workspace.yaml")) {
6625
6625
  return name2;
6626
6626
  }
6627
- const dirname3 = path9.dirname(name2);
6628
- if (dirname3 === name2) {
6627
+ const dirname4 = path9.dirname(name2);
6628
+ if (dirname4 === name2) {
6629
6629
  return original;
6630
6630
  }
6631
- return find(dirname3, original);
6631
+ return find(dirname4, original);
6632
6632
  } catch (error) {
6633
6633
  if (name2 === original) {
6634
6634
  if (error.code === "ENOENT") {
@@ -9278,6 +9278,13 @@ function networkError(message, hint) {
9278
9278
  hint ?? "Check your internet connection or run 'gad doctor'."
9279
9279
  );
9280
9280
  }
9281
+ function validationError(message, hint) {
9282
+ return new CliError(
9283
+ message,
9284
+ 3 /* VALIDATION_ERROR */,
9285
+ hint ?? "Check the command usage with --help."
9286
+ );
9287
+ }
9281
9288
  function mapStatusToExitCode(status) {
9282
9289
  if (status === 401 || status === 403) {
9283
9290
  return 1 /* AUTH_ERROR */;
@@ -9302,8 +9309,9 @@ function isNodeError(value) {
9302
9309
 
9303
9310
  // src/lib/version.ts
9304
9311
  var CLI_PACKAGE_NAME = "@great-arrow/cli";
9305
- var CLI_VERSION = "0.1.6";
9306
- var GAD_INSTALL_VERSION = "2026.06.22";
9312
+ var CLI_VERSION = "0.1.10";
9313
+ var GAD_INSTALL_VERSION = "2026.07.06";
9314
+ var GAD_INSTALL_MIN_VERSION = "2026.06.22";
9307
9315
  var GAD_INSTALL_HEADER = "X-GAD-Install-Version";
9308
9316
  var GAD_CLI_VERSION_HEADER = "X-GAD-CLI-Version";
9309
9317
 
@@ -9543,12 +9551,12 @@ h1{font-size:1.25rem;margin:0 0 .5rem}p{color:#9a9aa3;margin:0}</style></head>
9543
9551
  <body><div class="card"><h1>${ok ? "\u2713 Signed in to Great Arrow" : "\u2717 Sign-in failed"}</h1>
9544
9552
  <p>${escapeHtml(detail)}</p></div></body></html>`;
9545
9553
  function listen(port) {
9546
- return new Promise((resolve, reject) => {
9554
+ return new Promise((resolve3, reject) => {
9547
9555
  const server = createServer();
9548
9556
  server.on("error", reject);
9549
9557
  server.listen(port, LOOPBACK_HOST, () => {
9550
9558
  const addr = server.address();
9551
- resolve({ server, port: addr.port });
9559
+ resolve3({ server, port: addr.port });
9552
9560
  });
9553
9561
  });
9554
9562
  }
@@ -9573,7 +9581,7 @@ async function startLoopbackServer(opts) {
9573
9581
  port,
9574
9582
  close: () => server.close(),
9575
9583
  waitForCallback({ timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
9576
- return new Promise((resolve, reject) => {
9584
+ return new Promise((resolve3, reject) => {
9577
9585
  const timer = setTimeout(() => {
9578
9586
  server.close();
9579
9587
  reject(new Error("Timed out waiting for the browser callback."));
@@ -9613,7 +9621,7 @@ async function startLoopbackServer(opts) {
9613
9621
  "You can close this tab and return to your terminal."
9614
9622
  )
9615
9623
  );
9616
- settle(() => resolve({ code, state }));
9624
+ settle(() => resolve3({ code, state }));
9617
9625
  });
9618
9626
  });
9619
9627
  }
@@ -9867,11 +9875,11 @@ async function confirm(message) {
9867
9875
  input: process.stdin,
9868
9876
  output: process.stdout
9869
9877
  });
9870
- return new Promise((resolve) => {
9878
+ return new Promise((resolve3) => {
9871
9879
  rl.question(`${message} [y/N] `, (answer) => {
9872
9880
  rl.close();
9873
9881
  const normalized = answer.trim().toLowerCase();
9874
- resolve(normalized === "y" || normalized === "yes");
9882
+ resolve3(normalized === "y" || normalized === "yes");
9875
9883
  });
9876
9884
  });
9877
9885
  }
@@ -10038,10 +10046,10 @@ function maskToken(token) {
10038
10046
  }
10039
10047
  function openBrowser(url) {
10040
10048
  if (!/^https?:\/\//i.test(url)) return Promise.resolve(false);
10041
- return new Promise((resolve) => {
10049
+ return new Promise((resolve3) => {
10042
10050
  const platform = process.platform;
10043
10051
  const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
10044
- execFile(cmd, args, (error) => resolve(!error));
10052
+ execFile(cmd, args, (error) => resolve3(!error));
10045
10053
  });
10046
10054
  }
10047
10055
  var loginCommand = new Command("login").description("Authenticate the CLI with your Great Arrow Digital account").option(
@@ -10270,7 +10278,7 @@ function parseMcpPayload(result) {
10270
10278
 
10271
10279
  // src/lib/client.ts
10272
10280
  var DEFAULT_TIMEOUT_MS2 = 3e4;
10273
- var MAX_RETRIES = 3;
10281
+ var MAX_RETRIES = 5;
10274
10282
  var BASE_DELAY_MS = 1e3;
10275
10283
  function detectGitOrigin() {
10276
10284
  try {
@@ -10300,8 +10308,21 @@ function isRetryable(error) {
10300
10308
  function isRetryableStatus(status) {
10301
10309
  return status >= 500 && status < 600;
10302
10310
  }
10311
+ function describeError(error) {
10312
+ if (error instanceof Error) {
10313
+ const cause = error.cause;
10314
+ if (cause instanceof Error) {
10315
+ return `${error.name}: ${error.message}; cause=${cause.name}: ${cause.message}`;
10316
+ }
10317
+ if (cause) {
10318
+ return `${error.name}: ${error.message}; cause=${String(cause)}`;
10319
+ }
10320
+ return `${error.name}: ${error.message}`;
10321
+ }
10322
+ return String(error);
10323
+ }
10303
10324
  function sleep(ms) {
10304
- return new Promise((resolve) => setTimeout(resolve, ms));
10325
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
10305
10326
  }
10306
10327
  var GadClient = class {
10307
10328
  token;
@@ -10432,7 +10453,7 @@ var GadClient = class {
10432
10453
  } catch (error) {
10433
10454
  if (error instanceof CliError) throw error;
10434
10455
  throw networkError(
10435
- `Network error calling MCP tool '${toolName}'.`,
10456
+ `Network error calling MCP tool '${toolName}' (${describeError(error)}).`,
10436
10457
  "Check your internet connection or run 'gad doctor'."
10437
10458
  );
10438
10459
  }
@@ -10590,10 +10611,10 @@ function whoamiCommand() {
10590
10611
 
10591
10612
  // src/lib/stdin.ts
10592
10613
  function readStream(stream) {
10593
- return new Promise((resolve, reject) => {
10614
+ return new Promise((resolve3, reject) => {
10594
10615
  const chunks = [];
10595
10616
  stream.on("data", (chunk) => chunks.push(chunk));
10596
- stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
10617
+ stream.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
10597
10618
  stream.on("error", reject);
10598
10619
  });
10599
10620
  }
@@ -11215,14 +11236,928 @@ var claimCommand = new Command("claim").description("Preflight + atomically clai
11215
11236
  );
11216
11237
  var claim_default = claimCommand;
11217
11238
 
11239
+ // src/commands/work.ts
11240
+ import { readFileSync } from "node:fs";
11241
+
11242
+ // src/lib/work-status.ts
11243
+ import { execFileSync as execFileSync2 } from "node:child_process";
11244
+ function run(command, args, cwd) {
11245
+ try {
11246
+ const stdout = execFileSync2(command, args, {
11247
+ cwd,
11248
+ encoding: "utf8",
11249
+ stdio: ["ignore", "pipe", "pipe"],
11250
+ maxBuffer: 10 * 1024 * 1024
11251
+ });
11252
+ return { status: 0, stdout, stderr: "" };
11253
+ } catch (err) {
11254
+ const e = err;
11255
+ return {
11256
+ status: e.status ?? 1,
11257
+ stdout: e.stdout ?? "",
11258
+ stderr: e.stderr ?? ""
11259
+ };
11260
+ }
11261
+ }
11262
+ function git(args, cwd) {
11263
+ return run("git", args, cwd);
11264
+ }
11265
+ function requireGit(args, cwd) {
11266
+ const result = git(args, cwd);
11267
+ if (result.status !== 0) {
11268
+ throw new Error(
11269
+ `git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`
11270
+ );
11271
+ }
11272
+ return result.stdout.trim();
11273
+ }
11274
+ function isAncestor(ancestor, ref, cwd) {
11275
+ return git(["merge-base", "--is-ancestor", ancestor, ref], cwd).status === 0;
11276
+ }
11277
+ function revListCounts(head, cwd) {
11278
+ const result = git(
11279
+ ["rev-list", "--left-right", "--count", `origin/main...${head}`],
11280
+ cwd
11281
+ );
11282
+ if (result.status !== 0) return { behindMain: 0, aheadMain: 0 };
11283
+ const [behindRaw, aheadRaw] = result.stdout.trim().split(/\s+/);
11284
+ const behind = Number(behindRaw ?? 0);
11285
+ const ahead = Number(aheadRaw ?? 0);
11286
+ return {
11287
+ behindMain: Number.isFinite(behind) ? behind : 0,
11288
+ aheadMain: Number.isFinite(ahead) ? ahead : 0
11289
+ };
11290
+ }
11291
+ function parseWorktrees(raw) {
11292
+ const worktrees = [];
11293
+ let current = {};
11294
+ function flush() {
11295
+ if (current.path && current.head) {
11296
+ worktrees.push({
11297
+ path: current.path,
11298
+ branch: current.branch ?? "(detached)",
11299
+ head: current.head
11300
+ });
11301
+ }
11302
+ current = {};
11303
+ }
11304
+ for (const line of raw.split("\n")) {
11305
+ if (line === "") {
11306
+ flush();
11307
+ } else if (line.startsWith("worktree ")) {
11308
+ current.path = line.slice("worktree ".length);
11309
+ } else if (line.startsWith("HEAD ")) {
11310
+ current.head = line.slice("HEAD ".length);
11311
+ } else if (line.startsWith("branch refs/heads/")) {
11312
+ current.branch = line.slice("branch refs/heads/".length);
11313
+ } else if (line === "detached") {
11314
+ current.branch = "(detached)";
11315
+ }
11316
+ }
11317
+ flush();
11318
+ return worktrees;
11319
+ }
11320
+ function classifyWorktree(audit) {
11321
+ if (audit.path.includes("/.claude/worktrees/")) return "agent-harness";
11322
+ if (audit.dirty) return "review-dirty";
11323
+ if (audit.mergedToMain) return "cleanup-ready";
11324
+ if (audit.upstreamGone) return "upstream-gone";
11325
+ if (!audit.upstream) return "local-rescue";
11326
+ return "active-unmerged";
11327
+ }
11328
+ function auditWorktrees(root) {
11329
+ const raw = requireGit(["worktree", "list", "--porcelain"], root);
11330
+ return parseWorktrees(raw).map((worktree) => {
11331
+ const status = git(["status", "--porcelain"], worktree.path).stdout.trim();
11332
+ const branchStatus = git(["status", "--short", "--branch"], worktree.path).stdout.split("\n")[0]?.trim();
11333
+ const upstream = git(
11334
+ ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
11335
+ worktree.path
11336
+ );
11337
+ const counts = revListCounts(worktree.head, root);
11338
+ const baseAudit = {
11339
+ path: worktree.path,
11340
+ branch: worktree.branch,
11341
+ head: worktree.head,
11342
+ dirty: status.length > 0,
11343
+ mergedToMain: isAncestor(worktree.head, "origin/main", root),
11344
+ upstream: upstream.status === 0 ? upstream.stdout.trim() : null,
11345
+ upstreamGone: branchStatus?.includes("[gone]") ?? false,
11346
+ ...counts
11347
+ };
11348
+ return { ...baseAudit, classification: classifyWorktree(baseAudit) };
11349
+ });
11350
+ }
11351
+ function auditDetachedLocalBranches(root, checkedOutBranches) {
11352
+ const raw = requireGit(
11353
+ ["for-each-ref", "--format=%(refname:short)", "refs/heads"],
11354
+ root
11355
+ );
11356
+ return raw.split("\n").map((branch) => branch.trim()).filter(Boolean).filter((branch) => !checkedOutBranches.has(branch)).map((branch) => {
11357
+ const head = requireGit(["rev-parse", branch], root);
11358
+ const counts = revListCounts(head, root);
11359
+ return {
11360
+ branch,
11361
+ head,
11362
+ mergedToMain: isAncestor(head, "origin/main", root),
11363
+ ...counts
11364
+ };
11365
+ });
11366
+ }
11367
+ function readOpenPullRequests(root) {
11368
+ const result = run(
11369
+ "gh",
11370
+ [
11371
+ "pr",
11372
+ "list",
11373
+ "--state",
11374
+ "open",
11375
+ "--limit",
11376
+ "50",
11377
+ "--json",
11378
+ "number,title,headRefName,mergeStateStatus,isDraft,url"
11379
+ ],
11380
+ root
11381
+ );
11382
+ if (result.status !== 0) return [];
11383
+ try {
11384
+ return JSON.parse(result.stdout);
11385
+ } catch {
11386
+ return [];
11387
+ }
11388
+ }
11389
+ async function readProductionVersion(apiUrl) {
11390
+ const controller = new AbortController();
11391
+ const timeout2 = setTimeout(() => controller.abort(), 8e3);
11392
+ try {
11393
+ const response = await fetch(`${apiUrl.replace(/\/$/, "")}/api/version`, {
11394
+ signal: controller.signal
11395
+ });
11396
+ if (!response.ok) return null;
11397
+ return await response.json();
11398
+ } catch {
11399
+ return null;
11400
+ } finally {
11401
+ clearTimeout(timeout2);
11402
+ }
11403
+ }
11404
+ function countBy(values) {
11405
+ return values.reduce(
11406
+ (counts, value) => {
11407
+ counts[value] = (counts[value] ?? 0) + 1;
11408
+ return counts;
11409
+ },
11410
+ {}
11411
+ );
11412
+ }
11413
+ async function collectWorkStatus(opts) {
11414
+ const root = requireGit(["rev-parse", "--show-toplevel"]);
11415
+ if (opts.fetchRemote !== false) {
11416
+ git(["fetch", "origin", "main", "--prune"], root);
11417
+ }
11418
+ const originMain = requireGit(["rev-parse", "origin/main"], root);
11419
+ const originMainSubject = requireGit(
11420
+ ["log", "-1", "--pretty=%s", "origin/main"],
11421
+ root
11422
+ );
11423
+ const currentBranch = requireGit(["branch", "--show-current"], root);
11424
+ const currentHead = requireGit(["rev-parse", "HEAD"], root);
11425
+ const currentClean = git(["status", "--porcelain"], root).stdout.trim().length === 0;
11426
+ const stashes = git(["stash", "list"], root).stdout.split("\n").map((line) => line.trim()).filter(Boolean);
11427
+ const worktrees = auditWorktrees(root);
11428
+ const checkedOutBranches = new Set(
11429
+ worktrees.map((worktree) => worktree.branch).filter((branch) => branch !== "(detached)")
11430
+ );
11431
+ const detachedBranches = auditDetachedLocalBranches(root, checkedOutBranches);
11432
+ const openPullRequests = readOpenPullRequests(root);
11433
+ const production = opts.includeProduction === false ? null : await readProductionVersion(opts.apiUrl);
11434
+ const productionSha = production?.sha ?? null;
11435
+ const productionContainedByMain = productionSha ? isAncestor(productionSha, "origin/main", root) : null;
11436
+ const productionAtMain = productionSha === originMain;
11437
+ const detachedUnmerged = detachedBranches.filter(
11438
+ (branch) => !branch.mergedToMain
11439
+ );
11440
+ const blockedPrs = openPullRequests.filter(
11441
+ (pr) => pr.isDraft || !["CLEAN", "HAS_HOOKS"].includes(pr.mergeStateStatus)
11442
+ );
11443
+ return {
11444
+ root,
11445
+ current: {
11446
+ branch: currentBranch,
11447
+ head: currentHead,
11448
+ clean: currentClean,
11449
+ containedByMain: isAncestor(currentHead, "origin/main", root)
11450
+ },
11451
+ originMain: { sha: originMain, subject: originMainSubject },
11452
+ production: production ? {
11453
+ ...production,
11454
+ containedByMain: productionContainedByMain,
11455
+ atOriginMain: productionAtMain
11456
+ } : null,
11457
+ summary: {
11458
+ worktrees: worktrees.length,
11459
+ worktreeClassifications: countBy(
11460
+ worktrees.map((worktree) => worktree.classification)
11461
+ ),
11462
+ detachedLocalBranches: detachedBranches.length,
11463
+ detachedUnmergedBranches: detachedUnmerged.length,
11464
+ stashes: stashes.length,
11465
+ openPullRequests: openPullRequests.length,
11466
+ blockedPullRequests: blockedPrs.length
11467
+ },
11468
+ worktrees,
11469
+ detachedBranches,
11470
+ openPullRequests,
11471
+ strictFailures: []
11472
+ };
11473
+ }
11474
+
11475
+ // src/commands/work.ts
11476
+ function writeJson(value) {
11477
+ process.stdout.write(`${JSON.stringify(value)}
11478
+ `);
11479
+ }
11480
+ function shortSha(value) {
11481
+ return typeof value === "string" ? value.slice(0, 12) : "";
11482
+ }
11483
+ function collectOptions(opts) {
11484
+ const result = { apiUrl: opts.apiUrl };
11485
+ if (opts.fetch !== void 0) result.fetchRemote = opts.fetch;
11486
+ if (opts.production !== void 0) result.includeProduction = opts.production;
11487
+ return result;
11488
+ }
11489
+ function workCommand() {
11490
+ const cmd = new Command("work").description(
11491
+ "Inspect and publish unshipped work / work-at-risk state"
11492
+ );
11493
+ cmd.command("status").description("Show canonical work-at-risk state for the active workspace").option("--local", "Run a local git/worktree audit instead of app state").option("--no-fetch", "Skip git fetch when using --local").option(
11494
+ "--no-production",
11495
+ "Skip production version probe when using --local"
11496
+ ).option("--limit <n>", "Maximum app rows to show", "20").action(
11497
+ async (options, actionCmd) => {
11498
+ const output = globalOutputOpts(actionCmd);
11499
+ const fmt = createFormatter({
11500
+ ...output,
11501
+ isTTY: isTTY()
11502
+ });
11503
+ const auth = await resolveAuth();
11504
+ if (options.local) {
11505
+ const report = await collectWorkStatus({
11506
+ ...collectOptions({
11507
+ apiUrl: auth.apiUrl,
11508
+ fetch: options.fetch,
11509
+ production: options.production
11510
+ })
11511
+ });
11512
+ if (output.json) return writeJson(report);
11513
+ fmt.detail(
11514
+ {
11515
+ current: `${report.current.branch || "(detached)"} @ ${shortSha(
11516
+ report.current.head
11517
+ )}`,
11518
+ clean: report.current.clean,
11519
+ worktrees: report.summary.worktrees,
11520
+ dirty: report.summary.worktreeClassifications["review-dirty"] ?? 0,
11521
+ activeUnmerged: report.summary.worktreeClassifications["active-unmerged"] ?? 0,
11522
+ detachedUnmerged: report.summary.detachedUnmergedBranches,
11523
+ blockedPrs: report.summary.blockedPullRequests,
11524
+ stashes: report.summary.stashes
11525
+ },
11526
+ [
11527
+ { key: "current", header: "Current" },
11528
+ { key: "clean", header: "Clean" },
11529
+ { key: "worktrees", header: "Worktrees" },
11530
+ { key: "dirty", header: "Dirty" },
11531
+ { key: "activeUnmerged", header: "Unmerged" },
11532
+ { key: "detachedUnmerged", header: "Detached" },
11533
+ { key: "blockedPrs", header: "Blocked PRs" },
11534
+ { key: "stashes", header: "Stashes" }
11535
+ ]
11536
+ );
11537
+ return;
11538
+ }
11539
+ const client = new GadClient({
11540
+ token: auth.token,
11541
+ apiUrl: auth.apiUrl
11542
+ });
11543
+ const data = await client.get("/api/work/status", {
11544
+ limit: String(Number.parseInt(options.limit ?? "20", 10) || 20)
11545
+ });
11546
+ if (output.json) return writeJson(data);
11547
+ const items = Array.isArray(data.items) ? data.items : [];
11548
+ fmt.table(items, [
11549
+ { key: "riskLevel", header: "Risk" },
11550
+ { key: "title", header: "Item" },
11551
+ { key: "recommendedAction", header: "Action" }
11552
+ ]);
11553
+ }
11554
+ );
11555
+ cmd.command("publish").description("Publish the local release audit into the active workspace").option("--file <path>", "Publish a precomputed release-audit JSON file").option("--no-fetch", "Skip git fetch before collecting local status").option("--no-production", "Skip production version probe").option("--machine-id <id>", "Stable machine/session identifier").action(
11556
+ async (options, actionCmd) => {
11557
+ const output = globalOutputOpts(actionCmd);
11558
+ const fmt = createFormatter({
11559
+ ...output,
11560
+ isTTY: isTTY()
11561
+ });
11562
+ const auth = await resolveAuth();
11563
+ const client = new GadClient({
11564
+ token: auth.token,
11565
+ apiUrl: auth.apiUrl
11566
+ });
11567
+ const report = options.file ? JSON.parse(readFileSync(options.file, "utf8")) : await collectWorkStatus({
11568
+ ...collectOptions({
11569
+ apiUrl: auth.apiUrl,
11570
+ fetch: options.fetch,
11571
+ production: options.production
11572
+ })
11573
+ });
11574
+ const result = await client.post("/api/work/publish", {
11575
+ schemaVersion: 1,
11576
+ kind: "release-logjam",
11577
+ clientSource: "gad-cli",
11578
+ machineId: options.machineId,
11579
+ report
11580
+ });
11581
+ if (output.json) return writeJson(result);
11582
+ const r = result;
11583
+ fmt.success(
11584
+ `Published work-at-risk snapshot (${r.itemCount ?? 0} item${r.itemCount === 1 ? "" : "s"}, ${r.resolvedCount ?? 0} resolved).`
11585
+ );
11586
+ if (Array.isArray(r.topItems) && r.topItems.length > 0) {
11587
+ fmt.table(r.topItems, [
11588
+ { key: "riskLevel", header: "Risk" },
11589
+ { key: "title", header: "Item" },
11590
+ { key: "recommendedAction", header: "Action" }
11591
+ ]);
11592
+ }
11593
+ }
11594
+ );
11595
+ return cmd;
11596
+ }
11597
+ var work_default = workCommand;
11598
+
11599
+ // src/commands/worktree.ts
11600
+ import { execFileSync as execFileSync3 } from "node:child_process";
11601
+ import { existsSync, readFileSync as readFileSync2, statSync, writeFileSync } from "node:fs";
11602
+ import { basename, dirname as dirname2, isAbsolute, join as join2, resolve } from "node:path";
11603
+ var realExec = (cmd, args, opts) => {
11604
+ const out = execFileSync3(cmd, args, {
11605
+ cwd: opts?.cwd,
11606
+ encoding: "utf8",
11607
+ stdio: opts?.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
11608
+ });
11609
+ return typeof out === "string" ? out : "";
11610
+ };
11611
+ function assertCleanTree(statusPorcelain) {
11612
+ if (statusPorcelain.trim().length === 0) return;
11613
+ throw validationError(
11614
+ "Working tree is dirty \u2014 commit, stash, or clean up uncommitted changes first.",
11615
+ "A new task worktree must start from a clean state so another task's in-flight files can't leak into your first commit."
11616
+ );
11617
+ }
11618
+ function parseDefaultBranch(symbolicRef) {
11619
+ if (!symbolicRef) return null;
11620
+ const match = symbolicRef.trim().match(/^refs\/remotes\/origin\/(.+)$/);
11621
+ return match ? match[1] : null;
11622
+ }
11623
+ function detectPackageManager(repoRoot) {
11624
+ if (existsSync(join2(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
11625
+ if (existsSync(join2(repoRoot, "bun.lockb"))) return "bun";
11626
+ if (existsSync(join2(repoRoot, "yarn.lock"))) return "yarn";
11627
+ return "npm";
11628
+ }
11629
+ function defaultWorktreeDir(repoRoot, branch) {
11630
+ const slug = (branch.split("/").pop() ?? branch).replace(
11631
+ /[^A-Za-z0-9._-]+/g,
11632
+ "-"
11633
+ );
11634
+ return join2(dirname2(repoRoot), `${basename(repoRoot)}-${slug}`);
11635
+ }
11636
+ var AGENT_PROCESS_RE = /(^|\/)(claude|kiro|cursor|codex)/i;
11637
+ function psField(pid, field) {
11638
+ try {
11639
+ return execFileSync3("ps", ["-o", `${field}=`, "-p", String(pid)], {
11640
+ encoding: "utf8"
11641
+ }).trim();
11642
+ } catch {
11643
+ return "";
11644
+ }
11645
+ }
11646
+ function findAgentPid(startPid) {
11647
+ let pid = Number(startPid);
11648
+ for (let i = 0; i < 40 && pid > 1; i++) {
11649
+ if (AGENT_PROCESS_RE.test(basename(psField(pid, "comm")))) return pid;
11650
+ const ppid = Number(psField(pid, "ppid"));
11651
+ if (!ppid || ppid === pid) break;
11652
+ pid = ppid;
11653
+ }
11654
+ return null;
11655
+ }
11656
+ function ownGitDir(worktreeDir) {
11657
+ const dotGit = join2(worktreeDir, ".git");
11658
+ if (!existsSync(dotGit)) return null;
11659
+ if (statSync(dotGit).isDirectory()) return dotGit;
11660
+ const match = readFileSync2(dotGit, "utf8").trim().match(/^gitdir:\s*(.+)$/m);
11661
+ if (!match) return null;
11662
+ return isAbsolute(match[1]) ? match[1] : resolve(worktreeDir, match[1]);
11663
+ }
11664
+ function stampWorktreeOwner(worktreeDir) {
11665
+ try {
11666
+ if (process.platform === "win32") return null;
11667
+ const gitDir = ownGitDir(worktreeDir);
11668
+ if (!gitDir) return null;
11669
+ const ownerPid = findAgentPid(process.ppid);
11670
+ if (!ownerPid) return null;
11671
+ writeFileSync(
11672
+ join2(gitDir, "agent-owner.json"),
11673
+ JSON.stringify(
11674
+ {
11675
+ ownerPid,
11676
+ comm: basename(psField(ownerPid, "comm")),
11677
+ stampedAt: (/* @__PURE__ */ new Date()).toISOString()
11678
+ },
11679
+ null,
11680
+ 2
11681
+ )
11682
+ );
11683
+ return ownerPid;
11684
+ } catch {
11685
+ return null;
11686
+ }
11687
+ }
11688
+ function createTaskWorktree(opts) {
11689
+ const exec2 = opts.exec ?? realExec;
11690
+ const cwd = opts.cwd ?? process.cwd();
11691
+ const { branch } = opts;
11692
+ let repoRoot;
11693
+ try {
11694
+ repoRoot = exec2("git", ["rev-parse", "--show-toplevel"], { cwd }).trim();
11695
+ } catch {
11696
+ throw validationError(
11697
+ "Not inside a git repository.",
11698
+ "Run this from the repo you want a task worktree for."
11699
+ );
11700
+ }
11701
+ assertCleanTree(exec2("git", ["status", "--porcelain"], { cwd: repoRoot }));
11702
+ let fetched = true;
11703
+ try {
11704
+ exec2("git", ["fetch", "origin", "--quiet"], { cwd: repoRoot });
11705
+ } catch {
11706
+ fetched = false;
11707
+ }
11708
+ let base = null;
11709
+ try {
11710
+ base = parseDefaultBranch(
11711
+ exec2("git", ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], {
11712
+ cwd: repoRoot
11713
+ })
11714
+ );
11715
+ } catch {
11716
+ base = null;
11717
+ }
11718
+ if (!base) {
11719
+ for (const candidate of ["main", "master"]) {
11720
+ try {
11721
+ exec2(
11722
+ "git",
11723
+ [
11724
+ "show-ref",
11725
+ "--verify",
11726
+ "--quiet",
11727
+ `refs/remotes/origin/${candidate}`
11728
+ ],
11729
+ { cwd: repoRoot }
11730
+ );
11731
+ base = candidate;
11732
+ break;
11733
+ } catch {
11734
+ }
11735
+ }
11736
+ }
11737
+ if (!base) {
11738
+ throw validationError(
11739
+ "Could not determine the origin default branch.",
11740
+ "Ensure the repo has an 'origin' remote with a main or master branch (git fetch origin)."
11741
+ );
11742
+ }
11743
+ let branchExists = false;
11744
+ try {
11745
+ exec2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
11746
+ cwd: repoRoot
11747
+ });
11748
+ branchExists = true;
11749
+ } catch {
11750
+ branchExists = false;
11751
+ }
11752
+ if (branchExists) {
11753
+ throw validationError(
11754
+ `Branch '${branch}' already exists locally \u2014 pick a new name or remove it.`
11755
+ );
11756
+ }
11757
+ const dir = opts.dir ? resolve(cwd, opts.dir) : defaultWorktreeDir(repoRoot, branch);
11758
+ const baseRef = `origin/${base}`;
11759
+ exec2("git", ["worktree", "add", dir, "-b", branch, baseRef], {
11760
+ cwd: repoRoot
11761
+ });
11762
+ const stampedPid = opts.stamp === false ? null : stampWorktreeOwner(dir);
11763
+ const packageManager = detectPackageManager(repoRoot);
11764
+ let installed = false;
11765
+ if (opts.install !== false && existsSync(join2(repoRoot, "package.json"))) {
11766
+ exec2(packageManager, ["install"], { cwd: dir, inherit: true });
11767
+ installed = true;
11768
+ }
11769
+ return {
11770
+ dir,
11771
+ branch,
11772
+ baseRef,
11773
+ packageManager,
11774
+ installed,
11775
+ fetched,
11776
+ stampedPid
11777
+ };
11778
+ }
11779
+ var worktreeCommand = new Command("worktree").description(
11780
+ "Isolated task worktrees for parallel agents"
11781
+ );
11782
+ worktreeCommand.command("new").description(
11783
+ "Create a fresh worktree on a new branch off the origin default branch, with a real install"
11784
+ ).argument("<branch>", "New branch name (e.g. feat/fix-login)").argument("[dir]", "Target directory (default: sibling <repo>-<slug>)").option("--no-install", "Skip the dependency install step").action((branch, dir, _options, cmd) => {
11785
+ const parentOpts = cmd.optsWithGlobals();
11786
+ const fmt = createFormatter({
11787
+ json: parentOpts.json || false,
11788
+ quiet: parentOpts.quiet || false,
11789
+ isTTY: isTTY(),
11790
+ noColor: parentOpts.noColor || false
11791
+ });
11792
+ const localOpts = cmd.opts();
11793
+ const result = createTaskWorktree({
11794
+ branch,
11795
+ ...dir ? { dir } : {},
11796
+ install: localOpts.install
11797
+ });
11798
+ if (parentOpts.json) {
11799
+ process.stdout.write(JSON.stringify(result) + "\n");
11800
+ return;
11801
+ }
11802
+ if (!result.fetched) {
11803
+ fmt.warn(
11804
+ "Could not fetch origin \u2014 branched off the last locally known " + result.baseRef + "."
11805
+ );
11806
+ }
11807
+ fmt.success(
11808
+ `Worktree ready: ${result.dir} on ${result.branch} (based on ${result.baseRef}).`
11809
+ );
11810
+ if (result.stampedPid) {
11811
+ fmt.info(
11812
+ "Stamped this agent session as the worktree owner (pairs with 'gad install-hooks')."
11813
+ );
11814
+ }
11815
+ if (!result.installed) {
11816
+ fmt.info(
11817
+ `Install skipped \u2014 run '${result.packageManager} install' inside the worktree before building.`
11818
+ );
11819
+ }
11820
+ fmt.info(`Next: cd ${result.dir}`);
11821
+ fmt.info(`Done: git worktree remove ${result.dir}`);
11822
+ });
11823
+ var worktree_default = worktreeCommand;
11824
+
11825
+ // src/commands/install-hooks.ts
11826
+ import { execFileSync as execFileSync4 } from "node:child_process";
11827
+ import {
11828
+ chmodSync,
11829
+ existsSync as existsSync2,
11830
+ mkdirSync,
11831
+ readFileSync as readFileSync3,
11832
+ writeFileSync as writeFileSync2
11833
+ } from "node:fs";
11834
+ import { isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
11835
+ var GUARD_MARKER = "# gad-agent-guard v1";
11836
+ var SCHEMA_AUDIT_MARKER = "# gad-schema-audit v1";
11837
+ var GUARD_FILE = "gad-commit-guard.mjs";
11838
+ var realExec2 = (cmd, args, opts) => execFileSync4(cmd, args, {
11839
+ cwd: opts?.cwd,
11840
+ encoding: "utf8",
11841
+ stdio: ["ignore", "pipe", "pipe"]
11842
+ });
11843
+ function buildGuardScript() {
11844
+ return `#!/usr/bin/env node
11845
+ // gad-agent-guard v1 \u2014 pre-commit backstop against cross-worktree clobber.
11846
+ // Installed by \`gad install-hooks\`. Blocks a commit made inside a worktree
11847
+ // owned by a DIFFERENT still-live agent session. FAILS OPEN on every
11848
+ // ambiguity (no marker, dead owner, human committer, unsupported platform).
11849
+ // Deliberate override: ALLOW_FOREIGN_WORKTREE=1.
11850
+ import { existsSync, statSync, readFileSync } from "node:fs";
11851
+ import { join, basename, isAbsolute, resolve } from "node:path";
11852
+ import { execFileSync } from "node:child_process";
11853
+
11854
+ const OWNER_FILE = "agent-owner.json";
11855
+ const AGENT_RE = /(^|\\/)(claude|kiro|cursor|codex)/i;
11856
+
11857
+ function psField(pid, field) {
11858
+ try {
11859
+ return execFileSync("ps", ["-o", field + "=", "-p", String(pid)], {
11860
+ encoding: "utf8",
11861
+ }).trim();
11862
+ } catch {
11863
+ return "";
11864
+ }
11865
+ }
11866
+
11867
+ function ancestry(startPid) {
11868
+ const pids = new Set();
11869
+ let pid = Number(startPid);
11870
+ for (let i = 0; i < 40 && pid > 1; i++) {
11871
+ pids.add(pid);
11872
+ const ppid = Number(psField(pid, "ppid"));
11873
+ if (!ppid || ppid === pid) break;
11874
+ pid = ppid;
11875
+ }
11876
+ return pids;
11877
+ }
11878
+
11879
+ function isAlive(pid) {
11880
+ try {
11881
+ process.kill(pid, 0);
11882
+ return true;
11883
+ } catch {
11884
+ return false;
11885
+ }
11886
+ }
11887
+
11888
+ function ownGitDir(cwd) {
11889
+ const dotGit = join(cwd, ".git");
11890
+ if (!existsSync(dotGit)) return null;
11891
+ if (statSync(dotGit).isDirectory()) return dotGit;
11892
+ const m = readFileSync(dotGit, "utf8").trim().match(/^gitdir:\\s*(.+)$/m);
11893
+ if (!m) return null;
11894
+ return isAbsolute(m[1]) ? m[1] : resolve(cwd, m[1]);
11895
+ }
11896
+
11897
+ function evaluate(cwd, env) {
11898
+ if (env.ALLOW_FOREIGN_WORKTREE) return { block: false, reason: "override" };
11899
+ if (process.platform === "win32")
11900
+ return { block: false, reason: "unsupported-platform" };
11901
+ const isAgent =
11902
+ env.CLAUDECODE ||
11903
+ env.CLAUDE_CODE ||
11904
+ env.KIRO_AGENT ||
11905
+ env.CURSOR_AGENT ||
11906
+ env.CODEX_AGENT ||
11907
+ env.AGENT_SESSION;
11908
+ if (!isAgent) return { block: false, reason: "not-an-agent" };
11909
+ const gd = ownGitDir(cwd);
11910
+ if (!gd) return { block: false, reason: "no-gitdir" };
11911
+ const markerPath = join(gd, OWNER_FILE);
11912
+ if (!existsSync(markerPath)) return { block: false, reason: "no-marker" };
11913
+ let owner;
11914
+ try {
11915
+ owner = JSON.parse(readFileSync(markerPath, "utf8"));
11916
+ } catch {
11917
+ return { block: false, reason: "marker-unreadable" };
11918
+ }
11919
+ if (!owner || !owner.ownerPid)
11920
+ return { block: false, reason: "marker-no-owner" };
11921
+ const anc = ancestry(process.ppid);
11922
+ if (anc.has(Number(owner.ownerPid)))
11923
+ return { block: false, reason: "owner-match" };
11924
+ if (!isAlive(Number(owner.ownerPid)))
11925
+ return { block: false, reason: "owner-dead" };
11926
+ return { block: true, reason: "foreign-owner ownerPid=" + owner.ownerPid };
11927
+ }
11928
+
11929
+ const { block, reason } = evaluate(process.cwd(), process.env);
11930
+ if (block) {
11931
+ console.error("");
11932
+ console.error(
11933
+ "gad-agent-guard: commit blocked \u2014 this worktree belongs to a DIFFERENT live agent session.",
11934
+ );
11935
+ console.error(" " + reason);
11936
+ console.error(
11937
+ " You are about to commit another agent's in-flight work. Use your own worktree:",
11938
+ );
11939
+ console.error(" gad worktree new <branch>");
11940
+ console.error(" Deliberate override: ALLOW_FOREIGN_WORKTREE=1");
11941
+ console.error("");
11942
+ process.exit(1);
11943
+ }
11944
+ `;
11945
+ }
11946
+ function buildHookBlock() {
11947
+ return `${GUARD_MARKER} (managed by \`gad install-hooks\` \u2014 do not edit this block)
11948
+ if command -v node >/dev/null 2>&1; then
11949
+ gad_guard_dir="$(git rev-parse --git-common-dir 2>/dev/null)"
11950
+ if [ -n "$gad_guard_dir" ] && [ -f "$gad_guard_dir/hooks/${GUARD_FILE}" ]; then
11951
+ node "$gad_guard_dir/hooks/${GUARD_FILE}" || exit 1
11952
+ fi
11953
+ fi
11954
+ `;
11955
+ }
11956
+ function buildSchemaAuditHookBlock() {
11957
+ return `${SCHEMA_AUDIT_MARKER} (managed by \`gad install-hooks\` \u2014 do not edit this block)
11958
+ if [ -f package.json ] && grep -q '"schema:audit"' package.json; then
11959
+ if command -v pnpm >/dev/null 2>&1; then
11960
+ pnpm schema:audit --pre-push || exit 1
11961
+ else
11962
+ echo "gad-schema-audit: pnpm not found; skipping schema audit" >&2
11963
+ fi
11964
+ fi
11965
+ `;
11966
+ }
11967
+ function planHookInstall(existing) {
11968
+ if (existing !== null && existing.includes(GUARD_MARKER)) {
11969
+ return { action: "noop", content: existing };
11970
+ }
11971
+ if (existing === null) {
11972
+ return { action: "create", content: `#!/bin/sh
11973
+ ${buildHookBlock()}` };
11974
+ }
11975
+ return {
11976
+ action: "append",
11977
+ content: `${existing.trimEnd()}
11978
+
11979
+ ${buildHookBlock()}`
11980
+ };
11981
+ }
11982
+ function planSchemaAuditHookInstall(existing) {
11983
+ if (existing !== null && existing.includes(SCHEMA_AUDIT_MARKER)) {
11984
+ return { action: "noop", content: existing };
11985
+ }
11986
+ if (existing === null) {
11987
+ return {
11988
+ action: "create",
11989
+ content: `#!/bin/sh
11990
+ ${buildSchemaAuditHookBlock()}`
11991
+ };
11992
+ }
11993
+ return {
11994
+ action: "append",
11995
+ content: `${existing.trimEnd()}
11996
+
11997
+ ${buildSchemaAuditHookBlock()}`
11998
+ };
11999
+ }
12000
+ function installCommitGuard(opts) {
12001
+ const exec2 = opts?.exec ?? realExec2;
12002
+ const cwd = opts?.cwd ?? process.cwd();
12003
+ let repoRoot;
12004
+ let commonDir;
12005
+ try {
12006
+ repoRoot = exec2("git", ["rev-parse", "--show-toplevel"], { cwd }).trim();
12007
+ commonDir = exec2("git", ["rev-parse", "--git-common-dir"], {
12008
+ cwd: repoRoot
12009
+ }).trim();
12010
+ } catch {
12011
+ throw validationError(
12012
+ "Not inside a git repository.",
12013
+ "Run this from the repo whose commits you want guarded."
12014
+ );
12015
+ }
12016
+ if (!isAbsolute2(commonDir)) commonDir = resolve2(repoRoot, commonDir);
12017
+ const hooksDir = join3(commonDir, "hooks");
12018
+ mkdirSync(hooksDir, { recursive: true });
12019
+ const guardPath = join3(hooksDir, GUARD_FILE);
12020
+ writeFileSync2(guardPath, buildGuardScript());
12021
+ chmodSync(guardPath, 493);
12022
+ const huskyHook = join3(repoRoot, ".husky", "pre-commit");
12023
+ const hookPath = existsSync2(huskyHook) ? huskyHook : join3(hooksDir, "pre-commit");
12024
+ const existing = existsSync2(hookPath) ? readFileSync3(hookPath, "utf8") : null;
12025
+ const plan = planHookInstall(existing);
12026
+ if (plan.action !== "noop") {
12027
+ writeFileSync2(hookPath, plan.content);
12028
+ }
12029
+ chmodSync(hookPath, 493);
12030
+ const huskyDir = join3(repoRoot, ".husky");
12031
+ const huskyPrePush = join3(huskyDir, "pre-push");
12032
+ const schemaAuditHookPath = existsSync2(huskyPrePush) || existsSync2(huskyDir) ? huskyPrePush : join3(hooksDir, "pre-push");
12033
+ const existingSchemaAuditHook = existsSync2(schemaAuditHookPath) ? readFileSync3(schemaAuditHookPath, "utf8") : null;
12034
+ const schemaAuditPlan = planSchemaAuditHookInstall(existingSchemaAuditHook);
12035
+ if (schemaAuditPlan.action !== "noop") {
12036
+ writeFileSync2(schemaAuditHookPath, schemaAuditPlan.content);
12037
+ }
12038
+ chmodSync(schemaAuditHookPath, 493);
12039
+ return {
12040
+ guardPath,
12041
+ hookPath,
12042
+ action: plan.action,
12043
+ schemaAuditHookPath,
12044
+ schemaAuditAction: schemaAuditPlan.action
12045
+ };
12046
+ }
12047
+ var installHooksCommand = new Command("install-hooks").description(
12048
+ "Install the fail-open agent commit guard into this repo's pre-commit hook (idempotent)"
12049
+ ).action((_options, cmd) => {
12050
+ const parentOpts = cmd.optsWithGlobals();
12051
+ const fmt = createFormatter({
12052
+ json: parentOpts.json || false,
12053
+ quiet: parentOpts.quiet || false,
12054
+ isTTY: isTTY(),
12055
+ noColor: parentOpts.noColor || false
12056
+ });
12057
+ const result = installCommitGuard();
12058
+ if (parentOpts.json) {
12059
+ process.stdout.write(JSON.stringify(result) + "\n");
12060
+ return;
12061
+ }
12062
+ if (result.action === "noop") {
12063
+ fmt.success(`Commit guard already installed at ${result.hookPath}.`);
12064
+ } else {
12065
+ fmt.success(
12066
+ `Commit guard ${result.action === "append" ? "chained into" : "installed at"} ${result.hookPath}.`
12067
+ );
12068
+ }
12069
+ if (result.schemaAuditAction === "noop") {
12070
+ fmt.success(
12071
+ `Schema audit pre-push hook already installed at ${result.schemaAuditHookPath}.`
12072
+ );
12073
+ } else {
12074
+ fmt.success(
12075
+ `Schema audit pre-push hook ${result.schemaAuditAction === "append" ? "chained into" : "installed at"} ${result.schemaAuditHookPath}.`
12076
+ );
12077
+ }
12078
+ fmt.info(
12079
+ "The guard fails open for humans and dead sessions; override deliberately with ALLOW_FOREIGN_WORKTREE=1."
12080
+ );
12081
+ fmt.info(
12082
+ "Pair with 'gad worktree new <branch>' so each agent session owns its worktree."
12083
+ );
12084
+ });
12085
+ var install_hooks_default = installHooksCommand;
12086
+
12087
+ // src/commands/guardrails.ts
12088
+ function writeJson2(value) {
12089
+ process.stdout.write(`${JSON.stringify(value)}
12090
+ `);
12091
+ }
12092
+ function normalizeResponse(value) {
12093
+ return typeof value === "object" && value !== null ? value : {};
12094
+ }
12095
+ async function createClient2() {
12096
+ const auth = await resolveAuth();
12097
+ return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
12098
+ }
12099
+ function renderGuardrails(data, cmd) {
12100
+ const output = globalOutputOpts(cmd);
12101
+ const fmt = createFormatter({ ...output, isTTY: isTTY() });
12102
+ if (output.json) return writeJson2(data);
12103
+ const guardrails = Array.isArray(data.guardrails) ? data.guardrails : [];
12104
+ fmt.table(
12105
+ guardrails.map((guardrail) => ({
12106
+ ...guardrail,
12107
+ state: guardrail.enabled ? "on" : "off",
12108
+ surfacesLabel: guardrail.surfaces.join(", ")
12109
+ })),
12110
+ [
12111
+ { key: "state", header: "State" },
12112
+ { key: "id", header: "ID" },
12113
+ { key: "title", header: "Guardrail" },
12114
+ { key: "command", header: "Command" },
12115
+ { key: "surfacesLabel", header: "Surfaces" }
12116
+ ]
12117
+ );
12118
+ }
12119
+ function guardrailsCommand() {
12120
+ const cmd = new Command("guardrails").description(
12121
+ "List and manage optional engineering guardrails for the active workspace"
12122
+ );
12123
+ cmd.command("list").description("List guardrails, explanations, commands, and enabled state").action(async (_options, actionCmd) => {
12124
+ const client = await createClient2();
12125
+ const result = normalizeResponse(
12126
+ await client.mcpCall("engineering_guardrails_list", {})
12127
+ );
12128
+ renderGuardrails(result, actionCmd);
12129
+ });
12130
+ cmd.command("enable").description("Enable a guardrail for the active workspace").argument("<id>", "Guardrail id from `gad guardrails list`").action(async (id, _options, actionCmd) => {
12131
+ const client = await createClient2();
12132
+ const result = normalizeResponse(
12133
+ await client.mcpCall("engineering_guardrails_update", {
12134
+ id,
12135
+ enabled: true
12136
+ })
12137
+ );
12138
+ renderGuardrails(result, actionCmd);
12139
+ });
12140
+ cmd.command("disable").description("Disable a guardrail for the active workspace").argument("<id>", "Guardrail id from `gad guardrails list`").action(async (id, _options, actionCmd) => {
12141
+ const client = await createClient2();
12142
+ const result = normalizeResponse(
12143
+ await client.mcpCall("engineering_guardrails_update", {
12144
+ id,
12145
+ enabled: false
12146
+ })
12147
+ );
12148
+ renderGuardrails(result, actionCmd);
12149
+ });
12150
+ return cmd;
12151
+ }
12152
+
11218
12153
  // src/commands/doctor.ts
11219
- import { existsSync } from "node:fs";
12154
+ import { existsSync as existsSync3 } from "node:fs";
11220
12155
  import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
11221
12156
  import { homedir as homedir2 } from "node:os";
11222
- import { dirname as dirname2, join as join2 } from "node:path";
12157
+ import { dirname as dirname3, join as join4 } from "node:path";
11223
12158
 
11224
12159
  // src/lib/codex-config.ts
11225
- import { execFileSync as execFileSync2 } from "node:child_process";
12160
+ import { execFileSync as execFileSync5 } from "node:child_process";
11226
12161
  var CODEX_SERVER_NAME = "great-arrow";
11227
12162
  var CODEX_CLIENT_ID = "codex-cli";
11228
12163
  var SAFE_GLOBAL_MCP_SERVERS = /* @__PURE__ */ new Set([
@@ -11245,13 +12180,31 @@ function mcpEndpoint(apiUrl) {
11245
12180
  function tomlEscape(value) {
11246
12181
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
11247
12182
  }
12183
+ function compareVersion(a, b) {
12184
+ if (!a && !b) return 0;
12185
+ if (!a) return -1;
12186
+ if (!b) return 1;
12187
+ const left = a.match(/\d+|[a-z]+/gi) ?? [];
12188
+ const right = b.match(/\d+|[a-z]+/gi) ?? [];
12189
+ const length = Math.max(left.length, right.length);
12190
+ for (let i = 0; i < length; i += 1) {
12191
+ const l = left[i] ?? "0";
12192
+ const r = right[i] ?? "0";
12193
+ const ln = Number(l);
12194
+ const rn = Number(r);
12195
+ const bothNumeric = Number.isFinite(ln) && Number.isFinite(rn);
12196
+ if (bothNumeric && ln !== rn) return ln > rn ? 1 : -1;
12197
+ if (!bothNumeric && l !== r) return l > r ? 1 : -1;
12198
+ }
12199
+ return 0;
12200
+ }
11248
12201
  function detectGitOrigin2(cwd = process.cwd()) {
11249
12202
  try {
11250
12203
  const env4 = { ...process.env };
11251
12204
  for (const key of Object.keys(env4)) {
11252
12205
  if (key.startsWith("GIT_")) delete env4[key];
11253
12206
  }
11254
- const url = execFileSync2(
12207
+ const url = execFileSync5(
11255
12208
  "git",
11256
12209
  ["-C", cwd, "remote", "get-url", "origin"],
11257
12210
  {
@@ -11381,9 +12334,29 @@ function extractConfiguredRepoKeys(text) {
11381
12334
  }
11382
12335
  return [...keys];
11383
12336
  }
11384
- function hasJobbyJobMarker(server, text) {
11385
- return /jobby\s*job|jobbyjob|jobbyjob\.ai/i.test(`${server}
11386
- ${text}`);
12337
+ function extractInstallVersion(text) {
12338
+ const escapedHeader = GAD_INSTALL_HEADER.replace(
12339
+ /[.*+?^${}()|[\]\\]/g,
12340
+ "\\$&"
12341
+ );
12342
+ const match = text.match(
12343
+ new RegExp(`^\\s*${escapedHeader}\\s*=\\s*"([^"]+)"\\s*$`, "m")
12344
+ );
12345
+ return match?.[1]?.trim() || null;
12346
+ }
12347
+ function localPathArgs(text) {
12348
+ return [...text.matchAll(/"((?:\/|~\/)[^"]+)"/g)].map((m) => m[1] ?? "");
12349
+ }
12350
+ function hasForeignRepoMarker(text, expectedRepoKey) {
12351
+ if (!expectedRepoKey) return false;
12352
+ const repoName = expectedRepoKey.split("/").pop()?.toLowerCase() ?? "";
12353
+ if (!repoName) return false;
12354
+ for (const p of localPathArgs(text)) {
12355
+ const lower = p.toLowerCase();
12356
+ const looksLikeCheckout = /server-filesystem/.test(text) || /\.(c?js|mjs|ts|py|sh)$/.test(lower);
12357
+ if (looksLikeCheckout && !lower.includes(repoName)) return true;
12358
+ }
12359
+ return false;
11387
12360
  }
11388
12361
  function isUnmanagedServer(server) {
11389
12362
  return !SAFE_GLOBAL_MCP_SERVERS.has(server);
@@ -11394,6 +12367,7 @@ function inspectCodexConfig(content, expectedRepoUrl) {
11394
12367
  const serverNames = [...grouped.keys()].sort();
11395
12368
  const greatArrowText = grouped.get(CODEX_SERVER_NAME) ?? "";
11396
12369
  const hasGreatArrowServer = grouped.has(CODEX_SERVER_NAME);
12370
+ const greatArrowInstallVersion = extractInstallVersion(greatArrowText);
11397
12371
  const greatArrowRepoKeys = extractConfiguredRepoKeys(greatArrowText);
11398
12372
  const greatArrowHasRepoHeader = Boolean(
11399
12373
  expectedRepoKey && greatArrowRepoKeys.includes(expectedRepoKey)
@@ -11404,7 +12378,7 @@ function inspectCodexConfig(content, expectedRepoUrl) {
11404
12378
  if (server === CODEX_SERVER_NAME) continue;
11405
12379
  const keys = extractConfiguredRepoKeys(text);
11406
12380
  const mismatchedExplicitRepo = expectedRepoKey && keys.length > 0 && !keys.some((key) => key === expectedRepoKey);
11407
- if (mismatchedExplicitRepo || hasJobbyJobMarker(server, text)) {
12381
+ if (mismatchedExplicitRepo || hasForeignRepoMarker(text, expectedRepoKey)) {
11408
12382
  staleRepoServers.push(server);
11409
12383
  }
11410
12384
  }
@@ -11436,6 +12410,36 @@ function inspectCodexConfig(content, expectedRepoUrl) {
11436
12410
  }
11437
12411
  );
11438
12412
  }
12413
+ if (hasGreatArrowServer) {
12414
+ if (!greatArrowInstallVersion) {
12415
+ issues.push({
12416
+ code: "install_version_missing",
12417
+ level: "warn",
12418
+ message: "Great Arrow is missing the install-version header.",
12419
+ hint: "Run `gad mcp install codex` to refresh Codex update tracking."
12420
+ });
12421
+ } else if (compareVersion(greatArrowInstallVersion, GAD_INSTALL_MIN_VERSION) < 0) {
12422
+ issues.push({
12423
+ code: "install_version_required",
12424
+ level: "fail",
12425
+ message: `Great Arrow install ${greatArrowInstallVersion} is below the supported minimum ${GAD_INSTALL_MIN_VERSION}.`,
12426
+ hint: "Update the CLI with `npm install -g @great-arrow/cli@latest`, then run `gad mcp install codex`."
12427
+ });
12428
+ } else if (compareVersion(greatArrowInstallVersion, GAD_INSTALL_VERSION) < 0) {
12429
+ issues.push({
12430
+ code: "install_version_stale",
12431
+ level: "warn",
12432
+ message: `Great Arrow install ${greatArrowInstallVersion} can be refreshed to ${GAD_INSTALL_VERSION}.`,
12433
+ hint: "Run `gad mcp install codex` to rewrite the Codex block with the latest self-healing headers."
12434
+ });
12435
+ } else {
12436
+ issues.push({
12437
+ code: "install_version_current",
12438
+ level: "pass",
12439
+ message: `Great Arrow install version ${greatArrowInstallVersion} is current.`
12440
+ });
12441
+ }
12442
+ }
11439
12443
  if (staleRepoServers.length > 0) {
11440
12444
  issues.push({
11441
12445
  code: "stale_repo_servers",
@@ -11463,6 +12467,8 @@ function inspectCodexConfig(content, expectedRepoUrl) {
11463
12467
  expectedRepoKey,
11464
12468
  hasGreatArrowServer,
11465
12469
  greatArrowHasRepoHeader,
12470
+ greatArrowInstallVersion,
12471
+ greatArrowInstallVersionCurrent: Boolean(greatArrowInstallVersion) && compareVersion(greatArrowInstallVersion, GAD_INSTALL_VERSION) >= 0,
11466
12472
  unmanagedServers,
11467
12473
  staleRepoServers,
11468
12474
  issues
@@ -11481,8 +12487,134 @@ function repairCodexConfig(content, staleRepoServers) {
11481
12487
  };
11482
12488
  }
11483
12489
 
12490
+ // src/lib/json-mcp-config.ts
12491
+ var JSON_SERVER_NAME = "great-arrow";
12492
+ function stripBom(content) {
12493
+ return content.codePointAt(0) === 65279 ? content.slice(1) : content;
12494
+ }
12495
+ function asString(v) {
12496
+ return typeof v === "string" ? v : null;
12497
+ }
12498
+ function explicitRepoKeys(entry) {
12499
+ const keys = /* @__PURE__ */ new Set();
12500
+ const headerVal = asString(entry.headers?.["X-Repo-URL"]) ?? asString(entry.headers?.["x-repo-url"]);
12501
+ const envVals = [
12502
+ asString(entry.env?.MEMORY_REPO_URL),
12503
+ asString(entry.env?.GAD_REPO_URL)
12504
+ ];
12505
+ for (const v of [headerVal, ...envVals]) {
12506
+ const k = repoKey(v);
12507
+ if (k) keys.add(k);
12508
+ }
12509
+ const text = JSON.stringify([entry.url, entry.args]);
12510
+ const gitUrls = /(?:https:\/\/|ssh:\/\/git@|git@)(?:github\.com|gitlab\.com|bitbucket\.org)[:/][^\s"',\]]+/gi;
12511
+ for (const m of text.matchAll(gitUrls)) {
12512
+ const k = repoKey(m[0]);
12513
+ if (k) keys.add(k);
12514
+ }
12515
+ return [...keys];
12516
+ }
12517
+ function inspectJsonMcpConfig(content, expectedRepoUrl) {
12518
+ const expectedRepoKey = repoKey(expectedRepoUrl);
12519
+ const base = {
12520
+ expectedRepoUrl: expectedRepoUrl ?? null,
12521
+ expectedRepoKey
12522
+ };
12523
+ if (!content.trim()) {
12524
+ return {
12525
+ ...base,
12526
+ parseError: null,
12527
+ staleRepoServers: [],
12528
+ issues: [
12529
+ { code: "no_config", level: "warn", message: "No MCP config found." }
12530
+ ]
12531
+ };
12532
+ }
12533
+ let parsed;
12534
+ try {
12535
+ parsed = JSON.parse(stripBom(content));
12536
+ } catch (e) {
12537
+ return {
12538
+ ...base,
12539
+ parseError: e instanceof Error ? e.message : String(e),
12540
+ staleRepoServers: [],
12541
+ issues: [
12542
+ {
12543
+ code: "parse_error",
12544
+ level: "warn",
12545
+ message: "Could not parse MCP config (left untouched)."
12546
+ }
12547
+ ]
12548
+ };
12549
+ }
12550
+ const serversRaw = parsed.mcpServers;
12551
+ const servers = serversRaw && typeof serversRaw === "object" && !Array.isArray(serversRaw) ? serversRaw : {};
12552
+ const stale = [];
12553
+ for (const [name, entry] of Object.entries(servers)) {
12554
+ if (name === JSON_SERVER_NAME) continue;
12555
+ if (!entry || typeof entry !== "object") continue;
12556
+ const keys = explicitRepoKeys(entry);
12557
+ const mismatchedExplicit = expectedRepoKey && keys.length > 0 && !keys.some((k) => k === expectedRepoKey);
12558
+ if (mismatchedExplicit || hasForeignRepoMarker(JSON.stringify(entry), expectedRepoKey)) {
12559
+ stale.push(name);
12560
+ }
12561
+ }
12562
+ stale.sort();
12563
+ const issues = [
12564
+ JSON_SERVER_NAME in servers ? {
12565
+ code: "great_arrow_present",
12566
+ level: "pass",
12567
+ message: "Great Arrow MCP server present."
12568
+ } : {
12569
+ code: "great_arrow_missing",
12570
+ level: "warn",
12571
+ message: "Great Arrow MCP server not found in this config.",
12572
+ hint: "Run `gad mcp install` from the repo root."
12573
+ },
12574
+ stale.length > 0 ? {
12575
+ code: "stale_repo_servers",
12576
+ level: "fail",
12577
+ message: `Found stale repo-scoped MCP server(s): ${stale.join(", ")}.`,
12578
+ hint: "Run `gad doctor clients --enforce` to remove the stale blocks."
12579
+ } : {
12580
+ code: "no_stale_repo_servers",
12581
+ level: "pass",
12582
+ message: "No stale repo-scoped MCP server blocks found."
12583
+ }
12584
+ ];
12585
+ return { ...base, parseError: null, staleRepoServers: stale, issues };
12586
+ }
12587
+ function repairJsonMcpConfig(content, staleRepoServers) {
12588
+ if (!content.trim() || staleRepoServers.length === 0) {
12589
+ return { content, removedServers: [] };
12590
+ }
12591
+ let parsed;
12592
+ try {
12593
+ parsed = JSON.parse(stripBom(content));
12594
+ } catch {
12595
+ return { content, removedServers: [] };
12596
+ }
12597
+ const servers = parsed.mcpServers;
12598
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
12599
+ return { content, removedServers: [] };
12600
+ }
12601
+ const removed = [];
12602
+ for (const name of new Set(staleRepoServers)) {
12603
+ if (name in servers) {
12604
+ delete servers[name];
12605
+ removed.push(name);
12606
+ }
12607
+ }
12608
+ removed.sort();
12609
+ if (removed.length === 0) return { content, removedServers: [] };
12610
+ return {
12611
+ content: JSON.stringify(parsed, null, 2) + "\n",
12612
+ removedServers: removed
12613
+ };
12614
+ }
12615
+
11484
12616
  // src/commands/doctor.ts
11485
- function compareVersion(a, b) {
12617
+ function compareVersion2(a, b) {
11486
12618
  const left = a.match(/\d+|[a-z]+/gi) ?? [];
11487
12619
  const right = b.match(/\d+|[a-z]+/gi) ?? [];
11488
12620
  const length = Math.max(left.length, right.length);
@@ -11584,7 +12716,7 @@ async function checkCliVersion(auth) {
11584
12716
  const latest = cli.latest ?? CLI_VERSION;
11585
12717
  const minimum = cli.minimum ?? CLI_VERSION;
11586
12718
  const updateCommand2 = cli.updateCommand ?? `npm install -g ${CLI_PACKAGE_NAME}@latest`;
11587
- if (compareVersion(CLI_VERSION, minimum) < 0) {
12719
+ if (compareVersion2(CLI_VERSION, minimum) < 0) {
11588
12720
  return {
11589
12721
  name: "CLI Version",
11590
12722
  status: "fail",
@@ -11592,7 +12724,7 @@ async function checkCliVersion(auth) {
11592
12724
  hint: `Update with: ${updateCommand2}`
11593
12725
  };
11594
12726
  }
11595
- if (compareVersion(CLI_VERSION, latest) < 0) {
12727
+ if (compareVersion2(CLI_VERSION, latest) < 0) {
11596
12728
  return {
11597
12729
  name: "CLI Version",
11598
12730
  status: "warn",
@@ -11682,7 +12814,7 @@ async function checkMcp(auth) {
11682
12814
  }
11683
12815
  }
11684
12816
  function defaultCodexConfigPath() {
11685
- return join2(homedir2(), ".codex", "config.toml");
12817
+ return join4(homedir2(), ".codex", "config.toml");
11686
12818
  }
11687
12819
  function codexIssuesToChecks(issues) {
11688
12820
  return issues.map((issue) => {
@@ -11720,13 +12852,13 @@ function codexCommand() {
11720
12852
  });
11721
12853
  const configPath = options.config ?? defaultCodexConfigPath();
11722
12854
  const expectedRepoUrl = options.expectedRepo ?? detectGitOrigin2();
11723
- let content = existsSync(configPath) ? await readFile2(configPath, "utf8") : "";
12855
+ let content = existsSync3(configPath) ? await readFile2(configPath, "utf8") : "";
11724
12856
  let backupPath = null;
11725
12857
  let removedServers = [];
11726
12858
  if (options.enforce) {
11727
12859
  const apiUrl = extractCodexGreatArrowApiUrl(content) ?? (await resolveAuth()).apiUrl;
11728
12860
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
11729
- await mkdir2(dirname2(configPath), { recursive: true });
12861
+ await mkdir2(dirname3(configPath), { recursive: true });
11730
12862
  if (content.trim()) {
11731
12863
  backupPath = `${configPath}.bak.${timestamp}`;
11732
12864
  await copyFile(configPath, backupPath);
@@ -11808,6 +12940,137 @@ function codexCommand() {
11808
12940
  }
11809
12941
  );
11810
12942
  }
12943
+ function defaultJsonClientConfigs() {
12944
+ const home = homedir2();
12945
+ return [
12946
+ { name: "Claude Code", path: join4(home, ".claude.json") },
12947
+ { name: "Cursor", path: join4(home, ".cursor", "mcp.json") },
12948
+ { name: "Kiro", path: join4(home, ".kiro", "settings", "mcp.json") },
12949
+ { name: "Gemini CLI", path: join4(home, ".gemini", "settings.json") }
12950
+ ];
12951
+ }
12952
+ function clientsCommand() {
12953
+ return new Command("clients").description(
12954
+ "Check JSON MCP client configs (Claude Code / Cursor / Kiro / Gemini) for wrong-repo server entries and optionally repair"
12955
+ ).option(
12956
+ "--expected-repo <url>",
12957
+ "Expected repo URL (defaults to current git origin)"
12958
+ ).option(
12959
+ "--enforce",
12960
+ "Back up each config and remove stale repo-scoped MCP servers"
12961
+ ).option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(
12962
+ async (options, command) => {
12963
+ const g = globalOutputOpts(command);
12964
+ const color2 = createColor(g.noColor);
12965
+ const fmt = createFormatter({
12966
+ json: g.json,
12967
+ quiet: g.quiet,
12968
+ isTTY: isTTY(),
12969
+ noColor: g.noColor
12970
+ });
12971
+ const expectedRepoUrl = options.expectedRepo ?? detectGitOrigin2();
12972
+ const checks = [];
12973
+ const results = [];
12974
+ for (const { name, path: path9 } of defaultJsonClientConfigs()) {
12975
+ if (!existsSync3(path9)) continue;
12976
+ let content = await readFile2(path9, "utf8");
12977
+ let backupPath = null;
12978
+ let removedServers = [];
12979
+ if (options.enforce) {
12980
+ const pre = inspectJsonMcpConfig(content, expectedRepoUrl);
12981
+ if (!pre.parseError && pre.staleRepoServers.length > 0) {
12982
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
12983
+ backupPath = `${path9}.bak.${timestamp}`;
12984
+ await copyFile(path9, backupPath);
12985
+ const repaired = repairJsonMcpConfig(
12986
+ content,
12987
+ pre.staleRepoServers
12988
+ );
12989
+ content = repaired.content;
12990
+ removedServers = repaired.removedServers;
12991
+ await writeFile2(path9, content, "utf8");
12992
+ }
12993
+ }
12994
+ const inspection = inspectJsonMcpConfig(content, expectedRepoUrl);
12995
+ for (const issue of inspection.issues) {
12996
+ const check = {
12997
+ name: `${name}: ${issue.code}`,
12998
+ status: issue.level,
12999
+ message: issue.message
13000
+ };
13001
+ if (issue.hint) check.hint = issue.hint;
13002
+ checks.push(check);
13003
+ }
13004
+ if (options.enforce && removedServers.length > 0) {
13005
+ const enforcementCheck = {
13006
+ name: `${name}: enforcement`,
13007
+ status: "pass",
13008
+ message: `Removed stale server(s): ${removedServers.join(", ")}`
13009
+ };
13010
+ if (backupPath) enforcementCheck.hint = `Backup: ${backupPath}`;
13011
+ checks.push(enforcementCheck);
13012
+ }
13013
+ results.push({
13014
+ name,
13015
+ path: path9,
13016
+ backupPath,
13017
+ removedServers,
13018
+ inspection
13019
+ });
13020
+ }
13021
+ if (results.length === 0) {
13022
+ checks.push({
13023
+ name: "JSON clients",
13024
+ status: "warn",
13025
+ message: "No JSON MCP client configs found on this machine."
13026
+ });
13027
+ }
13028
+ if (g.json) {
13029
+ process.stdout.write(
13030
+ JSON.stringify(
13031
+ { expectedRepoUrl, enforce: Boolean(options.enforce), results },
13032
+ null,
13033
+ 2
13034
+ ) + "\n"
13035
+ );
13036
+ } else {
13037
+ if (!expectedRepoUrl) {
13038
+ process.stdout.write(
13039
+ renderCheck(
13040
+ {
13041
+ name: "JSON clients: repo detection",
13042
+ status: "warn",
13043
+ message: "No git origin detected for this directory",
13044
+ hint: "Run from a repo root or pass --expected-repo."
13045
+ },
13046
+ color2
13047
+ ) + "\n"
13048
+ );
13049
+ }
13050
+ for (const check of checks) {
13051
+ process.stdout.write(renderCheck(check, color2) + "\n");
13052
+ }
13053
+ if (!g.quiet) {
13054
+ const failed = checks.filter((c) => c.status === "fail").length;
13055
+ const warned = checks.filter((c) => c.status === "warn").length;
13056
+ process.stdout.write("\n");
13057
+ if (failed === 0 && warned === 0) {
13058
+ fmt.success("JSON client config guard passed");
13059
+ } else if (failed === 0) {
13060
+ fmt.warn(`JSON client guard passed with ${warned} warning(s)`);
13061
+ } else {
13062
+ fmt.error(
13063
+ `JSON client guard found ${failed} failure(s), ${warned} warning(s)`
13064
+ );
13065
+ }
13066
+ }
13067
+ }
13068
+ if (checks.some((check) => check.status === "fail")) {
13069
+ process.exitCode = 1;
13070
+ }
13071
+ }
13072
+ );
13073
+ }
11811
13074
  function doctorCommand() {
11812
13075
  const cmd = new Command("doctor").description("Run a comprehensive health check").option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(async (options, command) => {
11813
13076
  const g = globalOutputOpts(command);
@@ -11862,6 +13125,7 @@ function doctorCommand() {
11862
13125
  }
11863
13126
  });
11864
13127
  cmd.addCommand(codexCommand());
13128
+ cmd.addCommand(clientsCommand());
11865
13129
  return cmd;
11866
13130
  }
11867
13131
 
@@ -11907,7 +13171,7 @@ var integrationListColumns = [
11907
13171
  }
11908
13172
  }
11909
13173
  ];
11910
- async function createClient2() {
13174
+ async function createClient3() {
11911
13175
  const auth = await resolveAuth();
11912
13176
  return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11913
13177
  }
@@ -11969,7 +13233,7 @@ var listCommand3 = new Command("list").description("List all connected integrati
11969
13233
  isTTY: isTTY(),
11970
13234
  noColor: false
11971
13235
  });
11972
- const client = await createClient2();
13236
+ const client = await createClient3();
11973
13237
  const result = await client.mcpCall("integrations_list", {});
11974
13238
  const data = result;
11975
13239
  let integrations = [];
@@ -11994,7 +13258,7 @@ var syncCommand = new Command("sync").description("Trigger a manual sync for the
11994
13258
  isTTY: isTTY(),
11995
13259
  noColor: false
11996
13260
  });
11997
- const client = await createClient2();
13261
+ const client = await createClient3();
11998
13262
  fmt.info(`Triggering sync for "${kind}"...`);
11999
13263
  try {
12000
13264
  const result = await client.mcpCall("integration_sync", { kind });
@@ -12032,7 +13296,7 @@ var CLIENT_IDS = {
12032
13296
  codex: CODEX_CLIENT_ID,
12033
13297
  gemini: "gemini-cli"
12034
13298
  };
12035
- async function createClient3() {
13299
+ async function createClient4() {
12036
13300
  const auth = await resolveAuth();
12037
13301
  const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
12038
13302
  return { client, apiUrl: auth.apiUrl };
@@ -12094,11 +13358,11 @@ async function pathExists(path9) {
12094
13358
  }
12095
13359
  async function installCodexConfig(apiUrl, token, repoUrl) {
12096
13360
  const { homedir: homedir3 } = await import("node:os");
12097
- const { join: join3 } = await import("node:path");
13361
+ const { join: join5 } = await import("node:path");
12098
13362
  const { chmod, copyFile: copyFile2, mkdir: mkdir3, readFile: readFile3, writeFile: writeFile3 } = await import("node:fs/promises");
12099
- const codexDir = join3(homedir3(), ".codex");
12100
- const configPath = join3(codexDir, "config.toml");
12101
- const envPath = join3(codexDir, ".env");
13363
+ const codexDir = join5(homedir3(), ".codex");
13364
+ const configPath = join5(codexDir, "config.toml");
13365
+ const envPath = join5(codexDir, ".env");
12102
13366
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
12103
13367
  await mkdir3(codexDir, { recursive: true });
12104
13368
  const existingConfig = await pathExists(configPath) ? await readFile3(configPath, "utf8") : "";
@@ -12140,7 +13404,7 @@ var statusCommand = new Command("status").description(
12140
13404
  isTTY: isTTY(),
12141
13405
  noColor: false
12142
13406
  });
12143
- const { client, apiUrl } = await createClient3();
13407
+ const { client, apiUrl } = await createClient4();
12144
13408
  let statusData = {
12145
13409
  status: "unknown",
12146
13410
  proxyVersion: "\u2014",
@@ -12199,7 +13463,7 @@ var logsCommand = new Command("logs").description("Tail MCP connection log with
12199
13463
  });
12200
13464
  const c = createColor(false);
12201
13465
  const lineCount = Number.parseInt(options.lines ?? "20", 10);
12202
- const { client } = await createClient3();
13466
+ const { client } = await createClient4();
12203
13467
  try {
12204
13468
  const result = await client.mcpCall("integration_sync_logs", {
12205
13469
  limit: lineCount
@@ -12365,6 +13629,9 @@ var installCommand = new Command("install").description("Generate MCP configurat
12365
13629
  repoUrl
12366
13630
  );
12367
13631
  fmt.success(`Wrote Codex MCP config to ${configPath} and ${envPath}`);
13632
+ fmt.info(
13633
+ "Restart Codex, start a fresh chat, then call tools_discover; tools_discover and tools_enable should be visible before enabling more Great Arrow tools."
13634
+ );
12368
13635
  return;
12369
13636
  }
12370
13637
  const config = generateMcpConfig(
@@ -12379,8 +13646,8 @@ var installCommand = new Command("install").description("Generate MCP configurat
12379
13646
  return;
12380
13647
  }
12381
13648
  const { writeFile: writeFile3 } = await import("node:fs/promises");
12382
- const { join: join3 } = await import("node:path");
12383
- const outputPath = join3(process.cwd(), ".mcp.json");
13649
+ const { join: join5 } = await import("node:path");
13650
+ const outputPath = join5(process.cwd(), ".mcp.json");
12384
13651
  await writeFile3(outputPath, configJson + "\n", "utf-8");
12385
13652
  fmt.success(`Wrote ${clientName} MCP config to .mcp.json`);
12386
13653
  }
@@ -13073,7 +14340,7 @@ var retryifyAsync = (fn, options) => {
13073
14340
  throw error;
13074
14341
  const delay2 = Math.round(interval * Math.random());
13075
14342
  if (delay2 > 0) {
13076
- const delayPromise = new Promise((resolve) => setTimeout(resolve, delay2));
14343
+ const delayPromise = new Promise((resolve3) => setTimeout(resolve3, delay2));
13077
14344
  return delayPromise.then(() => attempt.apply(void 0, args));
13078
14345
  } else {
13079
14346
  return attempt.apply(void 0, args);
@@ -13332,23 +14599,23 @@ var Temp = {
13332
14599
  }
13333
14600
  },
13334
14601
  truncate: (filePath) => {
13335
- const basename = path2.basename(filePath);
13336
- if (basename.length <= LIMIT_BASENAME_LENGTH)
14602
+ const basename2 = path2.basename(filePath);
14603
+ if (basename2.length <= LIMIT_BASENAME_LENGTH)
13337
14604
  return filePath;
13338
- const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename);
14605
+ const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename2);
13339
14606
  if (!truncable)
13340
14607
  return filePath;
13341
- const truncationLength = basename.length - LIMIT_BASENAME_LENGTH;
13342
- return `${filePath.slice(0, -basename.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
14608
+ const truncationLength = basename2.length - LIMIT_BASENAME_LENGTH;
14609
+ return `${filePath.slice(0, -basename2.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
13343
14610
  }
13344
14611
  };
13345
14612
  node_default(Temp.purgeSyncAll);
13346
14613
  var temp_default = Temp;
13347
14614
 
13348
14615
  // node_modules/.pnpm/atomically@2.1.1/node_modules/atomically/dist/index.js
13349
- function writeFileSync(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
14616
+ function writeFileSync3(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
13350
14617
  if (isString(options))
13351
- return writeFileSync(filePath, data, { encoding: options });
14618
+ return writeFileSync3(filePath, data, { encoding: options });
13352
14619
  const timeout2 = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
13353
14620
  const retryOptions = { timeout: timeout2 };
13354
14621
  let tempDisposer = null;
@@ -13675,7 +14942,7 @@ var Configstore = class {
13675
14942
  }
13676
14943
  if (error.name === "SyntaxError") {
13677
14944
  if (this._clearInvalidConfig) {
13678
- writeFileSync(this._path, "", writeFileOptions);
14945
+ writeFileSync3(this._path, "", writeFileOptions);
13679
14946
  return {};
13680
14947
  }
13681
14948
  throw error;
@@ -13687,7 +14954,7 @@ var Configstore = class {
13687
14954
  set all(value) {
13688
14955
  try {
13689
14956
  import_graceful_fs.default.mkdirSync(path4.dirname(this._path), mkdirOptions);
13690
- writeFileSync(this._path, JSON.stringify(value, void 0, " "), writeFileOptions);
14957
+ writeFileSync3(this._path, JSON.stringify(value, void 0, " "), writeFileOptions);
13691
14958
  } catch (error) {
13692
14959
  handlePermissionError(error);
13693
14960
  }
@@ -14639,14 +15906,14 @@ var TimeoutError = class extends Error {
14639
15906
 
14640
15907
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
14641
15908
  async function timeout(request, init, abortController, options) {
14642
- return new Promise((resolve, reject) => {
15909
+ return new Promise((resolve3, reject) => {
14643
15910
  const timeoutId = setTimeout(() => {
14644
15911
  if (abortController) {
14645
15912
  abortController.abort();
14646
15913
  }
14647
15914
  reject(new TimeoutError(request));
14648
15915
  }, options.timeout);
14649
- void options.fetch(request, init).then(resolve).catch(reject).then(() => {
15916
+ void options.fetch(request, init).then(resolve3).catch(reject).then(() => {
14650
15917
  clearTimeout(timeoutId);
14651
15918
  });
14652
15919
  });
@@ -14654,7 +15921,7 @@ async function timeout(request, init, abortController, options) {
14654
15921
 
14655
15922
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
14656
15923
  async function delay(ms, { signal }) {
14657
- return new Promise((resolve, reject) => {
15924
+ return new Promise((resolve3, reject) => {
14658
15925
  if (signal) {
14659
15926
  signal.throwIfAborted();
14660
15927
  signal.addEventListener("abort", abortHandler, { once: true });
@@ -14665,7 +15932,7 @@ async function delay(ms, { signal }) {
14665
15932
  }
14666
15933
  const timeoutId = setTimeout(() => {
14667
15934
  signal?.removeEventListener("abort", abortHandler);
14668
- resolve();
15935
+ resolve3();
14669
15936
  }, ms);
14670
15937
  });
14671
15938
  }
@@ -16383,6 +17650,10 @@ program2.addCommand(memoryCommand);
16383
17650
  program2.addCommand(workspace_default);
16384
17651
  program2.addCommand(todo_default);
16385
17652
  program2.addCommand(claim_default);
17653
+ program2.addCommand(work_default());
17654
+ program2.addCommand(worktree_default);
17655
+ program2.addCommand(install_hooks_default);
17656
+ program2.addCommand(guardrailsCommand());
16386
17657
  program2.addCommand(doctorCommand());
16387
17658
  program2.addCommand(integrations_default);
16388
17659
  program2.addCommand(mcp_default);