@indigoai-us/hq-cli 5.313.0 → 5.315.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,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.315.0] — 2026-10-02
6
+ - The CLI loads the Sentry error-reporting library only when it first needs it, so commands that report nothing start with fewer imports. Reporting, scrubbing and flushing work as before.
7
+ - Keep first-sync guidance enabled without its rollout gate and remove the obsolete plan-limit fallback flag-off test.
8
+
9
+ - Gate autopilot nudge deduplication behind a default-off company flag, preserving message IDs and requiring a 30-minute active-condition cooldown for repeat nudges.
10
+ - Let lane autopilot skip malformed lead inputs individually behind a default-off flag and notify the affected lead once.
11
+ - Lead autopilot can batch stale KPI reminders into one oldest-first message per pass when the company enables `lanes.autopilot-kpi-staleness-batch`.
12
+ - Preserve the last policy tag when migrating trigger policies.
13
+ - Add read-only `hq lanes keep-alive-audit` reporting for lead keep-alive timing, early resumes, and missed restarts.
14
+ - Resolve bare lane IDs to their stored lane record before messaging or showing them, preventing messages from being written under unresolved IDs.
15
+
5
16
  ## [5.313.0] — 2026-10-02
6
17
  - Gate live-only lane hire-grant counting by company; blocked and awaiting-input children no longer occupy slots when enabled, and their resume is checked against current capacity.
7
18
  - Add company-gated lane low-disk admission protection and Codex rollout retention.
@@ -256,13 +256,18 @@ trigger_expr() {
256
256
 
257
257
  # tag_tokens <raw-tags-line-without-brackets> — normalised, filtered tag tokens
258
258
  # (one per line). vendor:x -> x; meta tags dropped; lowercased.
259
+ mpt_process_tag() {
260
+ local tag="$1"
261
+ tag="$(printf '%s' "$tag" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
262
+ tag="${tag#vendor:}" # vendor:linear -> linear
263
+ [ -n "$tag" ] || return 0
264
+ case "$TAG_STOP" in *" $tag "*) return 0 ;; esac
265
+ printf '%s\n' "$tag"
266
+ }
267
+
259
268
  tag_tokens() {
260
- printf '%s' "$1" | tr ',' '\n' | while IFS= read -r tag; do
261
- tag="$(printf '%s' "$tag" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
262
- tag="${tag#vendor:}" # vendor:linear -> linear
263
- [ -n "$tag" ] || continue
264
- case "$TAG_STOP" in *" $tag "*) continue ;; esac
265
- printf '%s\n' "$tag"
269
+ printf '%s' "$1" | tr ',' '\n' | while IFS= read -r tag || [ -n "$tag" ]; do
270
+ mpt_process_tag "$tag"
266
271
  done
267
272
  }
268
273
 
@@ -329,7 +334,7 @@ for dir in "${DIRS[@]}"; do
329
334
  if policy_has_when "$f"; then skipped=$((skipped+1)); continue; fi
330
335
 
331
336
  trig="$(fm trigger "$f")"
332
- tags="$(fm tags "$f" | sed 's/^\[//; s/\]$//')"
337
+ tags="$(fm tags "$f" | sed 's/[[:space:]][[:space:]]*#.*$//; s/^\[//; s/\]$//')"
333
338
  enforcement="$(fm enforcement "$f")"
334
339
  if ! built="$(build_when "$trig" "$tags" "$enforcement")"; then
335
340
  untriggered=$((untriggered+1))
@@ -8038,6 +8038,24 @@ export declare const COMMAND_CATALOG: readonly [{
8038
8038
  readonly description: "Print the KR freshness report as JSON";
8039
8039
  }];
8040
8040
  readonly subcommands: readonly [];
8041
+ }, {
8042
+ readonly name: "keep-alive-audit";
8043
+ readonly description: "Audit lead lane keep-alive timing";
8044
+ readonly aliases: readonly [];
8045
+ readonly hidden: false;
8046
+ readonly usage: "[options]";
8047
+ readonly arguments: readonly [];
8048
+ readonly options: readonly [{
8049
+ readonly flags: "--since <iso>";
8050
+ readonly description: "Include stopped turns at or after this ISO date-time";
8051
+ }, {
8052
+ readonly flags: "--lane <id>";
8053
+ readonly description: "Audit one exact lane id";
8054
+ }, {
8055
+ readonly flags: "--json";
8056
+ readonly description: "Print the report as JSON";
8057
+ }];
8058
+ readonly subcommands: readonly [];
8041
8059
  }, {
8042
8060
  readonly name: "autopilot";
8043
8061
  readonly description: "Run lead autopilot checks and report follow-through metrics";
@@ -10346,6 +10346,29 @@ export const COMMAND_CATALOG = [
10346
10346
  ],
10347
10347
  "subcommands": []
10348
10348
  },
