@indigoai-us/hq-cli 5.303.0 → 5.304.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.304.0] — 2026-10-01
6
+ - `hq lanes show` skips owner refresh only when a failed Outpost dispatch explicitly recorded `pickup_persisted: false` and no owner heartbeat; it reports `remote_refresh: "skipped_undispatched"` in that case.
7
+
5
8
  ## [5.303.0] — 2026-10-01
6
9
  - Fixed bundled shell scripts failing on Windows when Git Bash is installed but `bash` is not on `PATH`.
7
10
  - The bundled token usage script now reads Claude sessions from `CLAUDE_CONFIG_DIR/projects` and the standard Claude project directories, including nested session folders (subagent transcripts are skipped), when no project directory is given.
@@ -132,6 +132,8 @@ export declare function parseMaxRuntimeSeconds(raw: unknown, workerId: string):
132
132
  export declare const PICKUP_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "model_unsupported"];
133
133
  export declare const DISPATCH_TERMINAL_REASON: "outpost_dispatch_failed";
134
134
  export type PickupTerminalReason = (typeof PICKUP_TERMINAL_REASONS)[number];
135
+ /** Only explicit non-persistence evidence can prove a failed dispatch has no owner snapshot. */
136
+ export declare function isUndispatchedOutpostMirror(lane: LaneRecord): boolean;
135
137
  /**
136
138
  * The CAS updater a pickup failure applies. A lane that is already terminal
137
139
  * for any reason (a cancel that landed after the caller's last read, an
@@ -1284,9 +1284,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, opt
1284
1284
  ((parentLaneIds === undefined || (lane.senior.kind === "lane" &&
1285
1285
  lane.senior.id !== undefined &&
1286
1286
  parentLaneIds.has(lane.senior.id))) || options.laneIds?.has(lane.lane_id) === true) &&
1287
- !(isTerminalLaneState(lane.state) &&
1288
- lane.terminal_reason === DISPATCH_TERMINAL_REASON &&
1289
- lane.last_host_report_at === undefined) &&
1287
+ !isUndispatchedOutpostMirror(lane) &&
1290
1288
  (options.includeTerminalMirrors === true || !isTerminalLaneState(lane.state)))
1291
1289
  .sort((a, b) => {
1292
1290
  const terminalOrder = Number(isTerminalLaneState(a.state)) - Number(isTerminalLaneState(b.state));
@@ -2524,6 +2522,14 @@ export const PICKUP_TERMINAL_REASONS = [
2524
2522
  "model_unsupported",
2525
2523
  ];
2526
2524
  export const DISPATCH_TERMINAL_REASON = "outpost_dispatch_failed";
2525
+ /** Only explicit non-persistence evidence can prove a failed dispatch has no owner snapshot. */
2526
+ export function isUndispatchedOutpostMirror(lane) {
2527
+ return lane.host.kind === "outpost" &&
2528
+ isTerminalLaneState(lane.state) &&
2529
+ lane.terminal_reason === DISPATCH_TERMINAL_REASON &&
2530
+ lane.terminal_details?.pickup_persisted === false &&
2531
+ lane.last_host_report_at == null;
2532
+ }
2527
2533
  function parsePickupTerminalReason(value) {
2528
2534
  if (PICKUP_TERMINAL_REASONS.includes(value)) {
2529
2535
  return value;
@@ -3647,6 +3653,9 @@ async function dispatchCreatedOutpostLane(ctx, input) {
3647
3653
  command_id: dispatched.commandId,
3648
3654
  exit_code: dispatched.exitCode,
3649
3655
  status: dispatched.status,
3656
+ ...(remote?.pickup_persisted === false || ownerError.details.pickup_persisted === false
3657
+ ? { pickup_persisted: false }
3658
+ : {}),
3650
3659
  owner_error: ownerError,
3651
3660
  });
3652
3661
  await emitLaneTerminalStateSoft(ctx, failed);
@@ -5038,13 +5047,17 @@ async function runOutpostIdentityBackfillTarget(ctx, opts) {
5038
5047
  }
5039
5048
  async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
5040
5049
  if (lane.host.kind !== "outpost" || laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
5041
- return lane;
5050
+ return { lane };
5051
+ }
5052
+ if (isUndispatchedOutpostMirror(lane)) {
5053
+ return { lane, remoteRefresh: "skipped_undispatched" };
5042
5054
  }
5043
5055
  const outpostId = lane.host.id;
5044
5056
  const descendantIds = localLanes
5045
5057
  .filter((candidate) => candidate.lane_id !== lane.lane_id &&
5046
5058
  candidate.host.kind === "outpost" && candidate.host.id === outpostId &&
5047
5059
  walkHitsLane(ctx.hqRoot(), candidate.senior, lane.lane_id, ctx))
5060
+ .filter((candidate) => !isUndispatchedOutpostMirror(candidate))
5048
5061
  .map((candidate) => candidate.lane_id);
5049
5062
  const refresh = async (laneId) => {
5050
5063
  try {
@@ -5061,13 +5074,14 @@ async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
5061
5074
  };
5062
5075
  const refreshed = await mapWithConcurrency([lane.lane_id, ...descendantIds], OUTPOST_HIRE_SUMMARY_CONCURRENCY, refresh);
5063
5076
  const refreshedLane = refreshed[0] ?? null;
5064
- return refreshedLane ?? lane;
5077
+ return { lane: refreshedLane ?? lane };
5065
5078
  }
5066
5079
  async function runShow(ctx, laneId, hasPendingQuestionForLane) {
5067
5080
  const hqRoot = ctx.hqRoot();
5068
5081
  const initialLane = readLane(hqRoot, laneId);
5069
5082
  const initialLanes = loadAllLanes(hqRoot);
5070
- const storedLane = await refreshOutpostLaneCopiesForShow(ctx, initialLane, initialLanes);
5083
+ const showRefresh = await refreshOutpostLaneCopiesForShow(ctx, initialLane, initialLanes);
5084
+ const storedLane = showRefresh.lane;
5071
5085
  const allLanes = loadAllLanes(hqRoot);
5072
5086
  const hire = await laneHirePresentation(ctx, storedLane, allLanes);
5073
5087
  const remoteQueued = storedLane.state === "queued" &&
@@ -5152,6 +5166,7 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane) {
5152
5166
  stalled_reason: lane.stalled_reason ?? null,
5153
5167
  ask_delivery_failed: askDeliveryFailed,
5154
5168
  delivery,
5169
+ ...(showRefresh.remoteRefresh === undefined ? {} : { remote_refresh: showRefresh.remoteRefresh }),
5155
5170
  provider_session_id: lane.provider_session_id ?? null,
5156
5171
  provider_session_id_absent: lane.provider_session_id_absent ?? null,
5157
5172
  envelope_repair_mode: lane.envelope_repair?.mode ?? null,
@@ -55,7 +55,7 @@ export declare const DEFAULT_PER_CASE_TIMEOUT_MS = 10000;
55
55
  */
56
56
  export declare const SANDBOX_COPY_RELPATHS: readonly string[];
57
57
  /** One gate execution's inputs. */
58
- export interface GateRunInput {
58
+ interface GateRunInput {
59
59
  /** Absolute path to the sandboxed `hook-gate.sh`. */
60
60
  gatePath: string;
61
61
  /** Absolute path to the sandboxed hook script the gate delegates to. */
@@ -72,7 +72,7 @@ export interface GateRunInput {
72
72
  timeoutMs: number;
73
73
  }
74
74
  /** One gate execution's observed outcome. */
75
- export interface GateRunResult {
75
+ interface GateRunResult {
76
76
  /** True when the execution exceeded {@link GateRunInput.timeoutMs}. */
77
77
  timedOut: boolean;
78
78
  /** Gate exit code, or null when it did not exit normally (timeout/signal). */
@@ -81,14 +81,7 @@ export interface GateRunResult {
81
81
  stderr: string;
82
82
  }
83
83
  /** Executes one gate invocation. Injectable so the aggregation is unit-testable. */
84
- export type GateRunner = (input: GateRunInput) => GateRunResult;
85
- /**
86
- * The real runner: spawn `bash hook-gate.sh <id> <script>` with the payload on
87
- * stdin, `HQ_HOOK_PROFILE` set, and a hard timeout. A timeout is detected by the
88
- * `ETIMEDOUT` error code spawnSync reports; the process is killed with SIGKILL so
89
- * a hook cannot ignore the bound.
90
- */
91
- export declare const defaultGateRunner: GateRunner;
84
+ type GateRunner = (input: GateRunInput) => GateRunResult;
92
85
  /** Options for {@link runDeepGuardTests}. All are injectable for testing. */
93
86
  export interface DeepTestOptions {
94
87
  /** Per-case time bound. Default: {@link DEFAULT_PER_CASE_TIMEOUT_MS}. */
@@ -109,4 +102,5 @@ export declare function cleanupAllDeepSandboxes(): void;
109
102
  * an empty list — creating no sandbox — when there is nothing to run.
110
103
  */
111
104
  export declare function runDeepGuardTests(context: CheckContext, options?: DeepTestOptions): Promise<CheckResult[]>;
105
+ export {};
112
106
  //# sourceMappingURL=executor.d.ts.map
@@ -74,7 +74,7 @@ export const SANDBOX_COPY_RELPATHS = [
74
74
  * `ETIMEDOUT` error code spawnSync reports; the process is killed with SIGKILL so
75
75
  * a hook cannot ignore the bound.
76
76
  */
77
- export const defaultGateRunner = (input) => {
77
+ const defaultGateRunner = (input) => {
78
78
  const result = spawnSync("bash", [input.gatePath, input.hookId, input.hookScriptPath], {
79
79
  input: input.payload,
80
80
  cwd: input.cwd,
@@ -46,14 +46,6 @@ import { type PlatformId } from "../payload-shapes.js";
46
46
  export declare const PARITY_FAMILY_ID = "deep-parity";
47
47
  /** Human title of the parity family. */
48
48
  export declare const PARITY_FAMILY_TITLE = "Cross-platform parity replay";
49
- /** Default per-case, per-platform time bound. */
50
- export declare const DEFAULT_PARITY_TIMEOUT_MS = 10000;
51
- /** The gate profile the replay runs each hook under. Strict is the broadest
52
- * allowlist, so profile gating never suppresses the hook whose LOGIC is being
53
- * compared across platforms — profile-specific behaviour is US-008's concern. */
54
- export declare const PARITY_GATE_PROFILE = "strict";
55
- /** The session id stamped onto every replay payload. */
56
- export declare const PARITY_SESSION_ID = "hq-doctor-parity-replay";
57
49
  /** One platform replay's inputs. */
58
50
  export interface PlatformRunInput {
59
51
  /** Which platform's adapter path to exercise. */
@@ -85,27 +77,7 @@ export interface PlatformRunOutput {
85
77
  /** Executes one platform replay. Injectable so the comparison is unit-testable. */
86
78
  export type PlatformRunner = (input: PlatformRunInput) => PlatformRunOutput;
87
79
  /** The real runner set, keyed by platform. */
88
- export type PlatformRunners = Record<PlatformId, PlatformRunner>;
89
- /** The real Claude runner: the canonical script through the Claude gate. */
90
- export declare const defaultClaudeRunner: PlatformRunner;
91
- /** The real Codex runner: the duplicated mirror through the Codex gate. */
92
- export declare const defaultCodexRunner: PlatformRunner;
93
- /**
94
- * The real Grok runner. Grok shares the canonical `.claude/hooks/` scripts with
95
- * Claude — it has no mirror of its own — and its adapter's whole job is to read
96
- * the camelCase payload, map the tool alias back to canonical, build the Claude
97
- * JSON, and pipe it to the same `hook-gate.sh`. We reproduce that routing here
98
- * (via {@link toGrokPayload} + {@link grokToSnakePayload}) rather than spawn the
99
- * adapter directly, for two reasons: the live adapter runs a whole tool
100
- * *sequence* where the replay needs one isolated hook, and its user-global
101
- * bridge cannot be simulated from a project-local run (US-005 keeps bridge
102
- * presence a wiring check). The Grok block protocol still applies — the gate's
103
- * non-zero exit is what the adapter would translate into a stdout `deny`
104
- * ({@link interpretGrokBlock} in verdictFor recognises both).
105
- */
106
- export declare const defaultGrokRunner: PlatformRunner;
107
- /** The default real runner set wired to each platform's real adapter path. */
108
- export declare const defaultPlatformRunners: PlatformRunners;
80
+ type PlatformRunners = Record<PlatformId, PlatformRunner>;
109
81
  /** Options for {@link runParityReplay}. All injectable for testing. */
110
82
  export interface ParityOptions {
111
83
  /** Per-case, per-platform time bound. Default: {@link DEFAULT_PARITY_TIMEOUT_MS}. */
@@ -126,4 +98,5 @@ export declare function cleanupAllParitySandboxes(): void;
126
98
  * empty list — creating no sandbox — when there is nothing to replay.
127
99
  */
128
100
  export declare function runParityReplay(context: CheckContext, options?: ParityOptions): Promise<CheckResult[]>;
101
+ export {};
129
102
  //# sourceMappingURL=parity.d.ts.map
@@ -52,13 +52,13 @@ export const PARITY_FAMILY_ID = "deep-parity";
52
52
  /** Human title of the parity family. */
53
53
  export const PARITY_FAMILY_TITLE = "Cross-platform parity replay";
54
54
  /** Default per-case, per-platform time bound. */
55
- export const DEFAULT_PARITY_TIMEOUT_MS = 10_000;
55
+ const DEFAULT_PARITY_TIMEOUT_MS = 10_000;
56
56
  /** The gate profile the replay runs each hook under. Strict is the broadest
57
57
  * allowlist, so profile gating never suppresses the hook whose LOGIC is being
58
58
  * compared across platforms — profile-specific behaviour is US-008's concern. */
59
- export const PARITY_GATE_PROFILE = "strict";
59
+ const PARITY_GATE_PROFILE = "strict";
60
60
  /** The session id stamped onto every replay payload. */
61
- export const PARITY_SESSION_ID = "hq-doctor-parity-replay";
61
+ const PARITY_SESSION_ID = "hq-doctor-parity-replay";
62
62
  function spawnWithStdin(command, args, stdin, input) {
63
63
  const result = spawnSync(command, args, {
64
64
  input: stdin,
@@ -96,12 +96,12 @@ function runThroughGate(input, platformDir, payload) {
96
96
  return { ...spawned };
97
97
  }
98
98
  /** The real Claude runner: the canonical script through the Claude gate. */
99
- export const defaultClaudeRunner = (input) => runThroughGate(input, ".claude", JSON.stringify(toClaudePayload(input.testCase, {
99
+ const defaultClaudeRunner = (input) => runThroughGate(input, ".claude", JSON.stringify(toClaudePayload(input.testCase, {
100
100
  cwd: input.sandbox,
101
101
  sessionId: PARITY_SESSION_ID,
102
102
  })));
103
103
  /** The real Codex runner: the duplicated mirror through the Codex gate. */
104
- export const defaultCodexRunner = (input) => runThroughGate(input, ".codex", JSON.stringify(toCodexPayload(input.testCase, {
104
+ const defaultCodexRunner = (input) => runThroughGate(input, ".codex", JSON.stringify(toCodexPayload(input.testCase, {
105
105
  cwd: input.sandbox,
106
106
  sessionId: PARITY_SESSION_ID,
107
107
  })));
@@ -118,7 +118,7 @@ export const defaultCodexRunner = (input) => runThroughGate(input, ".codex", JSO
118
118
  * non-zero exit is what the adapter would translate into a stdout `deny`
119
119
  * ({@link interpretGrokBlock} in verdictFor recognises both).
120
120
  */
121
- export const defaultGrokRunner = (input) => {
121
+ const defaultGrokRunner = (input) => {
122
122
  const grokPayload = toGrokPayload(input.testCase, {
123
123
  cwd: input.sandbox,
124
124
  sessionId: PARITY_SESSION_ID,
@@ -127,7 +127,7 @@ export const defaultGrokRunner = (input) => {
127
127
  return runThroughGate(input, ".claude", JSON.stringify(snake));
128
128
  };
129
129
  /** The default real runner set wired to each platform's real adapter path. */
130
- export const defaultPlatformRunners = {
130
+ const defaultPlatformRunners = {
131
131
  claude: defaultClaudeRunner,
132
132
  codex: defaultCodexRunner,
133
133
  grok: defaultGrokRunner,
@@ -46,21 +46,10 @@ export declare const DEFAULT_PER_CASE_TIMEOUT_MS = 15000;
46
46
  * come under sandboxed execution.
47
47
  */
48
48
  export type Precondition = "git-repo" | "session-metadata" | "company-scaffold";
49
- /** Every precondition the default seeder knows how to provide. */
50
- export declare const KNOWN_PRECONDITIONS: readonly Precondition[];
51
- /**
52
- * Tree entries copied from the live tree into every sandbox — the same curated
53
- * set US-008 uses: the hook scripts and gate, the settings a hook may read, the
54
- * Codex/Grok mirrors, and `core/` (helpers hooks source). Missing entries are
55
- * skipped so a partial tree still sandboxes cleanly.
56
- */
57
- export declare const SANDBOX_SEED_RELPATHS: readonly string[];
58
49
  /** Remove every side-effect sandbox still on disk. Safe to call repeatedly. */
59
50
  export declare function cleanupAllSandboxes(): void;
60
51
  /** The sandbox roots this process is still tracking (for tests). */
61
52
  export declare function trackedSandboxRoots(): string[];
62
- /** Remove one sandbox and stop tracking it. Idempotent. */
63
- export declare function destroySandbox(root: string): void;
64
53
  /** The outcome of seeding one sandbox. */
65
54
  export interface SeedResult {
66
55
  /** Absolute, realpath'd sandbox root under the OS temp dir. */
@@ -91,7 +80,7 @@ export interface SeedOptions {
91
80
  */
92
81
  export declare function seedSandbox(hqRoot: string, opts?: SeedOptions): SeedResult;
93
82
  /** One hook execution's inputs. */
94
- export interface HookRunInput {
83
+ interface HookRunInput {
95
84
  /** Absolute path to the sandboxed hook script. */
96
85
  hookScriptPath: string;
97
86
  /** The JSON payload piped to the hook's stdin. */
@@ -104,7 +93,7 @@ export interface HookRunInput {
104
93
  timeoutMs: number;
105
94
  }
106
95
  /** One hook execution's observed result (exit code is diagnostic only). */
107
- export interface HookRunResult {
96
+ interface HookRunResult {
108
97
  /** True when the execution exceeded {@link HookRunInput.timeoutMs}. */
109
98
  timedOut: boolean;
110
99
  /** Exit code, or null when it did not exit normally (timeout/signal). */
@@ -113,17 +102,9 @@ export interface HookRunResult {
113
102
  stderr: string;
114
103
  }
115
104
  /** Runs one hook. Injectable so the tier's aggregation is unit-testable. */
116
- export type HookRunner = (input: HookRunInput) => HookRunResult;
117
- /**
118
- * The real runner: fire the hook script directly with the payload on stdin, its
119
- * cwd and session env pointed at the sandbox, and a hard timeout. Unlike a pure
120
- * guard (US-008), a side-effecting hook is run directly rather than through the
121
- * gate — its exit code is irrelevant here; what matters is the effect it leaves
122
- * on the sandbox, which effects.ts reads afterwards.
123
- */
124
- export declare const defaultHookRunner: HookRunner;
105
+ type HookRunner = (input: HookRunInput) => HookRunResult;
125
106
  /** One parsed side-effecting fixture case (its own shape, not the US-007 schema). */
126
- export interface SideEffectCase {
107
+ interface SideEffectCase {
127
108
  name: string;
128
109
  event: string;
129
110
  tool: string;
@@ -136,13 +117,13 @@ export interface SideEffectCase {
136
117
  expectedFailure?: ExpectedFailure;
137
118
  }
138
119
  /** A validated side-effecting fixture. */
139
- export interface SideEffectFixture {
120
+ interface SideEffectFixture {
140
121
  hookId: string;
141
122
  relpath: string;
142
123
  cases: SideEffectCase[];
143
124
  }
144
125
  /** A side-effecting fixture that could not be parsed. */
145
- export interface SideEffectFixtureError {
126
+ interface SideEffectFixtureError {
146
127
  hookId: string | null;
147
128
  relpath: string;
148
129
  message: string;
@@ -187,4 +168,5 @@ export interface SideEffectTestOptions {
187
168
  * Returns an empty list when there is nothing side-effecting to run.
188
169
  */
189
170
  export declare function runSideEffectTests(context: CheckContext, options?: SideEffectTestOptions): Promise<CheckResult[]>;
171
+ export {};
190
172
  //# sourceMappingURL=sandbox.d.ts.map
@@ -46,7 +46,7 @@ import { captureEffectBaseline, describeEffect, defaultGitProbe, evaluateEffects
46
46
  /** Default per-case time bound. No latency target, but no case may hang. */
47
47
  export const DEFAULT_PER_CASE_TIMEOUT_MS = 15_000;
48
48
  /** Every precondition the default seeder knows how to provide. */
49
- export const KNOWN_PRECONDITIONS = [
49
+ const KNOWN_PRECONDITIONS = [
50
50
  "git-repo",
51
51
  "session-metadata",
52
52
  "company-scaffold",
@@ -57,7 +57,7 @@ export const KNOWN_PRECONDITIONS = [
57
57
  * Codex/Grok mirrors, and `core/` (helpers hooks source). Missing entries are
58
58
  * skipped so a partial tree still sandboxes cleanly.
59
59
  */
60
- export const SANDBOX_SEED_RELPATHS = [
60
+ const SANDBOX_SEED_RELPATHS = [
61
61
  ".claude/hooks",
62
62
  ".claude/settings.json",
63
63
  ".claude/settings.local.json",
@@ -100,7 +100,7 @@ function removeTree(root) {
100
100
  }
101
101
  }
102
102
  /** Remove one sandbox and stop tracking it. Idempotent. */
103
- export function destroySandbox(root) {
103
+ function destroySandbox(root) {
104
104
  removeTree(root);
105
105
  trackedSandboxes.delete(root);
106
106
  }
@@ -198,7 +198,7 @@ function git(root, args) {
198
198
  * gate — its exit code is irrelevant here; what matters is the effect it leaves
199
199
  * on the sandbox, which effects.ts reads afterwards.
200
200
  */
201
- export const defaultHookRunner = (input) => {
201
+ const defaultHookRunner = (input) => {
202
202
  const result = spawnSync("bash", [input.hookScriptPath], {
203
203
  input: input.payload,
204
204
  cwd: input.cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.303.0",
3
+ "version": "5.304.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {