@indigoai-us/hq-cli 5.315.0 → 5.317.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 (55) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/assets/scaffold/core/scripts/detect-stale-review-base.sh +199 -0
  3. package/assets/scaffold/core/scripts/repo-run-registry.sh +408 -0
  4. package/assets/scaffold/core/scripts/session-journal.sh +301 -0
  5. package/assets/scaffold/core/scripts/session-project.sh +800 -0
  6. package/assets/scaffold/core/scripts/session-title-config.sh +76 -0
  7. package/assets/scaffold/core/scripts/session-title.sh +305 -0
  8. package/assets/scaffold/core/scripts/share-suggestion-state.sh +541 -0
  9. package/dist/command-catalog.generated.d.ts +97 -0
  10. package/dist/command-catalog.generated.js +120 -0
  11. package/dist/commands/bot-jobs.d.ts +5 -1
  12. package/dist/commands/bot-jobs.js +24 -4
  13. package/dist/commands/core-detect-stale-review-base-native.d.ts +23 -0
  14. package/dist/commands/core-detect-stale-review-base-native.js +178 -0
  15. package/dist/commands/core-hook-state-helpers-native.d.ts +11 -0
  16. package/dist/commands/core-hook-state-helpers-native.js +74 -0
  17. package/dist/commands/core-repo-run-registry-native.d.ts +19 -0
  18. package/dist/commands/core-repo-run-registry-native.js +357 -0
  19. package/dist/commands/core-session-helper-runtime.d.ts +10 -0
  20. package/dist/commands/core-session-helper-runtime.js +7 -0
  21. package/dist/commands/core-session-journal-native.d.ts +4 -0
  22. package/dist/commands/core-session-journal-native.js +327 -0
  23. package/dist/commands/core-session-project-native.d.ts +3 -0
  24. package/dist/commands/core-session-project-native.js +855 -0
  25. package/dist/commands/core-session-title-config-native.d.ts +4 -0
  26. package/dist/commands/core-session-title-config-native.js +77 -0
  27. package/dist/commands/core-session-title-native.d.ts +3 -0
  28. package/dist/commands/core-session-title-native.js +306 -0
  29. package/dist/commands/core-share-suggestion-state-native.d.ts +18 -0
  30. package/dist/commands/core-share-suggestion-state-native.js +445 -0
  31. package/dist/commands/core.js +66 -0
  32. package/dist/commands/lanes.js +35 -5
  33. package/dist/fast-core.d.ts +1 -0
  34. package/dist/fast-core.js +10 -6
  35. package/dist/fast-scaffold.js +33 -0
  36. package/dist/lib/bot/job-quiet-flag.d.ts +4 -0
  37. package/dist/lib/bot/job-quiet-flag.js +14 -0
  38. package/dist/lib/bot/job-run.d.ts +5 -0
  39. package/dist/lib/bot/job-run.js +23 -3
  40. package/dist/lib/bot/run.d.ts +2 -0
  41. package/dist/lib/bot/run.js +15 -3
  42. package/dist/lib/core-utils/common.d.ts +0 -1
  43. package/dist/lib/core-utils/common.js +0 -9
  44. package/dist/lib/lanes/autopilot.d.ts +5 -1
  45. package/dist/lib/lanes/autopilot.js +72 -3
  46. package/dist/lib/lanes/disk-guard.d.ts +3 -4
  47. package/dist/lib/lanes/disk-guard.js +2 -2
  48. package/dist/lib/lanes/flag-keys.d.ts +3 -0
  49. package/dist/lib/lanes/flag-keys.js +3 -0
  50. package/dist/lib/lanes/hooks-install.js +78 -2
  51. package/dist/lib/lanes/keep-alive-audit.d.ts +6 -5
  52. package/dist/lib/lanes/outpost-dispatch.d.ts +9 -0
  53. package/dist/lib/lanes/outpost-dispatch.js +22 -0
  54. package/dist/utils/version-gate.js +8 -7
  55. package/package.json +1 -1
@@ -7633,6 +7633,118 @@ export const COMMAND_CATALOG = [
7633
7633
  "options": [],
7634
7634
  "subcommands": []
7635
7635
  },