10349
+ {
10350
+ "name": "keep-alive-audit",
10351
+ "description": "Audit lead lane keep-alive timing",
10352
+ "aliases": [],
10353
+ "hidden": false,
10354
+ "usage": "[options]",
10355
+ "arguments": [],
10356
+ "options": [
10357
+ {
10358
+ "flags": "--since <iso>",
10359
+ "description": "Include stopped turns at or after this ISO date-time"
10360
+ },
10361
+ {
10362
+ "flags": "--lane <id>",
10363
+ "description": "Audit one exact lane id"
10364
+ },
10365
+ {
10366
+ "flags": "--json",
10367
+ "description": "Print the report as JSON"
10368
+ }
10369
+ ],
10370
+ "subcommands": []
10371
+ },
10349
10372
  {
10350
10373
  "name": "autopilot",
10351
10374
  "description": "Run lead autopilot checks and report follow-through metrics",
@@ -135,14 +135,10 @@ function deriveWhen(trigger, rawTags, enforcementRaw) {
135
135
  seen.add(token);
136
136
  terms.push(triggerExpr.includes(" ") ? `( ${triggerExpr} )` : triggerExpr);
137
137
  }
138
- const withoutBrackets = rawTags.replace(/^\[/, "").replace(/\]$/, "");
138
+ const commentStrippedTags = rawTags.replace(/[ \t\v\f\r]+#.*$/, "");
139
+ const withoutBrackets = commentStrippedTags.replace(/^\[/, "").replace(/\]$/, "");
139
140
  const rawTagParts = withoutBrackets.split(",");
140
- for (let index = 0; index < rawTagParts.length; index += 1) {
141
- // tag_tokens pipes through `while read`; Bash drops the final tag when
142
- // the source has no trailing comma/newline because that read returns EOF.
143
- if (index === rawTagParts.length - 1 && !withoutBrackets.endsWith(","))
144
- break;
145
- const raw = rawTagParts[index];
141
+ for (const raw of rawTagParts) {
146
142
  const tag = raw.toLowerCase().replace(/^\s+|\s+$/g, "").replace(/^vendor:/, "");
147
143
  if (!tag || TAG_STOP.has(tag) || seen.has(tag))
148
144
  continue;
@@ -174,7 +170,7 @@ function unreadablePolicyDiagnostic(filename, error) {
174
170
  const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : "No such file or directory";
175
171
  const script = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../assets/scaffold/core/scripts/migrate-policy-triggers.sh");
176
172
  const awk = `awk: fatal: cannot open file \`${filename}' for reading: ${message}\n`;
177
- return `${script}: line 309: ${filename}: ${message}\n${awk.repeat(3)}`;
173
+ return `${script}: line 314: ${filename}: ${message}\n${awk.repeat(3)}`;
178
174
  }
179
175
  function shellGlobMd(fileSystem, directory) {
180
176
  try {
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
3
+ export interface LaneKeepAliveAuditCommandDeps {
4
+ hqRoot: () => string;
5
+ now: () => Date;
6
+ wrap: WrapHandler;
7
+ }
8
+ export declare function registerLaneKeepAliveAuditCommand(parent: Command, deps: LaneKeepAliveAuditCommandDeps): void;
9
+ export {};
10
+ //# sourceMappingURL=lanes-keep-alive-audit.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { buildKeepAliveAudit, formatKeepAliveAuditText } from "../lib/lanes/keep-alive-audit.js";
2
+ export function registerLaneKeepAliveAuditCommand(parent, deps) {
3
+ parent
4
+ .command("keep-alive-audit")
5
+ .description("Audit lead lane keep-alive timing")
6
+ .option("--since <iso>", "Include stopped turns at or after this ISO date-time")
7
+ .option("--lane <id>", "Audit one exact lane id")
8
+ .option("--json", "Print the report as JSON")
9
+ .action(deps.wrap(async (options) => {
10
+ const result = buildKeepAliveAudit({
11
+ hqRoot: deps.hqRoot(),
12
+ now: deps.now(),
13
+ ...(options.since === undefined ? {} : { since: options.since }),
14
+ ...(options.lane === undefined ? {} : { laneId: options.lane }),
15
+ });
16
+ console.log(options.json ? JSON.stringify(result) : formatKeepAliveAuditText(result));
17
+ }));
18
+ }
19
+ //# sourceMappingURL=lanes-keep-alive-audit.js.map
@@ -204,41 +204,37 @@ export function createLaneMessageHandlers(deps) {
204
204
  }
205
205
  async function runMessage(ctx, laneId, opts) {
206
206
  const hqRoot = ctx.hqRoot();
207
- const hireGrantContext = opts.grantContext === undefined
208
- ? undefined
209
- : (() => {
210
- if (!ctx.isSsmOwnerDispatch()) {
211
- throw new LanesError("hire_grant_dispatch_unauthorized", "Message grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
212
- }
213
- return parseOutpostHireGrantContext(opts.grantContext, laneId);
214
- })();
207
+ const requestedLaneId = laneId;
215
208
  let accepted = false;
216
209
  let claimedMessages = [];
217
210
  let storySwitchRollback;
218
211
  let storySwitchDeliveryStarted = false;
212
+ const readMessageText = (targetLaneId) => {
213
+ if (opts.text !== undefined && opts.textFile !== undefined) {
214
+ throw new LanesError("message_text_source_conflict", "Pass either --text or --text-file, not both", { lane_id: targetLaneId });
215
+ }
216
+ let messageText = opts.text;
217
+ if (opts.textFile !== undefined) {
218
+ const textPath = path.isAbsolute(opts.textFile)
219
+ ? opts.textFile
220
+ : path.resolve(ctx.callerCwd(), opts.textFile);
221
+ try {
222
+ messageText = ctx.readFileSync(textPath, "utf8");
223
+ }
224
+ catch (err) {
225
+ throw new LanesError("message_text_file_unreadable", `Could not read message text file ${opts.textFile}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: targetLaneId, path: textPath });
226
+ }
227
+ }
228
+ if (messageText === undefined || messageText.length === 0) {
229
+ throw new LanesError("missing_text", "Pass non-empty message text with --text or --text-file", { lane_id: targetLaneId });
230
+ }
231
+ return messageText;
232
+ };
219
233
  const markAccepted = () => {
220
234
  accepted = true;
221
235
  storySwitchDeliveryStarted = true;
222
236
  opts.onAccepted?.();
223
237
  };
224
- if (opts.text !== undefined && opts.textFile !== undefined) {
225
- throw new LanesError("message_text_source_conflict", "Pass either --text or --text-file, not both", { lane_id: laneId });
226
- }
227
- let text = opts.text;
228
- if (opts.textFile !== undefined) {
229
- const textPath = path.isAbsolute(opts.textFile)
230
- ? opts.textFile
231
- : path.resolve(ctx.callerCwd(), opts.textFile);
232
- try {
233
- text = ctx.readFileSync(textPath, "utf8");
234
- }
235
- catch (err) {
236
- throw new LanesError("message_text_file_unreadable", `Could not read message text file ${opts.textFile}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: laneId, path: textPath });
237
- }
238
- }
239
- if (text === undefined || text.length === 0) {
240
- throw new LanesError("missing_text", "Pass non-empty message text with --text or --text-file", { lane_id: laneId });
241
- }
242
238
  let resolution;
243
239
  try {
244
240
  resolution = await deps.resolveCommandLane(ctx, laneId);
@@ -257,9 +253,20 @@ export function createLaneMessageHandlers(deps) {
257
253
  primaryReportedMissing) {
258
254
  throw err;
259
255
  }
260
- await dispatchPrimaryLaneMessage(ctx, laneId, { ...opts, text });
256
+ const text = readMessageText(requestedLaneId);
257
+ await dispatchPrimaryLaneMessage(ctx, requestedLaneId, { ...opts, text });
261
258
  return;
262
259
  }
260
+ laneId = resolution.lane.lane_id;
261
+ let text = readMessageText(laneId);
262
+ const hireGrantContext = opts.grantContext === undefined
263
+ ? undefined
264
+ : (() => {
265
+ if (!ctx.isSsmOwnerDispatch()) {
266
+ throw new LanesError("hire_grant_dispatch_unauthorized", "Message grant context is only valid on an Outpost owner dispatch", { lane_id: laneId });
267
+ }
268
+ return parseOutpostHireGrantContext(opts.grantContext, laneId);
269
+ })();
263
270
  let lane = resolution.lane;
264
271
  if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
265
272
  const fallbackAt = lane.state === "awaiting_input" ? ctx.now().toISOString() : undefined;
@@ -416,7 +423,9 @@ export function createLaneMessageHandlers(deps) {
416
423
  let automaticAccountSelection = undefined;
417
424
  if (opts.account === undefined && mechanism === "resume") {
418
425
  try {
419
- automaticAccountSelection = await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile);
426
+ automaticAccountSelection = await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, {
427
+ tolerateInvalidHostLaneLists: lane.host.kind === "local",
428
+ });
420
429
  }
421
430
  catch (err) {
422
431
  const reasons = err instanceof LanesError && err.error === "claude_account_balance_no_eligible_account" &&
@@ -194,6 +194,7 @@ export interface LaneMessageHandlerDeps {
194
194
  resolveResumeProfile(ctx: Ctx, lane: LaneRecord, workerId: string): ResolvedResumeProfile;
195
195
  selectAutomaticClaudeResumeAccount(ctx: Ctx, lane: LaneRecord, workerProfile?: ResolvedResumeProfile, options?: {
196
196
  repairUnavailableAccount?: boolean;
197
+ tolerateInvalidHostLaneLists?: boolean;
197
198
  }): Promise<{
198
199
  selection: LaneAccountSelection;
199
200
  historyReason?: AccountHistoryEntry["reason"];
@@ -23,6 +23,7 @@ export { CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, LANE_OUTPOST_LOAD_BALANCING_FLAG }
23
23
  export { resolveAnswerChoice } from "./lanes-questions.js";
24
24
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
25
25
  import { registerLaneKpisCommand } from "./lanes-kpis.js";
26
+ import { registerLaneKeepAliveAuditCommand } from "./lanes-keep-alive-audit.js";
26
27
  import { registerLaneAutopilotReportCommand } from "./lanes-autopilot.js";
27
28
  import { registerLaneChecksCommand } from "./lanes-checks.js";
28
29
  import { registerLaneReportCommands } from "./lanes-report.js";
@@ -32,7 +33,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
32
33
  import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
33
34
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
34
35
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
35
- import { readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
36
+ import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
36
37
  import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG } from "../lib/lanes/flag-keys.js";
37
38
  import { CODEX_ROLLOUT_RETENTION_FLAG, configuredCodexRolloutHomes, lowDiskGuardEnabledForLane, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
38
39
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
@@ -1549,10 +1550,10 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1549
1550
  }
1550
1551
  const separator = laneId.lastIndexOf("_");
1551
1552
  const hostSuffix = separator < 0 ? "" : laneId.slice(separator + 1);
1552
- const ownerCompany = !hostSuffix && requestedUlid !== undefined && options.allowHostlessOwnerLookup
1553
+ const ownerLookupGate = !hostSuffix && requestedUlid !== undefined && options.allowHostlessOwnerLookup
1553
1554
  ? await options.allowHostlessOwnerLookup()
1554
- : undefined;
1555
- const hostlessUlidLookup = ownerCompany !== undefined;
1555
+ : false;
1556
+ const hostlessUlidLookup = ownerLookupGate !== false;
1556
1557
  if ((!hostSuffix && !hostlessUlidLookup) ||
1557
1558
  (hostSuffix && sanitizeHostComponent(hostSuffix) !== hostSuffix)) {
1558
1559
  throw commandLaneNotFound(laneId, options.outpostHint === true);
@@ -1647,9 +1648,9 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1647
1648
  const laneCompany = hostlessUlidLookup
1648
1649
  ? resolveLaneCompany(ctx.hqRoot(), lane).company
1649
1650
  : undefined;
1650
- const companyMatches = !hostlessUlidLookup || (ownerCompany !== undefined &&
1651
- laneCompany?.uid === ownerCompany.uid &&
1652
- laneCompany.slug === ownerCompany.slug);
1651
+ const companyMatches = !hostlessUlidLookup || ownerLookupGate === true || (ownerLookupGate === "check-company" &&
1652
+ laneCompany !== undefined && laneCompany !== null &&
1653
+ await options.allowHostlessOwnerLookup(laneCompany));
1653
1654
  if (lane.host.kind === "local" && ((!hostlessUlidLookup && lane.lane_id === laneId &&
1654
1655
  sanitizeHostComponent(lane.host.id) === hostSuffix) ||
1655
1656
  (hostlessUlidLookup && laneUlidPart(lane.lane_id) === requestedUlid &&
@@ -1683,6 +1684,20 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1683
1684
  ownerHost: match.lane.host,
1684
1685
  };
1685
1686
  }
1687
+ async function resolveMessageLaneTarget(ctx, laneId) {
1688
+ return resolveCommandLaneTarget(ctx, laneId, {
1689
+ allowHostlessOwnerLookup: async (company) => {
1690
+ if (!company) {
1691
+ const localOverride = readLanesFlagLocalOverride(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, ctx.env);
1692
+ return localOverride ?? "check-company";
1693
+ }
1694
+ return readLanesRegistryFlag(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
1695
+ company,
1696
+ env: ctx.env,
1697
+ });
1698
+ },
1699
+ });
1700
+ }
1686
1701
  function laneUlidPart(laneId) {
1687
1702
  const separator = laneId.indexOf("_");
1688
1703
  if (separator !== -1 && separator !== 26)
@@ -1696,7 +1711,7 @@ function commandLaneNotFound(laneId, outpostHint, details = {}) {
1696
1711
  : "";
1697
1712
  return new LanesError("lane_not_found", `Lane ${laneId} was not found locally or on a registered Outpost with a matching host suffix${hint}`, { lane_id: laneId, ...details });
1698
1713
  }
1699
- async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1714
+ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config, options = {}) {
1700
1715
  const localHost = laneHostFromCtx(ctx);
1701
1716
  const localLanes = loadAllLanes(hqRoot);
1702
1717
  const local = buildHostCapacitySnapshot({
@@ -1782,7 +1797,8 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1782
1797
  laneListAvailable = true;
1783
1798
  }
1784
1799
  catch (err) {
1785
- if (isLanesError(err) && err.error === "outpost_lane_list_invalid")
1800
+ if (isLanesError(err) && err.error === "outpost_lane_list_invalid" &&
1801
+ options.tolerateInvalidLaneLists !== true)
1786
1802
  throw err;
1787
1803
  unavailableHosts.push({
1788
1804
  host: { kind: "outpost", id },
@@ -5293,9 +5309,9 @@ async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
5293
5309
  const refreshedLane = refreshed[0] ?? null;
5294
5310
  return { lane: refreshedLane ?? lane };
5295
5311
  }
5296
- async function runShow(ctx, laneId, hasPendingQuestionForLane) {
5312
+ async function runShow(ctx, laneId, hasPendingQuestionForLane, resolveLaneTarget) {
5297
5313
  const hqRoot = ctx.hqRoot();
5298
- const initialLane = readLane(hqRoot, laneId);
5314
+ const initialLane = (await resolveLaneTarget(ctx, laneId)).lane;
5299
5315
  const initialLanes = loadAllLanes(hqRoot);
5300
5316
  const showRefresh = await refreshOutpostLaneCopiesForShow(ctx, initialLane, initialLanes);
5301
5317
  const storedLane = showRefresh.lane;
@@ -6598,7 +6614,9 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6598
6614
  const mayRebalance = BALANCE_MOVABLE_STATES.includes(lane.state);
6599
6615
  if (!accountNeedsRepair && !mayRebalance)
6600
6616
  return undefined;
6601
- const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
6617
+ const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config, {
6618
+ tolerateInvalidLaneLists: options.tolerateInvalidHostLaneLists === true,
6619
+ });
6602
6620
  const targetHost = balanceHosts.snapshots.find((host) => host.capacity.host.kind === lane.host.kind && host.capacity.host.id === lane.host.id);
6603
6621
  const currentTargetAccount = lane.account === undefined
6604
6622
  ? undefined
@@ -8205,16 +8223,7 @@ export function registerLanesCommand(parent, deps = {}) {
8205
8223
  persistClaudeAccountSelection,
8206
8224
  printJson,
8207
8225
  recordResumeSetup,
8208
- resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId, {
8209
- allowHostlessOwnerLookup: async () => {
8210
- const company = await commandCtx.resolveCompany();
8211
- const enabled = await readLanesRegistryFlag(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
8212
- company,
8213
- env: commandCtx.env,
8214
- });
8215
- return enabled ? company : undefined;
8216
- },
8217
- }),
8226
+ resolveCommandLane: resolveMessageLaneTarget,
8218
8227
  resolveActiveQuestionCompany,
8219
8228
  hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
8220
8229
  cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
@@ -8310,6 +8319,7 @@ export function registerLanesCommand(parent, deps = {}) {
8310
8319
  }, wrap);
8311
8320
  registerOutpostPrunerCommand(lanes);
8312
8321
  registerLaneKpisCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
8322
+ registerLaneKeepAliveAuditCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
8313
8323
  registerLaneAutopilotReportCommand(lanes, {
8314
8324
  hqRoot: ctx.hqRoot,
8315
8325
  now: ctx.now,
@@ -8516,7 +8526,7 @@ export function registerLanesCommand(parent, deps = {}) {
8516
8526
  .description("Show one lane")
8517
8527
  .argument("<lane-id>", "Lane id")
8518
8528
  .action(wrap(async (laneId) => {
8519
- await runShow(ctx, laneId, questionHandlers.hasPendingQuestionForLane);
8529
+ await runShow(ctx, laneId, questionHandlers.hasPendingQuestionForLane, resolveMessageLaneTarget);
8520
8530
  }));
8521
8531
  const stories = lanes
8522
8532
  .command("story")
@@ -15,8 +15,8 @@
15
15
  * scalar metadata is dropped instead: the allowlist, not a caller version
16
16
  * match, determines what may leave the machine.
17
17
  */
18
- import * as Sentry from "@sentry/node";
19
18
  import type { CaptureContext, SeverityLevel } from "@sentry/node";
19
+ type SentryModule = typeof import("@sentry/node");
20
20
  export declare const DEFAULT_SENTRY_REPORT_TIMEOUT_MS = 750;
21
21
  export declare const MAX_SENTRY_REPORT_PAYLOAD_BYTES = 2048;
22
22
  export declare const MAX_SENTRY_REPORT_METADATA_VALUE_BYTES = 256;
@@ -76,8 +76,8 @@ export type SentryReportOptions = {
76
76
  export type SentryReportDependencies = {
77
77
  sentry: {
78
78
  captureMessage: (message: string, context?: CaptureContext | SeverityLevel) => string;
79
- addBreadcrumb: (breadcrumb: Parameters<typeof Sentry.addBreadcrumb>[0]) => void;
80
- metrics: Pick<typeof Sentry.metrics, "count">;
79
+ addBreadcrumb: (breadcrumb: Parameters<SentryModule["addBreadcrumb"]>[0]) => void;
80
+ metrics: Pick<SentryModule["metrics"], "count">;
81
81
  flush: (timeout?: number) => PromiseLike<boolean>;
82
82
  };
83
83
  stdout: Pick<NodeJS.WriteStream, "write">;
@@ -19,12 +19,18 @@ import { createHash } from "node:crypto";
19
19
  import { mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
20
20
  import { tmpdir } from "node:os";
21
21
  import { dirname, join } from "node:path";
22
- import * as Sentry from "@sentry/node";
22
+ import { createRequire } from "node:module";
23
23
  import { gte as semverGte, valid as validSemver } from "semver";
24
24
  import { CLI_VERSION } from "../../cli-version.js";
25
25
  import { beforeSend } from "../../sentry-before-send.js";
26
26
  import { resolveLiveRoot } from "../../utils/hq-roots.js";
27
27
  import { hookTimeoutDebugContextEnabled } from "./hook-timeout-debug-context-flag.js";
28
+ const requireSentry = createRequire(import.meta.url);
29
+ let sentryModule;
30
+ function loadSentryModule() {
31
+ sentryModule ??= requireSentry("@sentry/node");
32
+ return sentryModule;
33
+ }
28
34
  export const DEFAULT_SENTRY_REPORT_TIMEOUT_MS = 750;
29
35
  const MAX_SENTRY_REPORT_TIMEOUT_MS = 5_000;
30
36
  export const MAX_SENTRY_REPORT_PAYLOAD_BYTES = 2_048;
@@ -176,7 +182,22 @@ const SENTRY_REPORT_METADATA_KEY_HEADROOM = 2;
176
182
  // while unknown keys are deliberately dropped.
177
183
  export const MAX_SENTRY_REPORT_METADATA_KEYS = ALLOWED_SENTRY_REPORT_METADATA_KEYS.length + SENTRY_REPORT_METADATA_KEY_HEADROOM;
178
184
  const defaultDependencies = {
179
- sentry: Sentry,
185
+ sentry: {
186
+ captureMessage(message, context) {
187
+ return loadSentryModule().captureMessage(message, context);
188
+ },
189
+ addBreadcrumb(breadcrumb) {
190
+ loadSentryModule().addBreadcrumb(breadcrumb);
191
+ },
192
+ metrics: {
193
+ count(name, value, options) {
194
+ loadSentryModule().metrics.count(name, value, options);
195
+ },
196
+ },
197
+ flush(timeout) {
198
+ return loadSentryModule().flush(timeout);
199
+ },
200
+ },
180
201
  stdout: process.stdout,
181
202
  stderr: process.stderr,
182
203
  timeoutDebugContextEnabled: (signal) => hookTimeoutDebugContextEnabled({}, signal),
@@ -3,13 +3,11 @@ export declare const FIRST_SYNC_NEXT_STEP_COPY = "Save a file in your HQ folder,
3
3
  export declare const FIRST_SYNC_CONFIRMATION_COPY = "Your first work sync is complete.";
4
4
  export type FirstSyncStepOutcome = "shown" | "followed";
5
5
  export declare function shouldShowFirstSyncNextStep(input: {
6
- flagEnabled: boolean;
7
6
  interactive: boolean;
8
7
  alreadyShown: boolean;
9
8
  alreadyCompleted: boolean;
10
9
  }): boolean;
11
10
  export declare function shouldConfirmFirstWorkBearingSync(input: {
12
- flagEnabled: boolean;
13
11
  interactive: boolean;
14
12
  alreadyCompleted: boolean;
15
13
  workBearing: boolean;
@@ -27,7 +25,6 @@ export declare function firstSyncStepMarkerPaths(identity: string, home?: string
27
25
  };
28
26
  export declare function claimFirstSyncStepMarker(kind: "shown" | "completed", identity: string, home?: string): boolean;
29
27
  export declare function firstSyncStepTelemetryProperties(outcome: FirstSyncStepOutcome): Record<string, unknown>;
30
- export declare function isFirstSyncNextStepEnabled(): boolean;
31
28
  /** Called once after the first completed CLI action, before process finalization. */
32
29
  export declare function maybeShowFirstSyncNextStep(): boolean;
33
30
  /** Capture the first positive CLI sync result without changing transfer logic. */
@@ -2,7 +2,6 @@ import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
- import { resolveProcessFlagGate } from "./flag-registry.js";
6
5
  import { cognitoSubjectKey, isMachineIdentity, loadCachedTokens, } from "../utils/cognito-session.js";
7
6
  export const CLI_FIRST_SYNC_NEXT_STEP_FLAG = "cli.first-sync-next-step";
8
7
  export const FIRST_SYNC_NEXT_STEP_COPY = "Save a file in your HQ folder, then run `hq sync push`.";
@@ -17,14 +16,12 @@ const AUTOMATION_ENV_MARKERS = [
17
16
  ];
18
17
  const pendingOutcomes = [];
19
18
  export function shouldShowFirstSyncNextStep(input) {
20
- return (input.flagEnabled &&
21
- input.interactive &&
19
+ return (input.interactive &&
22
20
  !input.alreadyShown &&
23
21
  !input.alreadyCompleted);
24
22
  }
25
23
  export function shouldConfirmFirstWorkBearingSync(input) {
26
- return (input.flagEnabled &&
27
- input.interactive &&
24
+ return (input.interactive &&
28
25
  !input.alreadyCompleted &&
29
26
  input.workBearing);
30
27
  }
@@ -111,9 +108,6 @@ export function firstSyncStepTelemetryProperties(outcome) {
111
108
  flagKey: CLI_FIRST_SYNC_NEXT_STEP_FLAG,
112
109
  };
113
110
  }
114
- export function isFirstSyncNextStepEnabled() {
115
- return resolveProcessFlagGate(CLI_FIRST_SYNC_NEXT_STEP_FLAG, { fallback: false }, () => false);
116
- }
117
111
  function currentPersonIdentity() {
118
112
  return cognitoSubjectKey(loadCachedTokens());
119
113
  }
@@ -131,15 +125,13 @@ function queueOutcome(outcome) {
131
125
  }
132
126
  /** Called once after the first completed CLI action, before process finalization. */
133
127
  export function maybeShowFirstSyncNextStep() {
134
- // Keep the cheap eligibility/gate checks ahead of identity and filesystem
135
- // reads; neither an automated invocation nor a flag-off session needs state.
136
- if (!isCurrentSessionEligible() || !isFirstSyncNextStepEnabled())
128
+ // Keep the cheap eligibility check ahead of identity and filesystem reads.
129
+ if (!isCurrentSessionEligible())
137
130
  return false;
138
131
  const identity = currentPersonIdentity();
139
132
  if (!identity)
140
133
  return false;
141
134
  const shouldShow = shouldShowFirstSyncNextStep({
142
- flagEnabled: true,
143
135
  interactive: true,
144
136
  alreadyShown: firstSyncStepMarkerExists("shown", identity),
145
137
  alreadyCompleted: firstSyncStepMarkerExists("completed", identity),
@@ -165,13 +157,11 @@ export function recordFirstWorkBearingSync(result) {
165
157
  const alreadyCompleted = firstSyncStepMarkerExists("completed", identity);
166
158
  if (alreadyCompleted || !claimFirstSyncStepMarker("completed", identity))
167
159
  return false;
168
- // Persist completion even while the rollout is off so enabling the flag later
169
- // cannot prompt a person who already completed a work-bearing sync. JSON and
170
- // other non-interactive human syncs count too; only the message is TTY-gated.
160
+ // Persist completion for non-interactive human syncs too; only the message is
161
+ // TTY-gated.
171
162
  if (!isCurrentSessionEligible())
172
163
  return false;
173
164
  if (!shouldConfirmFirstWorkBearingSync({
174
- flagEnabled: isFirstSyncNextStepEnabled(),
175
165
  interactive: true,
176
166
  alreadyCompleted: false,
177
167
  workBearing: true,
@@ -5,10 +5,16 @@ export declare const LEAD_AUTOPILOT_DEFAULT_ENABLED = false;
5
5
  export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
6
6
  export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DEFAULT_ENABLED = false;
7
7
  export declare const LEAD_AUTOPILOT_MISSED_DEADLINE_ESCALATION_DELAY_MS: number;
8
+ export declare const LEAD_AUTOPILOT_NUDGE_DEDUPE_FLAG = "lanes.autopilot-nudge-dedupe";
9
+ export declare const LEAD_AUTOPILOT_NUDGE_DEDUPE_DEFAULT_ENABLED = false;
10
+ export declare const LEAD_AUTOPILOT_NUDGE_REDELIVERY_DELAY_MS: number;
11
+ export declare const LEAD_AUTOPILOT_NUDGE_STATE_RETENTION_MS: number;
8
12
  export declare const LEAD_AUTOPILOT_REVIEW_DELAY_MS: number;
9
13
  export declare const LEAD_AUTOPILOT_FILL_DELAY_MS: number;
10
14
  export declare const LEAD_AUTOPILOT_PROMPT_LOOP = "personal/knowledge/team-lead-operating-loop.md";
11
15
  export declare const LEAD_AUTOPILOT_KPI_STALE_AFTER_MINUTES = 55;
16
+ export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG = "lanes.autopilot-per-lead-errors";
17
+ export declare const LEAD_AUTOPILOT_PER_LEAD_ERRORS_DEFAULT_ENABLED = false;
12
18
  export interface RegisteredLead {
13
19
  team: string;
14
20
  lane: string;
@@ -74,6 +80,11 @@ export interface AutopilotPassResult {
74
80
  skipped: Array<{
75
81
  lead_lane: string;
76
82
  reason: string;
83
+ } | {
84
+ lane_id: string;
85
+ error: string;
86
+ path: string;
87
+ message: string;
77
88
  }>;
78
89
  kpi_status?: AutopilotKpiStatus[];
79
90
  /** Set on an Outpost that has not received a lead registry from the primary yet. */
@@ -97,6 +108,7 @@ export declare function planLeadAutopilotWakes(input: {
97
108
  managerStatus?: Record<string, unknown>;
98
109
  keepAliveResumed?: boolean;
99
110
  kpiStalenessEnabled?: boolean;
111
+ kpiStalenessBatchEnabled?: boolean;
100
112
  }): AutopilotWake[];
101
113
  export declare function runLeadAutopilotPass(input: {
102
114
  hqRoot: string;
@@ -106,6 +118,7 @@ export declare function runLeadAutopilotPass(input: {
106
118
  now: Date;
107
119
  dryRun?: boolean;
108
120
  readFlag?: typeof readLanesRegistryFlag;
121
+ readNudgeDedupeFlag?: typeof readLanesRegistryFlag;
109
122
  readDeadlineFlag?: typeof readLanesRegistryFlag;
110
123
  keepAliveResumedLeads?: ReadonlySet<string>;
111
124
  missedDeadlineEscalationDelayMs?: number;