7636
+ {
7637
+ "name": "share-suggestion-state",
7638
+ "description": "Manage hook share suggestion state",
7639
+ "aliases": [],
7640
+ "hidden": false,
7641
+ "usage": "[args...]",
7642
+ "arguments": [
7643
+ {
7644
+ "name": "args",
7645
+ "required": false,
7646
+ "variadic": true
7647
+ }
7648
+ ],
7649
+ "options": [],
7650
+ "subcommands": []
7651
+ },
7652
+ {
7653
+ "name": "repo-run-registry",
7654
+ "description": "Coordinate active runs across repositories",
7655
+ "aliases": [],
7656
+ "hidden": false,
7657
+ "usage": "[args...]",
7658
+ "arguments": [
7659
+ {
7660
+ "name": "args",
7661
+ "required": false,
7662
+ "variadic": true
7663
+ }
7664
+ ],
7665
+ "options": [],
7666
+ "subcommands": []
7667
+ },
7668
+ {
7669
+ "name": "detect-stale-review-base",
7670
+ "description": "Find stale remote review bases in an HQ root",
7671
+ "aliases": [],
7672
+ "hidden": false,
7673
+ "usage": "[args...]",
7674
+ "arguments": [
7675
+ {
7676
+ "name": "args",
7677
+ "required": false,
7678
+ "variadic": true
7679
+ }
7680
+ ],
7681
+ "options": [],
7682
+ "subcommands": []
7683
+ },
7684
+ {
7685
+ "name": "session-journal",
7686
+ "description": "Write and read session journal entries",
7687
+ "aliases": [],
7688
+ "hidden": false,
7689
+ "usage": "[args...]",
7690
+ "arguments": [
7691
+ {
7692
+ "name": "args",
7693
+ "required": false,
7694
+ "variadic": true
7695
+ }
7696
+ ],
7697
+ "options": [],
7698
+ "subcommands": []
7699
+ },
7700
+ {
7701
+ "name": "session-project",
7702
+ "description": "Find and update native session projects",
7703
+ "aliases": [],
7704
+ "hidden": false,
7705
+ "usage": "[args...]",
7706
+ "arguments": [
7707
+ {
7708
+ "name": "args",
7709
+ "required": false,
7710
+ "variadic": true
7711
+ }
7712
+ ],
7713
+ "options": [],
7714
+ "subcommands": []
7715
+ },
7716
+ {
7717
+ "name": "session-title",
7718
+ "description": "Compute the current session title",
7719
+ "aliases": [],
7720
+ "hidden": false,
7721
+ "usage": "[args...]",
7722
+ "arguments": [
7723
+ {
7724
+ "name": "args",
7725
+ "required": false,
7726
+ "variadic": true
7727
+ }
7728
+ ],
7729
+ "options": [],
7730
+ "subcommands": []
7731
+ },
7732
+ {
7733
+ "name": "session-title-config",
7734
+ "description": "Resolve session title settings",
7735
+ "aliases": [],
7736
+ "hidden": false,
7737
+ "usage": "[args...]",
7738
+ "arguments": [
7739
+ {
7740
+ "name": "args",
7741
+ "required": false,
7742
+ "variadic": true
7743
+ }
7744
+ ],
7745
+ "options": [],
7746
+ "subcommands": []
7747
+ },
7636
7748
  {
7637
7749
  "name": "worktree-gc",
7638
7750
  "description": "Safely collect old, preserved HQ worktrees",
@@ -10750,6 +10862,10 @@ export const COMMAND_CATALOG = [
10750
10862
  "flags": "--lane <id>",
10751
10863
  "description": "Filter by lane id (repeatable)"
10752
10864
  },
10865
+ {
10866
+ "flags": "--lane-ulid <ulid>",
10867
+ "description": "Filter by lane ULID for internal owner lookup"
10868
+ },
10753
10869
  {
10754
10870
  "flags": "--active",
10755
10871
  "description": "Include only lanes that are not terminal"
@@ -12943,6 +13059,10 @@ export const COMMAND_CATALOG = [
12943
13059
  "flags": "--tz <zone>",
12944
13060
  "description": "IANA timezone for the schedule (default: this computer's)"
12945
13061
  },
13062
+ {
13063
+ "flags": "--quiet",
13064
+ "description": "Skip owner DMs after successful runs (requires rollout flag)"
13065
+ },
12946
13066
  {
12947
13067
  "flags": "--rm <jobId>",
12948
13068
  "description": "Remove a scheduled job"
@@ -74,6 +74,7 @@ export declare function buildCreateJobBody(input: {
74
74
  cron: string;
75
75
  timezone: string;
76
76
  context?: Record<string, unknown>;
77
+ quiet?: boolean;
77
78
  }): CreateJobInput;
78
79
  /** A schedule in plain words, falling back to the raw expression. */
79
80
  export declare function describeSchedule(job: BotJobRecord): string;
@@ -81,7 +82,7 @@ export declare function describeSchedule(job: BotJobRecord): string;
81
82
  export declare function cronInWords(expr: string): string | null;
82
83
  /** "never run yet" / "succeeded 2026-09-15 09:00 UTC". */
83
84
  export declare function describeLastRun(job: BotJobRecord): string;
84
- export declare function formatJobsList(botName: string, jobs: BotJobRecord[]): string;
85
+ export declare function formatJobsList(botName: string, jobs: BotJobRecord[], showQuietOption?: boolean): string;
85
86
  /**
86
87
  * Turn a jobs-route failure into one plain line.
87
88
  *
@@ -113,6 +114,8 @@ export interface BotJobsDeps {
113
114
  parseSchedule?: typeof parseScheduleText;
114
115
  timezone?: () => string;
115
116
  out?: (line: string) => void;
117
+ /** Test seam for the default-off quiet-job rollout gate. */
118
+ quietOptionEnabled?: () => boolean | Promise<boolean>;
116
119
  }
117
120
  export interface BotJobsOptions {
118
121
  add?: string;
@@ -121,6 +124,7 @@ export interface BotJobsOptions {
121
124
  tz?: string;
122
125
  rm?: string;
123
126
  json?: boolean;
127
+ quiet?: boolean;
124
128
  }
125
129
  export declare function runBotJobs(nameArg: string, opts: BotJobsOptions, deps: BotJobsDeps): Promise<void>;
126
130
  export declare function registerBotJobsCommand(bot: Command, deps: BotJobsDeps): void;
@@ -21,6 +21,8 @@ import * as path from "node:path";
21
21
  import { localJobsClient } from "../lib/bot/local-jobs.js";
22
22
  import { BotApiError } from "../lib/bot/api.js";
23
23
  import { effectiveBotKind } from "../lib/bot/config.js";
24
+ import { isQuietJobContext, JOB_RUN_OPTIONS_CONTEXT_KEY } from "../lib/bot/job-run.js";
25
+ import { BOT_JOB_QUIET_OPTION_FLAG, botJobQuietOptionEnabled } from "../lib/bot/job-quiet-flag.js";
24
26
  // ─── Schedule translation ────────────────────────────────────────────────────
25
27
  /** Mirrors hq-pro-agents `MIN_RECURRING_INTERVAL_SECONDS`. */
26
28
  export const MIN_RECURRING_INTERVAL_SECONDS = 5 * 60;
@@ -245,7 +247,14 @@ export function buildCreateJobBody(input) {
245
247
  assertScheduleWithinFloor(cron);
246
248
  return {
247
249
  prompt,
248
- ...(input.context ? { context: input.context } : {}),
250
+ ...(input.context || input.quiet
251
+ ? {
252
+ context: {
253
+ ...(input.context ?? {}),
254
+ ...(input.quiet ? { [JOB_RUN_OPTIONS_CONTEXT_KEY]: { quiet: true } } : {}),
255
+ },
256
+ }
257
+ : {}),
249
258
  schedule: { kind: "recurring", cron, timezone: input.timezone },
250
259
  };
251
260
  }
@@ -308,7 +317,7 @@ export function describeLastRun(job) {
308
317
  const when = job.lastRunAt ? ` ${job.lastRunAt.slice(0, 16).replace("T", " ")} UTC` : "";
309
318
  return `${job.lastStatus ?? "ran"}${when}`;
310
319
  }
311
- export function formatJobsList(botName, jobs) {
320
+ export function formatJobsList(botName, jobs, showQuietOption = false) {
312
321
  if (jobs.length === 0) {
313
322
  return `${botName} has no scheduled jobs. Add one with: hq bot jobs ${botName} --add "<what to do>" --every "<when>"`;
314
323
  }
@@ -318,6 +327,8 @@ export function formatJobsList(botName, jobs) {
318
327
  lines.push(` ${chalk.bold(job.jobId)}${job.status && job.status !== "active" ? chalk.yellow(` (${job.status})`) : ""}`);
319
328
  lines.push(` ${job.prompt.replace(/\s+/g, " ").slice(0, 120)}`);
320
329
  lines.push(` runs ${describeSchedule(job)}`);
330
+ if (showQuietOption && isQuietJobContext(job.context))
331
+ lines.push(" delivery: quiet after successful runs");
321
332
  lines.push(` next run: ${job.nextRunAt ? `${job.nextRunAt.slice(0, 16).replace("T", " ")} UTC` : "not scheduled yet"}`);
322
333
  lines.push(` last run: ${describeLastRun(job)}`);
323
334
  }
@@ -365,6 +376,7 @@ export async function runBotJobs(nameArg, opts, deps) {
365
376
  const kind = effectiveBotKind(config);
366
377
  const out = deps.out ?? ((line) => console.log(line));
367
378
  const ctx = { botName: name, kind };
379
+ const quietOptionEnabled = deps.quietOptionEnabled ?? botJobQuietOptionEnabled;
368
380
  // The schedule lives with the bot: this bot runs on this computer, so its
369
381
  // jobs live in its folder and it fires them itself.
370
382
  let client;
@@ -377,6 +389,9 @@ export async function runBotJobs(nameArg, opts, deps) {
377
389
  if (opts.add !== undefined && opts.rm !== undefined) {
378
390
  refuse("Add a job or remove one, not both in the same command.");
379
391
  }
392
+ if (opts.quiet && opts.add === undefined) {
393
+ refuse("Use --quiet with --add to set a scheduled job's delivery option.");
394
+ }
380
395
  if (opts.rm !== undefined) {
381
396
  const jobId = opts.rm.trim();
382
397
  if (!jobId)
@@ -392,6 +407,9 @@ export async function runBotJobs(nameArg, opts, deps) {
392
407
  return;
393
408
  }
394
409
  if (opts.add !== undefined) {
410
+ if (opts.quiet && !(await quietOptionEnabled())) {
411
+ refuse(`--quiet requires the ${BOT_JOB_QUIET_OPTION_FLAG} flag, which is currently disabled.`);
412
+ }
395
413
  if (!opts.every && !opts.cron) {
396
414
  refuse(`Say how often it should run: --every "every weekday at 9am", or --cron "0 9 * * 1-5".`);
397
415
  }
@@ -402,7 +420,7 @@ export async function runBotJobs(nameArg, opts, deps) {
402
420
  const schedule = opts.every
403
421
  ? parse(opts.every, { hqRoot: config.hqRoot, timezone })
404
422
  : { cron: opts.cron, timezone };
405
- const body = buildCreateJobBody({ prompt: opts.add, cron: schedule.cron, timezone: schedule.timezone });
423
+ const body = buildCreateJobBody({ prompt: opts.add, cron: schedule.cron, timezone: schedule.timezone, quiet: opts.quiet });
406
424
  const api = await jobsApi();
407
425
  let created;
408
426
  try {
@@ -429,7 +447,8 @@ export async function runBotJobs(nameArg, opts, deps) {
429
447
  catch (err) {
430
448
  refuse(jobsErrorMessage(err, ctx));
431
449
  }
432
- out(opts.json ? JSON.stringify(jobs, null, 2) : formatJobsList(name, jobs));
450
+ const showQuietOption = await quietOptionEnabled();
451
+ out(opts.json ? JSON.stringify(jobs, null, 2) : formatJobsList(name, jobs, showQuietOption));
433
452
  }
434
453
  export function registerBotJobsCommand(bot, deps) {
435
454
  bot
@@ -439,6 +458,7 @@ export function registerBotJobsCommand(bot, deps) {
439
458
  .option("--every <when>", 'How often, in plain language (e.g. "every weekday at 9am")')
440
459
  .option("--cron <expr>", 'How often, as a cron expression (e.g. "0 9 * * 1-5")')
441
460
  .option("--tz <zone>", "IANA timezone for the schedule (default: this computer's)")
461
+ .option("--quiet", "Skip owner DMs after successful runs (requires rollout flag)")
442
462
  .option("--rm <jobId>", "Remove a scheduled job")
443
463
  .option("--json", "Print machine-readable JSON")
444
464
  .action(async (name, opts) => {
@@ -0,0 +1,23 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { HookHelperOutput } from "./core-share-suggestion-state-native.js";
4
+ export declare const DETECT_STALE_REVIEW_BASE_ASSET = "core/scripts/detect-stale-review-base.sh";
5
+ export declare const DETECT_STALE_REVIEW_BASE_HELP: string;
6
+ type GitResult = {
7
+ code: number;
8
+ stdout: string;
9
+ stderr: string;
10
+ };
11
+ export type StaleReviewDependencies = {
12
+ cwd: string;
13
+ env?: NodeJS.ProcessEnv;
14
+ fs?: Pick<typeof fs, "existsSync" | "statSync" | "realpathSync">;
15
+ runGit?: (args: string[]) => GitResult;
16
+ scriptDir?: string;
17
+ scriptPath?: string;
18
+ };
19
+ type DirectoryPathOps = Pick<typeof path, "dirname" | "join">;
20
+ export declare function findHqRootAncestor(root: string, existsSync: (candidate: string) => boolean, pathOps?: DirectoryPathOps): string | undefined;
21
+ export declare function runDetectStaleReviewBase(args: readonly string[], deps: StaleReviewDependencies): HookHelperOutput;
22
+ export {};
23
+ //# sourceMappingURL=core-detect-stale-review-base-native.d.ts.map
@@ -0,0 +1,178 @@
1
+ /** Native port of detect-stale-review-base.sh; only git is an external dependency. */
2
+ import { spawnSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ export const DETECT_STALE_REVIEW_BASE_ASSET = "core/scripts/detect-stale-review-base.sh";
6
+ export const DETECT_STALE_REVIEW_BASE_HELP = [
7
+ "# detect-stale-review-base.sh — find remote-tracking refs in the HQ root that",
8
+ "# have fallen so far behind local HEAD that a diff/review tool will pick one as",
9
+ "# its comparison base and try to review the entire tree.",
10
+ "#",
11
+ "Background: HQ autosaves one commit per edited file (.claude/hooks/hq-autocommit.sh)",
12
+ "and is never pushed to a remote (policy hq-root-never-push-remote). An HQ root",
13
+ "that is itself a git repo with a remote attached therefore accumulates local",
14
+ "commits the remote-tracking ref never receives. The gap only grows.",
15
+ "#",
16
+ "Review tools that auto-select a base branch resolve it from refs/remotes/*.",
17
+ "Once the gap is large enough that base is effectively \"the empty tree\", so the",
18
+ "review payload becomes the whole repository instead of the working delta. The",
19
+ "observed failure (2026-07-28) was a Codex desktop git worker taking SIGTRAP on",
20
+ "a 108,434-path / 29.2 MB payload built against a remote-tracking ref 22,881",
21
+ "commits behind local main, retried until the app reached 5.5 GB RSS.",
22
+ "#",
23
+ "Codex exposes no configuration for the review base — `review_model` only picks",
24
+ "the reviewing model, and `auto_review` governs the approval-reviewer subagent,",
25
+ "not code review. `codex review --base` is per-invocation and does not reach the",
26
+ "desktop app's automatic review. Removing the stale ref is therefore the only",
27
+ "lever available on the HQ side: with nothing stale to resolve, there is no",
28
+ "whole-tree base to pick.",
29
+ "#",
30
+ "Modes:",
31
+ " (default) human-readable report; exit 0.",
32
+ " --json machine-readable JSON array; exit 0.",
33
+ " --check exit 0 if clean, 1 if any stale base exists (advisory gate).",
34
+ " --fix delete the offending remote-tracking refs; print each action.",
35
+ "#",
36
+ "--fix deletes ONLY refs under refs/remotes/. It never touches refs/heads/,",
37
+ "never removes a configured remote, never fetches, and never pushes. The",
38
+ "deletion is recoverable: `git fetch <remote>` recreates the ref (and, on an HQ",
39
+ "root, recreates this condition — HQ roots are not meant to be fetched).",
40
+ "#",
41
+ "Thresholds (override with flags or env):",
42
+ " --max-paths N changed paths tolerated vs. a base (default 5000)",
43
+ " --max-commits N commits ahead tolerated vs. a base (default 1000)",
44
+ "A ref is stale when EITHER threshold is exceeded. Path count is the primary",
45
+ "signal: it is what determines the review payload size.",
46
+ ].map((line) => line === "#" || line.startsWith("# ") ? line : `# ${line}`).join("\n") + "\n\n";
47
+ function shellTrim(value) { return value.replace(/\n+$/, ""); }
48
+ export function findHqRootAncestor(root, existsSync, pathOps = path) {
49
+ let ancestor = pathOps.dirname(root);
50
+ while (ancestor) {
51
+ const parent = pathOps.dirname(ancestor);
52
+ if (parent === ancestor)
53
+ break;
54
+ if (existsSync(pathOps.join(ancestor, "core/core.yaml")))
55
+ return ancestor;
56
+ ancestor = parent;
57
+ }
58
+ return undefined;
59
+ }
60
+ export function runDetectStaleReviewBase(args, deps) {
61
+ const f = deps.fs ?? fs, env = deps.env ?? process.env;
62
+ const runGit = deps.runGit ?? ((argv) => {
63
+ const result = spawnSync("git", argv, { cwd: deps.cwd, env, encoding: "utf8" });
64
+ return { code: result.status ?? (result.signal ? 128 : 1), stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
65
+ });
66
+ let mode = "report", maxPaths = env.HQ_REVIEW_BASE_MAX_PATHS ?? "5000", maxCommits = env.HQ_REVIEW_BASE_MAX_COMMITS ?? "1000", override = "";
67
+ for (let index = 0; index < args.length;) {
68
+ const arg = args[index];
69
+ if (arg === "--json") {
70
+ mode = "json";
71
+ index += 1;
72
+ }
73
+ else if (arg === "--check") {
74
+ mode = "check";
75
+ index += 1;
76
+ }
77
+ else if (arg === "--fix") {
78
+ mode = "fix";
79
+ index += 1;
80
+ }
81
+ else if (arg === "--max-paths" || arg === "--max-commits" || arg === "--root") {
82
+ const value = args[index + 1];
83
+ if (value === undefined || value === "") {
84
+ const line = arg === "--max-paths" ? 54 : arg === "--max-commits" ? 55 : 56;
85
+ return { code: 1, stdout: "", stderr: `${deps.scriptPath ?? "detect-stale-review-base.sh"}: line ${line}: 2: ${arg} needs a value\n` };
86
+ }
87
+ if (arg === "--max-paths")
88
+ maxPaths = value;
89
+ else if (arg === "--max-commits")
90
+ maxCommits = value;
91
+ else
92
+ override = value;
93
+ index += 2;
94
+ }
95
+ else if (arg === "-h" || arg === "--help")
96
+ return { code: 0, stdout: DETECT_STALE_REVIEW_BASE_HELP, stderr: "" };
97
+ else
98
+ return { code: 2, stdout: "", stderr: `detect-stale-review-base: unknown argument: ${arg}\nusage: ${deps.scriptPath ?? "detect-stale-review-base.sh"} [--json|--check|--fix] [--max-paths N] [--max-commits N] [--root PATH]\n` };
99
+ }
100
+ if (!/^\d+$/.test(maxPaths))
101
+ return { code: 2, stdout: "", stderr: "detect-stale-review-base: --max-paths must be a non-negative integer\n" };
102
+ if (!/^\d+$/.test(maxCommits))
103
+ return { code: 2, stdout: "", stderr: "detect-stale-review-base: --max-commits must be a non-negative integer\n" };
104
+ const scriptDir = deps.scriptDir ?? path.join(deps.cwd, "core/scripts");
105
+ let root = override || env.HQ_ROOT || env.CLAUDE_PROJECT_DIR || path.resolve(scriptDir, "../..");
106
+ if (!f.existsSync(root))
107
+ return { code: 2, stdout: "", stderr: `detect-stale-review-base: no such directory: ${root}\n` };
108
+ try {
109
+ root = f.realpathSync(root);
110
+ }
111
+ catch {
112
+ root = path.resolve(root);
113
+ }
114
+ const emitClean = (why) => ({ code: 0, stdout: mode === "json" ? "[]\n" : mode === "check" ? "" : mode === "fix" ? `detect-stale-review-base: nothing to fix (${why})\n` : `detect-stale-review-base: clean (${why})\n`, stderr: "" });
115
+ if (!f.existsSync(path.join(root, "core/core.yaml")))
116
+ return { code: 2, stdout: "", stderr: `detect-stale-review-base: ${root} is not an HQ root (no core/core.yaml)\n` };
117
+ const ancestor = findHqRootAncestor(root, (candidate) => f.existsSync(candidate));
118
+ if (ancestor)
119
+ return { code: 2, stdout: "", stderr: `detect-stale-review-base: ${root} is nested inside the HQ root ${ancestor} — refusing. Working clones and worktree checkouts keep their remote-tracking refs; this tool only repairs a top-level HQ install.\n` };
120
+ const git = (...argv) => runGit(["-C", root, ...argv]);
121
+ const top = git("rev-parse", "--show-toplevel");
122
+ if (top.code !== 0 || shellTrim(top.stdout) !== root)
123
+ return emitClean("HQ root is not a git repository — no review base to go stale");
124
+ if (git("rev-parse", "--verify", "-q", "HEAD").code !== 0)
125
+ return emitClean("HQ root has no commits yet");
126
+ const refsResult = git("for-each-ref", "--format=%(refname)", "refs/remotes");
127
+ const refs = refsResult.code === 0 ? shellTrim(refsResult.stdout) : "";
128
+ if (!refs)
129
+ return emitClean("no remote-tracking refs in the HQ root");
130
+ const maxP = Number(maxPaths), maxC = Number(maxCommits), stale = [];
131
+ for (const ref of refs.split("\n")) {
132
+ if (!ref)
133
+ continue;
134
+ if (git("symbolic-ref", "-q", ref).code === 0)
135
+ continue;
136
+ const commitResult = git("rev-list", "--count", `${ref}..HEAD`);
137
+ const commitText = commitResult.code === 0 ? shellTrim(commitResult.stdout) : "";
138
+ if (!commitText)
139
+ continue;
140
+ const commits = Number(commitText);
141
+ let changedPaths = "";
142
+ if (commits <= maxC || mode === "json") {
143
+ const result = git("diff", "--name-only", `${ref}...HEAD`);
144
+ if (result.code !== 0)
145
+ return { code: 1, stdout: "", stderr: "" };
146
+ changedPaths = String((result.stdout.match(/\n/g) ?? []).length);
147
+ }
148
+ const paths = Number(changedPaths || 0);
149
+ if (commits <= maxC && paths <= maxP)
150
+ continue;
151
+ stale.push({ ref, commits, paths });
152
+ }
153
+ if (!stale.length)
154
+ return emitClean(`every remote-tracking ref is within ${maxP} paths / ${maxC} commits of HEAD`);
155
+ if (mode === "json")
156
+ return { code: 0, stdout: `${JSON.stringify(stale.map((row) => ({ ref: row.ref, commits_ahead: row.commits, changed_paths: row.paths })))}\n`, stderr: "" };
157
+ if (mode === "check")
158
+ return { code: 1, stdout: "", stderr: `detect-stale-review-base: ${stale.length} stale review base(s) in ${root}\n` };
159
+ if (mode === "fix") {
160
+ const lines = [];
161
+ for (const row of stale) {
162
+ const deleted = git("update-ref", "-d", row.ref);
163
+ if (deleted.code !== 0)
164
+ return { code: deleted.code, stdout: lines.join(""), stderr: deleted.stderr };
165
+ lines.push(`detect-stale-review-base: deleted ${row.ref}\n`);
166
+ }
167
+ lines.push(`detect-stale-review-base: removed ${stale.length} stale review base(s). A later \`git fetch\` in the HQ root recreates them — HQ roots are not meant to be fetched (policy hq-root-never-push-remote).\n`);
168
+ return { code: 0, stdout: lines.join(""), stderr: "" };
169
+ }
170
+ let out = `detect-stale-review-base: ${stale.length} stale review base(s) in ${root}\n\n`;
171
+ for (const row of stale) {
172
+ const commits = git("rev-list", "--count", `${row.ref}..HEAD`), diff = git("diff", "--name-only", `${row.ref}...HEAD`);
173
+ out += ` ${row.ref} — HEAD is ${shellTrim(commits.stdout) || "?"} commits ahead, ${(diff.stdout.match(/\n/g) ?? []).length} changed paths\n`;
174
+ }
175
+ out += "\nA review tool that auto-selects a base will pick one of these and build a\nwhole-tree payload from it. Remove them with:\n\n bash core/scripts/detect-stale-review-base.sh --fix\n";
176
+ return { code: 0, stdout: out, stderr: "" };
177
+ }
178
+ //# sourceMappingURL=core-detect-stale-review-base-native.js.map
@@ -0,0 +1,11 @@
1
+ /** Shared CLI registrations and fast-route adapters for Wave 8 hook helpers. */
2
+ import type { Command } from "commander";
3
+ import { runShareSuggestionState } from "./core-share-suggestion-state-native.js";
4
+ import { runRepoRunRegistry } from "./core-repo-run-registry-native.js";
5
+ import { runDetectStaleReviewBase } from "./core-detect-stale-review-base-native.js";
6
+ export { runShareSuggestionState, runRepoRunRegistry, runDetectStaleReviewBase };
7
+ export declare function registerCoreHookStateHelpers(core: Command): void;
8
+ export declare function runShareSuggestionStateFast(args: readonly string[], stdin: string, _cwd: string, hqRoot: string | undefined): number;
9
+ export declare function runRepoRunRegistryFast(args: readonly string[], _stdin: string, _cwd: string, hqRoot: string | undefined): number;
10
+ export declare function runDetectStaleReviewBaseFast(args: readonly string[], _stdin: string, _cwd: string, hqRoot: string | undefined): number;
11
+ //# sourceMappingURL=core-hook-state-helpers-native.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { resolveBundledAsset, resolveLiveRoot } from "../utils/hq-roots.js";
2
+ import { runBundledScript } from "../utils/run-bundled-script.js";
3
+ import { describeCoreCommand } from "./core-command-catalog.js";
4
+ import { coreNativeHookHelpersEnabled } from "./core-native-hook-helpers-flag.js";
5
+ import { runShareSuggestionState, SHARE_SUGGESTION_STATE_ASSET } from "./core-share-suggestion-state-native.js";
6
+ import { runRepoRunRegistry, REPO_RUN_REGISTRY_ASSET } from "./core-repo-run-registry-native.js";
7
+ import { runDetectStaleReviewBase, DETECT_STALE_REVIEW_BASE_ASSET } from "./core-detect-stale-review-base-native.js";
8
+ export { runShareSuggestionState, runRepoRunRegistry, runDetectStaleReviewBase };
9
+ const definitions = [
10
+ { name: "share-suggestion-state", asset: SHARE_SUGGESTION_STATE_ASSET, summary: "Manage hook share suggestion state", run: (args, stdin, _cwd, root) => runShareSuggestionState(args, stdin, { root, cwd: root, env: process.env, stdinIsTty: process.stdin.isTTY }) },
11
+ { name: "repo-run-registry", asset: REPO_RUN_REGISTRY_ASSET, summary: "Coordinate active runs across repositories", run: (args, _stdin, _cwd, root) => runRepoRunRegistry(args, { root, cwd: root, env: process.env, runShellFallback: (fallbackArgs) => {
12
+ const { code } = runBundledScript({ asset: REPO_RUN_REGISTRY_ASSET, args: [...fallbackArgs], cwd: root, hqRoot: root });
13
+ return { code, stdout: "", stderr: "" };
14
+ } }) },
15
+ { name: "detect-stale-review-base", asset: DETECT_STALE_REVIEW_BASE_ASSET, summary: "Find stale remote review bases in an HQ root", run: (args, _stdin, _cwd, root) => runDetectStaleReviewBase(args, { cwd: root, env: { ...process.env, HQ_ROOT: root }, scriptPath: resolveBundledAsset(DETECT_STALE_REVIEW_BASE_ASSET) }) },
16
+ ];
17
+ async function readStdin() {
18
+ if (process.stdin.isTTY)
19
+ return "";
20
+ const chunks = [];
21
+ for await (const chunk of process.stdin)
22
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
23
+ return Buffer.concat(chunks).toString("utf8");
24
+ }
25
+ function writeResult(result) {
26
+ if (result.stdout)
27
+ process.stdout.write(result.stdout);
28
+ if (result.stderr)
29
+ process.stderr.write(result.stderr);
30
+ if (result.code !== 0)
31
+ process.exitCode = result.code;
32
+ }
33
+ export function registerCoreHookStateHelpers(core) {
34
+ for (const entry of definitions) {
35
+ const command = core.command(entry.name).description(entry.summary).allowUnknownOption().allowExcessArguments().helpOption(false).argument("[args...]")
36
+ .action(async (args = [], _options, cmd) => {
37
+ const cwd = process.cwd(), hqRootArg = core.opts().hqRoot;
38
+ const root = resolveLiveRoot(hqRootArg ? { hqRoot: hqRootArg } : {});
39
+ const operands = cmd.args.length > 0 ? cmd.args : args;
40
+ if (!await coreNativeHookHelpersEnabled()) {
41
+ const { code } = runBundledScript({ asset: entry.asset, args: operands, cwd: root, hqRoot: root });
42
+ if (code !== 0)
43
+ process.exitCode = code;
44
+ return;
45
+ }
46
+ const stdin = entry.name === "share-suggestion-state" ? await readStdin() : "";
47
+ writeResult(await entry.run(operands, stdin, cwd, root));
48
+ });
49
+ describeCoreCommand(command, { execution: "native", asset: entry.asset, root: "live" });
50
+ }
51
+ }
52
+ function writeFast(result) {
53
+ if (result.stdout)
54
+ process.stdout.write(result.stdout);
55
+ if (result.stderr)
56
+ process.stderr.write(result.stderr);
57
+ return result.code;
58
+ }
59
+ export function runShareSuggestionStateFast(args, stdin, _cwd, hqRoot) {
60
+ const root = hqRoot ?? resolveLiveRoot();
61
+ return writeFast(runShareSuggestionState(args, stdin, { root, cwd: root, env: process.env, stdinIsTty: process.stdin.isTTY }));
62
+ }
63
+ export function runRepoRunRegistryFast(args, _stdin, _cwd, hqRoot) {
64
+ const root = hqRoot ?? resolveLiveRoot();
65
+ return writeFast(runRepoRunRegistry(args, { root, cwd: root, env: process.env, runShellFallback: (fallbackArgs) => {
66
+ const { code } = runBundledScript({ asset: REPO_RUN_REGISTRY_ASSET, args: [...fallbackArgs], cwd: root, hqRoot: root });
67
+ return { code, stdout: "", stderr: "" };
68
+ } }));
69
+ }
70
+ export function runDetectStaleReviewBaseFast(args, _stdin, _cwd, hqRoot) {
71
+ const root = hqRoot ?? resolveLiveRoot();
72
+ return writeFast(runDetectStaleReviewBase(args, { cwd: root, env: { ...process.env, HQ_ROOT: root }, scriptPath: resolveBundledAsset(DETECT_STALE_REVIEW_BASE_ASSET) }));
73
+ }
74
+ //# sourceMappingURL=core-hook-state-helpers-native.js.map
@@ -0,0 +1,19 @@
1
+ /** Native, dependency-light port of repo-run-registry.sh. */
2
+ import * as fs from "node:fs";
3
+ import type { HookHelperOutput } from "./core-share-suggestion-state-native.js";
4
+ export declare const REPO_RUN_REGISTRY_ASSET = "core/scripts/repo-run-registry.sh";
5
+ export type RegistryDependencies = {
6
+ root: string;
7
+ cwd: string;
8
+ env?: NodeJS.ProcessEnv;
9
+ fs?: Pick<typeof fs, "existsSync" | "statSync" | "readFileSync" | "writeFileSync" | "renameSync" | "mkdirSync" | "rmSync">;
10
+ now?: () => Date;
11
+ pid?: number;
12
+ isPidAlive?: (pid: number) => boolean;
13
+ hostname?: () => string;
14
+ sleep?: (milliseconds: number) => void;
15
+ parseDateEpoch?: (value: string) => number;
16
+ runShellFallback?: (args: readonly string[]) => HookHelperOutput;
17
+ };
18
+ export declare function runRepoRunRegistry(args: readonly string[], deps: RegistryDependencies): HookHelperOutput;
19
+ //# sourceMappingURL=core-repo-run-registry-native.d.ts.map