@indigoai-us/hq-cli 5.318.0 → 5.319.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,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.319.0] — 2026-10-02
6
+ - Let `hq access` use ACL-checked company presigned reads when STS vend rejects a truncated write-policy scope.
7
+ - Lead autopilot on an Outpost can wake leads that were created on that Outpost, behind the default-off `lanes.autopilot-outpost-native-leads` flag. Before, those leads were skipped as a host mismatch and got no wakes.
8
+ - Add a default-off fix for repo-run-registry worktree-scoped owner matching.
9
+ - Correct keep-alive audit verdicts for pending-work wakes and lanes that were not opted in.
10
+ - Detect and safely redeliver messages stranded in bare-ULID lane dropboxes.
11
+
5
12
  ## [5.318.0] — 2026-10-02
6
13
  - `hq lanes autopilot run` now marks missing lead registry entries as stale and includes a command to remove each stale row.
7
14
  - Keep retired policies retired regardless of YAML field order in the session helper.
@@ -330,12 +330,32 @@ _cmd_owner_of() {
330
330
  [[ -z "$repo" ]] && { echo ""; return; }
331
331
  _ensure_reg
332
332
  _prune_stale_locked
333
- jq --arg repo "$repo" --arg target "$(_abs_path "$path")" \
334
- '[.runs[] | select(
335
- (.scope == "repo" and .repo_path == $repo)
336
- or (.scope | startswith("worktree:")) and
337
- ($target | startswith(.scope | sub("^worktree:"; "")))
338
- )]' "$REG_FILE"
333
+ # hq core sets this bridge from the cached hq-flags value; absent means off.
334
+ if [[ "${HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX:-false}" == "true" ]]; then
335
+ jq --arg repo "$repo" --arg target "$(_abs_path "$path")" \
336
+ 'def worktree_root($scope):
337
+ ($scope | sub("^worktree:"; "")) as $raw
338
+ | ($raw | sub("/+$"; "")) as $trimmed
339
+ | if $trimmed == "" and ($raw | startswith("/")) then "/" else $trimmed end;
340
+ def worktree_covers($target; $scope):
341
+ worktree_root($scope) as $root
342
+ | if $root == "/" then ($target | startswith("/"))
343
+ elif $root == "" then false
344
+ else ($target == $root or ($target | startswith($root + "/")))
345
+ end;
346
+ [.runs[] as $entry | select(
347
+ ($entry.scope == "repo" and $entry.repo_path == $repo)
348
+ or (($entry.scope | startswith("worktree:")) and
349
+ worktree_covers($target; $entry.scope))
350
+ ) | $entry]' "$REG_FILE"
351
+ else
352
+ jq --arg repo "$repo" --arg target "$(_abs_path "$path")" \
353
+ '[.runs[] | select(
354
+ (.scope == "repo" and .repo_path == $repo)
355
+ or (.scope | startswith("worktree:")) and
356
+ ($target | startswith(.scope | sub("^worktree:"; "")))
357
+ )]' "$REG_FILE"
358
+ fi
339
359
  }
340
360
 
341
361
  # Check if a target path is owned by a foreign run.
@@ -363,17 +383,41 @@ _cmd_check() {
363
383
 
364
384
  # Collect foreign entries covering this target
365
385
  local matches
366
- matches=$(jq -c --arg repo "$repo" \
367
- --arg target "$abs_target" \
368
- --arg mypid "${my_pid:-0}" \
369
- --arg mysid "${my_session:-__none__}" \
370
- '[.runs[] | select(
371
- ((.scope == "repo" and .repo_path == $repo)
372
- or ((.scope | startswith("worktree:")) and
373
- ($target | startswith(.scope | sub("^worktree:"; "")))))
374
- and ((.pid | tostring) != $mypid)
375
- and (.session_id != $mysid)
376
- )]' "$REG_FILE" 2>/dev/null || echo '[]')
386
+ if [[ "${HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX:-false}" == "true" ]]; then
387
+ matches=$(jq -c --arg repo "$repo" \
388
+ --arg target "$abs_target" \
389
+ --arg mypid "${my_pid:-0}" \
390
+ --arg mysid "${my_session:-__none__}" \
391
+ 'def worktree_root($scope):
392
+ ($scope | sub("^worktree:"; "")) as $raw
393
+ | ($raw | sub("/+$"; "")) as $trimmed
394
+ | if $trimmed == "" and ($raw | startswith("/")) then "/" else $trimmed end;
395
+ def worktree_covers($target; $scope):
396
+ worktree_root($scope) as $root
397
+ | if $root == "/" then ($target | startswith("/"))
398
+ elif $root == "" then false
399
+ else ($target == $root or ($target | startswith($root + "/")))
400
+ end;
401
+ [.runs[] as $entry | select(
402
+ (($entry.scope == "repo" and $entry.repo_path == $repo)
403
+ or (($entry.scope | startswith("worktree:")) and
404
+ worktree_covers($target; $entry.scope)))
405
+ and (($entry.pid | tostring) != $mypid)
406
+ and ($entry.session_id != $mysid)
407
+ ) | $entry]' "$REG_FILE" 2>/dev/null || echo '[]')
408
+ else
409
+ matches=$(jq -c --arg repo "$repo" \
410
+ --arg target "$abs_target" \
411
+ --arg mypid "${my_pid:-0}" \
412
+ --arg mysid "${my_session:-__none__}" \
413
+ '[.runs[] | select(
414
+ ((.scope == "repo" and .repo_path == $repo)
415
+ or ((.scope | startswith("worktree:")) and
416
+ ($target | startswith(.scope | sub("^worktree:"; "")))))
417
+ and ((.pid | tostring) != $mypid)
418
+ and (.session_id != $mysid)
419
+ )]' "$REG_FILE" 2>/dev/null || echo '[]')
420
+ fi
377
421
 
378
422
  local n
379
423
  n=$(echo "$matches" | jq 'length')
@@ -7841,6 +7841,28 @@ export declare const COMMAND_CATALOG: readonly [{
7841
7841
  readonly description: "Print a human-readable table";
7842
7842
  }];
7843
7843
  readonly subcommands: readonly [];
7844
+ }, {
7845
+ readonly name: "repair-dropbox";
7846
+ readonly description: "Inspect or redeliver messages stranded in a bare-ULID lane dropbox";
7847
+ readonly aliases: readonly [];
7848
+ readonly hidden: false;
7849
+ readonly usage: "[options] <ulid>";
7850
+ readonly arguments: readonly [{
7851
+ readonly name: "ulid";
7852
+ readonly required: true;
7853
+ readonly variadic: false;
7854
+ }];
7855
+ readonly options: readonly [{
7856
+ readonly flags: "--company <slug>";
7857
+ readonly description: "Select the company by slug";
7858
+ }, {
7859
+ readonly flags: "--apply";
7860
+ readonly description: "Append marked messages to the canonical lane inbox and archive the orphan folder";
7861
+ }, {
7862
+ readonly flags: "--json";
7863
+ readonly description: "JSON on stdout (always)";
7864
+ }];
7865
+ readonly subcommands: readonly [];
7844
7866
  }, {
7845
7867
  readonly name: "report";
7846
7868
  readonly description: "Generate the owner report for tracker teams";
@@ -10087,6 +10087,35 @@ export const COMMAND_CATALOG = [
10087
10087
  ],
10088
10088
  "subcommands": []
10089
10089
  },
10090
+ {
10091
+ "name": "repair-dropbox",
10092
+ "description": "Inspect or redeliver messages stranded in a bare-ULID lane dropbox",
10093
+ "aliases": [],
10094
+ "hidden": false,
10095
+ "usage": "[options] <ulid>",
10096
+ "arguments": [
10097
+ {
10098
+ "name": "ulid",
10099
+ "required": true,
10100
+ "variadic": false
10101
+ }
10102
+ ],
10103
+ "options": [
10104
+ {
10105
+ "flags": "--company <slug>",
10106
+ "description": "Select the company by slug"
10107
+ },
10108
+ {
10109
+ "flags": "--apply",
10110
+ "description": "Append marked messages to the canonical lane inbox and archive the orphan folder"
10111
+ },
10112
+ {
10113
+ "flags": "--json",
10114
+ "description": "JSON on stdout (always)"
10115
+ }
10116
+ ],
10117
+ "subcommands": []
10118
+ },
10090
10119
  {
10091
10120
  "name": "report",
10092
10121
  "description": "Generate the owner report for tracker teams",
@@ -221,10 +221,20 @@ export async function runAccess(input) {
221
221
  }
222
222
  catch (err) {
223
223
  const status = httpStatusOf(err);
224
+ const vendError = typeof err === "object" && err !== null
225
+ ? err
226
+ : undefined;
224
227
  if (status === 403) {
225
228
  vendDenied = true;
226
229
  steps.push("vend: 403 (no-access)");
227
230
  }
231
+ else if ((status === 422 || vendError?.statusCode === 422) &&
232
+ vendError?.code === "POLICY_WRITE_SCOPE_TRUNCATED") {
233
+ // This failure means the company-wide STS write policy could not fit.
234
+ // Company reads use the separate ACL-filtered presign API, which remains
235
+ // the authority for listing and materializing this particular object.
236
+ steps.push("vend: write policy truncated; using presigned read");
237
+ }
228
238
  else if (status === 404) {
229
239
  steps.push("vend: 404");
230
240
  }
@@ -6,6 +6,6 @@ import { runDetectStaleReviewBase } from "./core-detect-stale-review-base-native
6
6
  export { runShareSuggestionState, runRepoRunRegistry, runDetectStaleReviewBase };
7
7
  export declare function registerCoreHookStateHelpers(core: Command): void;
8
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;
9
+ export declare function runRepoRunRegistryFast(args: readonly string[], _stdin: string, _cwd: string, hqRoot: string | undefined, _commandName: string, _hqRootArg?: string, worktreeScopeFixEnabled?: boolean): number;
10
10
  export declare function runDetectStaleReviewBaseFast(args: readonly string[], _stdin: string, _cwd: string, hqRoot: string | undefined): number;
11
11
  //# sourceMappingURL=core-hook-state-helpers-native.d.ts.map
@@ -2,16 +2,28 @@ import { resolveBundledAsset, resolveLiveRoot } from "../utils/hq-roots.js";
2
2
  import { runBundledScript } from "../utils/run-bundled-script.js";
3
3
  import { describeCoreCommand } from "./core-command-catalog.js";
4
4
  import { coreNativeHookHelpersEnabled } from "./core-native-hook-helpers-flag.js";
5
+ import { coreRrrWorktreeScopeFixEnabled } from "./core-rrr-worktree-scope-fix-flag.js";
5
6
  import { runShareSuggestionState, SHARE_SUGGESTION_STATE_ASSET } from "./core-share-suggestion-state-native.js";
6
7
  import { runRepoRunRegistry, REPO_RUN_REGISTRY_ASSET } from "./core-repo-run-registry-native.js";
7
8
  import { runDetectStaleReviewBase, DETECT_STALE_REVIEW_BASE_ASSET } from "./core-detect-stale-review-base-native.js";
8
9
  export { runShareSuggestionState, runRepoRunRegistry, runDetectStaleReviewBase };
9
10
  const definitions = [
10
11
  { 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
+ {
13
+ name: "repo-run-registry",
14
+ asset: REPO_RUN_REGISTRY_ASSET,
15
+ summary: "Coordinate active runs across repositories",
16
+ run: (args, _stdin, _cwd, root, worktreeScopeFixEnabled) => runRepoRunRegistry(args, {
17
+ root,
18
+ cwd: root,
19
+ env: process.env,
20
+ worktreeScopeFixEnabled,
21
+ runShellFallback: (fallbackArgs) => {
12
22
  const { code } = runBundledScript({ asset: REPO_RUN_REGISTRY_ASSET, args: [...fallbackArgs], cwd: root, hqRoot: root });
13
23
  return { code, stdout: "", stderr: "" };
14
- } }) },
24
+ },
25
+ }),
26
+ },
15
27
  { 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
28
  ];
17
29
  async function readStdin() {
@@ -37,14 +49,25 @@ export function registerCoreHookStateHelpers(core) {
37
49
  const cwd = process.cwd(), hqRootArg = core.opts().hqRoot;
38
50
  const root = resolveLiveRoot(hqRootArg ? { hqRoot: hqRootArg } : {});
39
51
  const operands = cmd.args.length > 0 ? cmd.args : args;
52
+ const worktreeScopeFixEnabled = entry.name === "repo-run-registry"
53
+ ? await coreRrrWorktreeScopeFixEnabled()
54
+ : undefined;
40
55
  if (!await coreNativeHookHelpersEnabled()) {
41
- const { code } = runBundledScript({ asset: entry.asset, args: operands, cwd: root, hqRoot: root });
56
+ const { code } = runBundledScript({
57
+ asset: entry.asset,
58
+ args: operands,
59
+ cwd: root,
60
+ hqRoot: root,
61
+ ...(entry.name === "repo-run-registry"
62
+ ? { env: { HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX: String(worktreeScopeFixEnabled) } }
63
+ : {}),
64
+ });
42
65
  if (code !== 0)
43
66
  process.exitCode = code;
44
67
  return;
45
68
  }
46
69
  const stdin = entry.name === "share-suggestion-state" ? await readStdin() : "";
47
- writeResult(await entry.run(operands, stdin, cwd, root));
70
+ writeResult(await entry.run(operands, stdin, cwd, root, worktreeScopeFixEnabled));
48
71
  });
49
72
  describeCoreCommand(command, { execution: "native", asset: entry.asset, root: "live" });
50
73
  }
@@ -60,12 +83,18 @@ export function runShareSuggestionStateFast(args, stdin, _cwd, hqRoot) {
60
83
  const root = hqRoot ?? resolveLiveRoot();
61
84
  return writeFast(runShareSuggestionState(args, stdin, { root, cwd: root, env: process.env, stdinIsTty: process.stdin.isTTY }));
62
85
  }
63
- export function runRepoRunRegistryFast(args, _stdin, _cwd, hqRoot) {
86
+ export function runRepoRunRegistryFast(args, _stdin, _cwd, hqRoot, _commandName, _hqRootArg, worktreeScopeFixEnabled = false) {
64
87
  const root = hqRoot ?? resolveLiveRoot();
65
- return writeFast(runRepoRunRegistry(args, { root, cwd: root, env: process.env, runShellFallback: (fallbackArgs) => {
88
+ return writeFast(runRepoRunRegistry(args, {
89
+ root,
90
+ cwd: root,
91
+ env: process.env,
92
+ worktreeScopeFixEnabled,
93
+ runShellFallback: (fallbackArgs) => {
66
94
  const { code } = runBundledScript({ asset: REPO_RUN_REGISTRY_ASSET, args: [...fallbackArgs], cwd: root, hqRoot: root });
67
95
  return { code, stdout: "", stderr: "" };
68
- } }));
96
+ },
97
+ }));
69
98
  }
70
99
  export function runDetectStaleReviewBaseFast(args, _stdin, _cwd, hqRoot) {
71
100
  const root = hqRoot ?? resolveLiveRoot();
@@ -13,6 +13,7 @@ export type RegistryDependencies = {
13
13
  hostname?: () => string;
14
14
  sleep?: (milliseconds: number) => void;
15
15
  parseDateEpoch?: (value: string) => number;
16
+ worktreeScopeFixEnabled?: boolean;
16
17
  runShellFallback?: (args: readonly string[]) => HookHelperOutput;
17
18
  };
18
19
  export declare function runRepoRunRegistry(args: readonly string[], deps: RegistryDependencies): HookHelperOutput;
@@ -40,6 +40,14 @@ function pathAbs(value, cwd, f) {
40
40
  return value;
41
41
  }
42
42
  }
43
+ function worktreeScopeCovers(target, scope) {
44
+ const rawRoot = scope.slice("worktree:".length);
45
+ const trimmedRoot = rawRoot.replace(/\/+$/, "");
46
+ const root = trimmedRoot === "" && rawRoot.startsWith("/") ? "/" : trimmedRoot;
47
+ if (root === "/")
48
+ return target.startsWith("/");
49
+ return root !== "" && (target === root || target.startsWith(`${root}/`));
50
+ }
43
51
  function parseStaleMinutes(root, f) {
44
52
  const personal = path.join(root, "personal/settings/orchestrator.yaml");
45
53
  const core = path.join(root, "core/settings/orchestrator.yaml");
@@ -215,7 +223,7 @@ export function runRepoRunRegistry(args, deps) {
215
223
  const state = JSON.parse(f.readFileSync(regFile, "utf8"));
216
224
  if (!state || typeof state !== "object" || !Array.isArray(state.runs))
217
225
  return deps.runShellFallback(args);
218
- if ((sub === "owner-of" || sub === "check") && state.runs.some((run) => typeof run?.scope === "string" && run.scope.startsWith("worktree:"))) {
226
+ if (!deps.worktreeScopeFixEnabled && (sub === "owner-of" || sub === "check") && state.runs.some((run) => typeof run?.scope === "string" && run.scope.startsWith("worktree:"))) {
219
227
  return deps.runShellFallback(args);
220
228
  }
221
229
  }
@@ -338,7 +346,7 @@ export function runRepoRunRegistry(args, deps) {
338
346
  return output();
339
347
  const absoluteTarget = pathAbs(target, deps.cwd, f), myPid = valueAfter(rest, "--pid") || "0", mySession = valueAfter(rest, "--session-id") || "__none__";
340
348
  const runs = Array.isArray(state.runs) ? state.runs : [];
341
- const covers = (run) => (run?.scope === "repo" && run?.repo_path === repo) || (typeof run?.scope === "string" && run.scope.startsWith("worktree:") && absoluteTarget.startsWith(run.scope.slice("worktree:".length)));
349
+ const covers = (run) => (run?.scope === "repo" && run?.repo_path === repo) || (typeof run?.scope === "string" && run.scope.startsWith("worktree:") && worktreeScopeCovers(absoluteTarget, run.scope));
342
350
  if (sub === "owner-of")
343
351
  return jsonOutput(0, runs.filter(covers));
344
352
  const owners = runs.filter((run) => covers(run) && String(run?.pid) !== myPid && run?.session_id !== mySession);
@@ -0,0 +1,4 @@
1
+ /** Default-off rollout gate for repo-run-registry worktree-scope matching. */
2
+ export declare const CORE_RRR_WORKTREE_SCOPE_FIX_FLAG = "core-rrr-worktree-scope-fix";
3
+ export declare function coreRrrWorktreeScopeFixEnabled(): Promise<boolean>;
4
+ //# sourceMappingURL=core-rrr-worktree-scope-fix-flag.d.ts.map
@@ -0,0 +1,7 @@
1
+ /** Default-off rollout gate for repo-run-registry worktree-scope matching. */
2
+ export const CORE_RRR_WORKTREE_SCOPE_FIX_FLAG = "core-rrr-worktree-scope-fix";
3
+ export async function coreRrrWorktreeScopeFixEnabled() {
4
+ const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
5
+ return resolveProcessFlagGate(CORE_RRR_WORKTREE_SCOPE_FIX_FLAG, { fallback: false }, () => false);
6
+ }
7
+ //# sourceMappingURL=core-rrr-worktree-scope-fix-flag.js.map
@@ -12,6 +12,7 @@ import { DEFAULT_ORPHAN_USER_IDLE_HOURS, findOrphanLanes, parseOrphanMinAge, par
12
12
  import { LANES_RUNS_REL } from "../lib/lanes/spawn.js";
13
13
  import { assertNoSecretText } from "../lib/lanes/text-scan.js";
14
14
  import { checkCompanyPath } from "../lib/lanes/checked-company-path.js";
15
+ import { findOrphanedBareIdDropboxes } from "../lib/lanes/orphaned-dropbox.js";
15
16
  export function mappingIncludesStory(mapping, storyId) {
16
17
  return (mapping.storyPrefix === undefined || storyId.startsWith(mapping.storyPrefix))
17
18
  && (mapping.excludePrefix === undefined || !storyId.startsWith(mapping.excludePrefix));
@@ -810,6 +811,17 @@ async function checkReport(root, company, projects, directiveProjects, projectFi
810
811
  host: finding.host,
811
812
  })),
812
813
  });
814
+ const orphanedBareIdDropboxes = findOrphanedBareIdDropboxes(root, company);
815
+ checks.push({
816
+ id: "orphaned_bare_id_dropbox",
817
+ result: orphanedBareIdDropboxes.length === 0 ? "ok" : "failing",
818
+ items: orphanedBareIdDropboxes.map((finding) => ({
819
+ id: finding.ulid,
820
+ reason: "orphaned_bare_id_dropbox",
821
+ message_count: finding.message_count,
822
+ newest_message_at: finding.newest_message_at,
823
+ })),
824
+ });
813
825
  return {
814
826
  ok: true,
815
827
  company: company.slug,
@@ -0,0 +1,14 @@
1
+ import type { Command } from "commander";
2
+ import type { LaneCompanyRef, LaneRecord } from "../lib/lanes/types.js";
3
+ type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
4
+ export interface LaneDropboxRepairDeps {
5
+ hqRoot: () => string;
6
+ now: () => Date;
7
+ resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
8
+ repairEnabled: (lane: LaneRecord) => Promise<boolean>;
9
+ wrap: WrapHandler;
10
+ }
11
+ /** Register the explicit recovery command. Dry-run is read-only; mutation is separately gated. */
12
+ export declare function registerLaneDropboxRepairCommand(parent: Command, deps: LaneDropboxRepairDeps): void;
13
+ export {};
14
+ //# sourceMappingURL=lanes-dropbox-repair.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { LanesError } from "../lib/lanes/errors.js";
2
+ import { findOrphanedBareIdDropboxes, repairOrphanedBareIdDropbox } from "../lib/lanes/orphaned-dropbox.js";
3
+ import { readLane } from "../lib/lanes/store.js";
4
+ /** Register the explicit recovery command. Dry-run is read-only; mutation is separately gated. */
5
+ export function registerLaneDropboxRepairCommand(parent, deps) {
6
+ parent.command("repair-dropbox")
7
+ .description("Inspect or redeliver messages stranded in a bare-ULID lane dropbox")
8
+ .argument("<ulid>", "26-character lane ULID")
9
+ .option("--company <slug>", "Select the company by slug")
10
+ .option("--apply", "Append marked messages to the canonical lane inbox and archive the orphan folder")
11
+ .action(deps.wrap(async (ulid, options) => {
12
+ const root = deps.hqRoot();
13
+ const globalFinding = findOrphanedBareIdDropboxes(root).find((item) => item.ulid === ulid.trim().toUpperCase());
14
+ if (globalFinding?.canonical_lane_ids.length !== undefined && globalFinding.canonical_lane_ids.length > 1) {
15
+ throw new LanesError("orphaned_dropbox_lane_ambiguous", `ULID ${globalFinding.ulid} matches more than one lane record`, { ulid: globalFinding.ulid });
16
+ }
17
+ const company = await deps.resolveCompany(options.company);
18
+ const finding = findOrphanedBareIdDropboxes(root, company).find((item) => item.ulid === ulid.trim().toUpperCase());
19
+ if (!finding)
20
+ throw new LanesError("orphaned_dropbox_not_found", `No orphaned bare-ID dropbox was found for ${ulid}`, { ulid });
21
+ if (finding.canonical_lane_ids.length !== 1)
22
+ throw new LanesError("orphaned_dropbox_lane_ambiguous", `ULID ${finding.ulid} matches more than one lane record`, { ulid: finding.ulid });
23
+ const lane = readLane(root, finding.canonical_lane_ids[0]);
24
+ const enabled = options.apply ? await deps.repairEnabled(lane) : false;
25
+ const result = repairOrphanedBareIdDropbox(root, ulid, {
26
+ apply: options.apply === true,
27
+ enabled,
28
+ now: deps.now(),
29
+ });
30
+ console.log(JSON.stringify({ ok: true, action: "repair-dropbox", ...result }, null, 2));
31
+ }));
32
+ }
33
+ //# sourceMappingURL=lanes-dropbox-repair.js.map
@@ -26,6 +26,7 @@ import { registerLaneKpisCommand } from "./lanes-kpis.js";
26
26
  import { registerLaneKeepAliveAuditCommand } from "./lanes-keep-alive-audit.js";
27
27
  import { registerLaneAutopilotReportCommand } from "./lanes-autopilot.js";
28
28
  import { registerLaneChecksCommand } from "./lanes-checks.js";
29
+ import { registerLaneDropboxRepairCommand } from "./lanes-dropbox-repair.js";
29
30
  import { registerLaneReportCommands } from "./lanes-report.js";
30
31
  import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
31
32
  import { registerLaneReviewCommand } from "./lanes-review.js";
@@ -34,7 +35,7 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
34
35
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
35
36
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
36
37
  import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
37
- import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG } from "../lib/lanes/flag-keys.js";
38
+ import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG } from "../lib/lanes/flag-keys.js";
38
39
  import { CODEX_ROLLOUT_RETENTION_FLAG, configuredCodexRolloutHomes, lowDiskGuardEnabledForLane, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
39
40
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
40
41
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
@@ -8322,6 +8323,21 @@ export function registerLanesCommand(parent, deps = {}) {
8322
8323
  deps: ctx.livenessDeps,
8323
8324
  }),
8324
8325
  }, wrap);
8326
+ registerLaneDropboxRepairCommand(lanes, {
8327
+ hqRoot: ctx.hqRoot,
8328
+ now: ctx.now,
8329
+ resolveCompany: ctx.resolveCompany,
8330
+ repairEnabled: async (lane) => {
8331
+ const resolution = resolveLaneCompany(ctx.hqRoot(), lane);
8332
+ const company = resolution.company ?? undefined;
8333
+ return readLanesRegistryFlagFailOff(LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, {
8334
+ company,
8335
+ ...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
8336
+ env: ctx.env,
8337
+ }, readLanesRegistryFlag);
8338
+ },
8339
+ wrap,
8340
+ });
8325
8341
  registerLaneReportCommands(lanes, {
8326
8342
  hqRoot: ctx.hqRoot,
8327
8343
  resolveCompany: ctx.resolveCompany,
package/dist/fast-core.js CHANGED
@@ -9,6 +9,7 @@
9
9
  import { resolveLiveRoot } from "./utils/hq-roots.js";
10
10
  import { runBundledScript } from "./utils/run-bundled-script.js";
11
11
  import { parseFastCore } from "./fast-scaffold.js";
12
+ import { CORE_RRR_WORKTREE_SCOPE_FIX_FLAG } from "./commands/core-rrr-worktree-scope-fix-flag.js";
12
13
  export async function readStdinIfPiped(stream) {
13
14
  if (stream.isTTY)
14
15
  return "";
@@ -35,6 +36,9 @@ export async function runFastCore(argv) {
35
36
  familyEnabled = envOverride ?? await (await import("./native-fast-route.js")).fastFlagEnabled(entry.familyFlagKey, process.env, { numericEnvironmentOverride: false });
36
37
  }
37
38
  const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
39
+ const worktreeScopeFixEnabled = entry.name === "repo-run-registry"
40
+ ? await (await import("./native-fast-route.js")).fastFlagEnabled(CORE_RRR_WORKTREE_SCOPE_FIX_FLAG, process.env, { numericEnvironmentOverride: false })
41
+ : undefined;
38
42
  const shouldInjectRoot = useShell
39
43
  ? entry.shellHqRoot === "live" || (entry.shellHqRoot === "reconcile-default" &&
40
44
  !parsed.args.includes("--hq-root") && !parsed.args.includes("--help") && !parsed.args.includes("-h"))
@@ -69,7 +73,7 @@ export async function runFastCore(argv) {
69
73
  }
70
74
  const readsStdin = entry.readsStdinFor?.(parsed.args) ?? entry.readsStdin ?? false;
71
75
  const stdin = readsStdin ? await readStdinIfPiped(process.stdin) : "";
72
- const code = await handler(parsed.args, stdin, process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg);
76
+ const code = await handler(parsed.args, stdin, process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg, worktreeScopeFixEnabled);
73
77
  // Let Node drain handler output before terminating. The timeout guard's
74
78
  // stderr message must survive the native route's process boundary.
75
79
  process.exitCode = code;
@@ -90,6 +94,7 @@ export async function runFastCore(argv) {
90
94
  ...(entry.name === "migrate-policy-triggers"
91
95
  ? { CLAUDE_PROJECT_DIR: process.env.CLAUDE_PROJECT_DIR || process.cwd() }
92
96
  : {}),
97
+ ...(entry.name === "repo-run-registry" ? { HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX: String(worktreeScopeFixEnabled) } : {}),
93
98
  };
94
99
  const { code } = runBundledScript({
95
100
  asset,
@@ -6,7 +6,7 @@ import { isControllerAliasOutpostId } from "./admission-host.js";
6
6
  import { LEAD_REGISTRY_REL, validateOutpostLeadRegistry } from "./autopilot-registry.js";
7
7
  import { appendDropboxMessage } from "./dropbox.js";
8
8
  import { LanesError } from "./errors.js";
9
- import { LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG, LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_FLAG, LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG, LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG as PER_LEAD_ERRORS_FLAG, } from "./flag-keys.js";
9
+ import { LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG, LANE_AUTOPILOT_NUDGE_DEDUPE_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_BATCH_FLAG, LEAD_AUTOPILOT_KPI_STALENESS_FLAG, LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG, LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG, LEAD_AUTOPILOT_PER_LEAD_ERRORS_FLAG as PER_LEAD_ERRORS_FLAG, } from "./flag-keys.js";
10
10
  import { resolveLaneCompany } from "./lane-company.js";
11
11
  import { readLanesRegistryFlag } from "./registry-flag.js";
12
12
  import { readAllLanes } from "./store.js";
@@ -786,9 +786,16 @@ async function runLeadAutopilotPassUnlocked(input) {
786
786
  result.skipped.push({ lead_lane: lead.lane_id, reason: "lead_company_unresolved" });
787
787
  continue;
788
788
  }
789
+ // A lead created on an Outpost records a local host, while its registry row names the Outpost.
790
+ const outpostNativeLead = !registeredLocalHost &&
791
+ input.outpostId === registered.host &&
792
+ lead.host.kind === "local" &&
793
+ lead.host.id === input.hostname &&
794
+ await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG, { company }).catch(() => false);
789
795
  const hostMatchesRegistry = registeredLocalHost
790
796
  ? lead.host.kind === "local"
791
- : lead.host.kind === "outpost" && lead.host.id === registered.host && input.outpostId === registered.host;
797
+ : outpostNativeLead ||
798
+ (lead.host.kind === "outpost" && lead.host.id === registered.host && input.outpostId === registered.host);
792
799
  if (!hostMatchesRegistry ||
793
800
  !laneRunsOnHost(lead, { hostname: input.hostname, outpostId: effectiveOutpostId })) {
794
801
  result.skipped.push({ lead_lane: lead.lane_id, reason: "lead_host_mismatch" });
@@ -22,4 +22,10 @@ export declare const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_DEFAULT_ENABLED = false;
22
22
  /** Enables staleness notices for a registered lead's dashboard headline. */
23
23
  export declare const LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG = "lanes.autopilot-headline-staleness";
24
24
  export declare const LEAD_AUTOPILOT_HEADLINE_STALENESS_DEFAULT_ENABLED = false;
25
+ /** Lets an Outpost's autopilot wake leads created on that Outpost, which record a local host. */
26
+ export declare const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot-outpost-native-leads";
27
+ export declare const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_DEFAULT_ENABLED = false;
28
+ /** Allows applying an operator-reviewed repair of messages stranded in bare-ULID dropboxes. */
29
+ export declare const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
30
+ export declare const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_DEFAULT_ENABLED = false;
25
31
  //# sourceMappingURL=flag-keys.d.ts.map
@@ -22,4 +22,10 @@ export const LEAD_AUTOPILOT_KPI_STALENESS_BATCH_DEFAULT_ENABLED = false;
22
22
  /** Enables staleness notices for a registered lead's dashboard headline. */
23
23
  export const LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG = "lanes.autopilot-headline-staleness";
24
24
  export const LEAD_AUTOPILOT_HEADLINE_STALENESS_DEFAULT_ENABLED = false;
25
+ /** Lets an Outpost's autopilot wake leads created on that Outpost, which record a local host. */
26
+ export const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot-outpost-native-leads";
27
+ export const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_DEFAULT_ENABLED = false;
28
+ /** Allows applying an operator-reviewed repair of messages stranded in bare-ULID dropboxes. */
29
+ export const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
30
+ export const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_DEFAULT_ENABLED = false;
25
31
  //# sourceMappingURL=flag-keys.js.map
@@ -1,4 +1,4 @@
1
- type KeepAliveAuditVerdict = "on_time" | "late" | "early_wake" | "message_wake" | "self_wake" | "early_by_keep_alive" | "not_restarted" | "waiting";
1
+ type KeepAliveAuditVerdict = "on_time" | "late" | "early_wake" | "message_wake" | "self_wake" | "early_by_keep_alive" | "not_restarted" | "keep_alive_off" | "waiting";
2
2
  interface KeepAliveAuditRestart {
3
3
  at: string;
4
4
  trigger: string;
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS } from "./keep-alive.js";
4
4
  import { LanesError } from "./errors.js";
5
5
  import { laneRunDir } from "./spawn.js";
6
- import { readAllLanes } from "./store.js";
6
+ import { listQuestionIds, readAllLanes, tryReadQuestion } from "./store.js";
7
7
  function readDoneClassifications(hqRoot, laneId) {
8
8
  const root = fs.realpathSync(hqRoot);
9
9
  const watcherLog = path.join(laneRunDir(root, laneId), "logs", "watcher.out");
@@ -56,6 +56,7 @@ const VERDICTS = [
56
56
  "message_wake",
57
57
  "self_wake",
58
58
  "early_by_keep_alive",
59
+ "keep_alive_off",
59
60
  "not_restarted",
60
61
  "waiting",
61
62
  ];
@@ -219,9 +220,66 @@ function hasDeliveredDropboxEvidence(hqRoot, laneId, stoppedAt, restartAt) {
219
220
  const realFile = fs.realpathSync(file);
220
221
  if (!withinRoot(root, realFile))
221
222
  return false;
222
- return stat.mtimeMs >= stoppedAt && stat.mtimeMs <= restartAt + 2 * 60_000;
223
+ if (stat.mtimeMs >= stoppedAt && stat.mtimeMs <= restartAt + 2 * 60_000)
224
+ return true;
225
+ if (!name.endsWith(".inbox.jsonl") && !name.endsWith(".resume.jsonl"))
226
+ return false;
227
+ const text = fs.readFileSync(realFile, "utf8");
228
+ for (const line of text.split(/\r?\n/)) {
229
+ if (!line.trim())
230
+ continue;
231
+ let value;
232
+ try {
233
+ value = JSON.parse(line);
234
+ }
235
+ catch (err) {
236
+ throw new LanesError("keep_alive_audit_event_invalid", `Cannot parse delivered lane message ${realFile}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: laneId, path: realFile });
237
+ }
238
+ if (isRecord(value)) {
239
+ const at = parseTime(value.at);
240
+ if (at !== undefined && at >= stoppedAt && at <= restartAt + 2 * 60_000)
241
+ return true;
242
+ }
243
+ }
244
+ return false;
223
245
  });
224
246
  }
247
+ function hasMessageResumeEvidence(lane, stoppedAt, restartAt) {
248
+ return (lane.resume_history ?? []).some((entry) => (entry.trigger === "message" || entry.trigger === "answered_questions") &&
249
+ (parseTime(entry.at) ?? Number.NaN) >= stoppedAt &&
250
+ (parseTime(entry.at) ?? Number.NaN) <= restartAt + 2 * 60_000);
251
+ }
252
+ function hasAnsweredQuestionEvidence(hqRoot, laneId, stoppedAt, restartAt) {
253
+ for (const questionId of listQuestionIds(hqRoot)) {
254
+ let question;
255
+ try {
256
+ question = tryReadQuestion(hqRoot, questionId);
257
+ }
258
+ catch {
259
+ // One unreadable question file must not fail the whole audit.
260
+ continue;
261
+ }
262
+ if (!question || question.lane_id !== laneId || question.origin?.id !== laneId)
263
+ continue;
264
+ const timestamps = [
265
+ question.delivered_at,
266
+ question.answer?.answered_at,
267
+ ...(question.answer_history ?? []).map((answer) => answer.answered_at),
268
+ ];
269
+ if (timestamps.some((timestamp) => {
270
+ const at = parseTime(timestamp);
271
+ return at !== undefined && at >= stoppedAt && at <= restartAt + 2 * 60_000;
272
+ }))
273
+ return true;
274
+ }
275
+ return false;
276
+ }
277
+ function laneWasKeepAliveOffAtStop(lane, stoppedAt) {
278
+ if (lane.keep_alive === true)
279
+ return false;
280
+ const optedInAt = parseTime(lane.keep_alive_opted_in_at);
281
+ return optedInAt === undefined || optedInAt > stoppedAt;
282
+ }
225
283
  function firstRestartAfter(lane, stop, nextEnvelopeMs) {
226
284
  const restart = (lane.resume_history ?? [])
227
285
  .map((entry) => ({ entry, at: parseTime(entry.at) }))
@@ -241,13 +299,18 @@ function classifyVerdict(input) {
241
299
  const restartAt = Date.parse(restart.at);
242
300
  if (restart.caller === lane.lane_id)
243
301
  return "self_wake";
302
+ if (restart.trigger === "message" || restart.trigger === "answered_questions")
303
+ return "message_wake";
244
304
  if (restart.trigger === "keep_alive") {
245
305
  const event = input.resumeEvents
246
- .filter((candidate) => candidate.at > input.stoppedAt && Math.abs(candidate.at - restartAt) <= 2 * 60_000)
306
+ .filter((candidate) => candidate.at > input.stoppedAt && candidate.at <= restartAt + 2 * 60_000)
247
307
  .sort((left, right) => Math.abs(left.at - restartAt) - Math.abs(right.at - restartAt))[0];
248
308
  if (event?.reason === "pending_work")
249
309
  return "message_wake";
250
- if (event?.reason === undefined && hasDeliveredDropboxEvidence(input.hqRoot, lane.lane_id, input.stoppedAt, restartAt)) {
310
+ if (event?.reason === undefined &&
311
+ (hasDeliveredDropboxEvidence(input.hqRoot, lane.lane_id, input.stoppedAt, restartAt) ||
312
+ hasMessageResumeEvidence(lane, input.stoppedAt, restartAt) ||
313
+ hasAnsweredQuestionEvidence(input.hqRoot, lane.lane_id, input.stoppedAt, restartAt))) {
251
314
  return "message_wake";
252
315
  }
253
316
  const nextCheckAt = parseTime(input.nextCheckAt);
@@ -264,6 +327,8 @@ function classifyVerdict(input) {
264
327
  return "early_wake";
265
328
  return "late";
266
329
  }
330
+ if (laneWasKeepAliveOffAtStop(lane, input.stoppedAt))
331
+ return "keep_alive_off";
267
332
  if (lane.state === "done" && nowMs > dueAt + KEEP_ALIVE_NOT_RESTARTED_GRACE_MS)
268
333
  return "not_restarted";
269
334
  return "waiting";
@@ -0,0 +1,24 @@
1
+ import type { LaneCompanyRef } from "./types.js";
2
+ export interface OrphanedBareIdDropbox {
3
+ ulid: string;
4
+ canonical_lane_ids: string[];
5
+ message_count: number;
6
+ newest_message_at: string | null;
7
+ source_dir: string;
8
+ }
9
+ /** Finds bare-ULID run folders that have no bare lane record but do have a canonical lane. */
10
+ export declare function findOrphanedBareIdDropboxes(hqRoot: string, company?: LaneCompanyRef): OrphanedBareIdDropbox[];
11
+ export interface RepairOrphanedDropboxResult {
12
+ ulid: string;
13
+ canonical_lane_id: string;
14
+ message_count: number;
15
+ newest_message_at: string | null;
16
+ applied: boolean;
17
+ archived_dir?: string;
18
+ }
19
+ export declare function repairOrphanedBareIdDropbox(hqRoot: string, ulidInput: string, options: {
20
+ apply: boolean;
21
+ enabled: boolean;
22
+ now: Date;
23
+ }): RepairOrphanedDropboxResult;
24
+ //# sourceMappingURL=orphaned-dropbox.d.ts.map
@@ -0,0 +1,213 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { LanesError } from "./errors.js";
4
+ import { LANES_RUNS_REL } from "./spawn.js";
5
+ import { readAllLanes, tryReadLane } from "./store.js";
6
+ import { resolveLaneCompany } from "./lane-company.js";
7
+ const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
8
+ function fail(error, message, details = {}) {
9
+ throw new LanesError(error, message, details);
10
+ }
11
+ function laneUlid(laneId) {
12
+ const prefix = laneId.slice(0, 26);
13
+ if (!ULID_PATTERN.test(prefix))
14
+ return null;
15
+ return laneId.length === 26 || laneId[26] === "_" ? prefix.toUpperCase() : null;
16
+ }
17
+ function checkedInbox(sourceDir) {
18
+ const dropboxDir = path.join(sourceDir, "dropbox");
19
+ const inbox = path.join(dropboxDir, "inbox.jsonl");
20
+ try {
21
+ const dropboxStat = fs.lstatSync(dropboxDir);
22
+ if (!dropboxStat.isDirectory() || dropboxStat.isSymbolicLink()) {
23
+ return fail("orphaned_dropbox_invalid", "Orphaned dropbox path must be a regular directory", { path: dropboxDir });
24
+ }
25
+ }
26
+ catch (error) {
27
+ if (error.code === "ENOENT")
28
+ return { inbox, messages: [] };
29
+ if (error instanceof LanesError)
30
+ throw error;
31
+ return fail("orphaned_dropbox_unreadable", `Could not inspect orphaned dropbox: ${error instanceof Error ? error.message : String(error)}`, { path: dropboxDir });
32
+ }
33
+ let stat;
34
+ try {
35
+ stat = fs.lstatSync(inbox);
36
+ }
37
+ catch (error) {
38
+ if (error.code === "ENOENT")
39
+ return { inbox, messages: [] };
40
+ return fail("orphaned_dropbox_unreadable", `Could not inspect orphaned inbox: ${error instanceof Error ? error.message : String(error)}`, { path: inbox });
41
+ }
42
+ if (!stat.isFile() || stat.isSymbolicLink()) {
43
+ return fail("orphaned_dropbox_invalid", "Orphaned inbox must be a regular file", { path: inbox });
44
+ }
45
+ let text;
46
+ try {
47
+ text = fs.readFileSync(inbox, "utf8");
48
+ }
49
+ catch (error) {
50
+ return fail("orphaned_dropbox_unreadable", `Could not read orphaned inbox: ${error instanceof Error ? error.message : String(error)}`, { path: inbox });
51
+ }
52
+ const messages = [];
53
+ for (const [index, line] of text.split(/\r?\n/).entries()) {
54
+ if (line.trim() === "")
55
+ continue;
56
+ let value;
57
+ try {
58
+ value = JSON.parse(line);
59
+ }
60
+ catch {
61
+ return fail("orphaned_dropbox_message_invalid", "Orphaned inbox contains malformed JSONL", { path: inbox, line: index + 1 });
62
+ }
63
+ if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.text !== "string") {
64
+ return fail("orphaned_dropbox_message_invalid", "Orphaned inbox contains an invalid dropbox message", { path: inbox, line: index + 1 });
65
+ }
66
+ messages.push(value);
67
+ }
68
+ return { inbox, messages };
69
+ }
70
+ function canonicalLanesByUlid(hqRoot) {
71
+ const grouped = new Map();
72
+ for (const lane of readAllLanes(hqRoot)) {
73
+ const ulid = laneUlid(lane.lane_id);
74
+ if (!ulid)
75
+ continue;
76
+ grouped.set(ulid, [...(grouped.get(ulid) ?? []), lane]);
77
+ }
78
+ return grouped;
79
+ }
80
+ /** Finds bare-ULID run folders that have no bare lane record but do have a canonical lane. */
81
+ export function findOrphanedBareIdDropboxes(hqRoot, company) {
82
+ const runsRoot = path.join(hqRoot, LANES_RUNS_REL);
83
+ let entries;
84
+ try {
85
+ const stat = fs.lstatSync(runsRoot);
86
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
87
+ return fail("orphaned_dropbox_runs_invalid", "Lane runs path must be a regular directory", { path: runsRoot });
88
+ }
89
+ entries = fs.readdirSync(runsRoot, { withFileTypes: true });
90
+ }
91
+ catch (error) {
92
+ if (error.code === "ENOENT")
93
+ return [];
94
+ if (error instanceof LanesError)
95
+ throw error;
96
+ return fail("orphaned_dropbox_runs_unreadable", `Could not list lane runs: ${error instanceof Error ? error.message : String(error)}`, { path: runsRoot });
97
+ }
98
+ const lanesByUlid = canonicalLanesByUlid(hqRoot);
99
+ const findings = [];
100
+ for (const entry of entries) {
101
+ if (!entry.isDirectory() || !ULID_PATTERN.test(entry.name))
102
+ continue;
103
+ const ulid = entry.name.toUpperCase();
104
+ if (tryReadLane(hqRoot, entry.name) !== null)
105
+ continue;
106
+ const canonicalLanes = lanesByUlid.get(ulid) ?? [];
107
+ const visibleLanes = company === undefined ? canonicalLanes : canonicalLanes.filter((lane) => {
108
+ const resolved = resolveLaneCompany(hqRoot, lane).company;
109
+ return resolved?.slug === company.slug && resolved.uid === company.uid;
110
+ });
111
+ const canonicalLaneIds = visibleLanes.map((lane) => lane.lane_id);
112
+ if (canonicalLaneIds.length === 0)
113
+ continue;
114
+ const sourceDir = path.join(runsRoot, entry.name);
115
+ const { messages } = checkedInbox(sourceDir);
116
+ const newest = messages.reduce((current, message) => {
117
+ if (typeof message.at !== "string")
118
+ return current;
119
+ const ms = Date.parse(message.at);
120
+ if (!Number.isFinite(ms) || (current !== null && current.ms >= ms))
121
+ return current;
122
+ return { at: message.at, ms };
123
+ }, null);
124
+ findings.push({
125
+ ulid,
126
+ canonical_lane_ids: canonicalLaneIds,
127
+ message_count: messages.length,
128
+ newest_message_at: newest?.at ?? null,
129
+ source_dir: sourceDir,
130
+ });
131
+ }
132
+ return findings.sort((a, b) => a.ulid.localeCompare(b.ulid));
133
+ }
134
+ export function repairOrphanedBareIdDropbox(hqRoot, ulidInput, options) {
135
+ const ulid = ulidInput.trim().toUpperCase();
136
+ if (!ULID_PATTERN.test(ulid))
137
+ return fail("orphaned_dropbox_ulid_invalid", "Expected a 26-character ULID", { ulid: ulidInput });
138
+ const finding = findOrphanedBareIdDropboxes(hqRoot).find((entry) => entry.ulid === ulid);
139
+ if (!finding)
140
+ return fail("orphaned_dropbox_not_found", `No orphaned bare-ID dropbox was found for ${ulid}`, { ulid });
141
+ if (finding.canonical_lane_ids.length !== 1) {
142
+ return fail("orphaned_dropbox_lane_ambiguous", `ULID ${ulid} matches more than one lane record`, { ulid, lane_ids: finding.canonical_lane_ids });
143
+ }
144
+ const canonicalLaneId = finding.canonical_lane_ids[0];
145
+ if (tryReadLane(hqRoot, canonicalLaneId) === null) {
146
+ return fail("orphaned_dropbox_lane_changed", `Canonical lane ${canonicalLaneId} disappeared before repair`, { ulid, lane_id: canonicalLaneId });
147
+ }
148
+ const { messages } = checkedInbox(finding.source_dir);
149
+ const newest = messages.reduce((current, message) => {
150
+ if (typeof message.at !== "string")
151
+ return current;
152
+ const ms = Date.parse(message.at);
153
+ if (!Number.isFinite(ms) || (current !== null && current.ms >= ms))
154
+ return current;
155
+ return { at: message.at, ms };
156
+ }, null);
157
+ const result = {
158
+ ulid,
159
+ canonical_lane_id: canonicalLaneId,
160
+ message_count: messages.length,
161
+ newest_message_at: newest?.at ?? null,
162
+ applied: false,
163
+ };
164
+ if (!options.apply)
165
+ return result;
166
+ if (!options.enabled)
167
+ return fail("orphaned_dropbox_repair_disabled", "Applying orphaned dropbox repairs requires the lanes.orphaned-bare-id-dropbox-repair flag", { ulid });
168
+ const archiveName = `${ulid}.orphaned-${options.now.toISOString().replaceAll(/[:.]/g, "-")}`;
169
+ const archivedDir = path.join(path.dirname(finding.source_dir), archiveName);
170
+ if (fs.existsSync(archivedDir))
171
+ return fail("orphaned_dropbox_archive_exists", "Orphaned dropbox archive path already exists", { ulid, path: archivedDir });
172
+ if (messages.length > 0) {
173
+ const targetRunDir = path.join(hqRoot, LANES_RUNS_REL, canonicalLaneId);
174
+ const targetDropboxDir = path.join(targetRunDir, "dropbox");
175
+ fs.mkdirSync(targetRunDir, { recursive: true });
176
+ for (const dir of [targetRunDir, targetDropboxDir]) {
177
+ try {
178
+ const stat = fs.lstatSync(dir);
179
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
180
+ return fail("orphaned_dropbox_target_invalid", "Canonical dropbox path must be a regular directory", { path: dir });
181
+ }
182
+ }
183
+ catch (error) {
184
+ if (error.code !== "ENOENT") {
185
+ return fail("orphaned_dropbox_target_unreadable", `Could not inspect canonical dropbox path: ${error instanceof Error ? error.message : String(error)}`, { path: dir });
186
+ }
187
+ fs.mkdirSync(dir);
188
+ }
189
+ }
190
+ const targetInbox = path.join(targetDropboxDir, "inbox.jsonl");
191
+ try {
192
+ const stat = fs.lstatSync(targetInbox);
193
+ if (!stat.isFile() || stat.isSymbolicLink())
194
+ return fail("orphaned_dropbox_target_invalid", "Canonical inbox must be a regular file", { path: targetInbox });
195
+ }
196
+ catch (error) {
197
+ if (error.code !== "ENOENT") {
198
+ return fail("orphaned_dropbox_target_unreadable", `Could not inspect canonical inbox: ${error instanceof Error ? error.message : String(error)}`, { path: targetInbox });
199
+ }
200
+ }
201
+ const redeliveredAt = options.now.toISOString();
202
+ const redelivered = messages.map((message) => ({
203
+ ...message,
204
+ redelivered: true,
205
+ redelivered_from: ulid,
206
+ redelivered_at: redeliveredAt,
207
+ }));
208
+ fs.appendFileSync(targetInbox, `${redelivered.map((message) => JSON.stringify(message)).join("\n")}\n`, "utf8");
209
+ }
210
+ fs.renameSync(finding.source_dir, archivedDir);
211
+ return { ...result, applied: true, archived_dir: archivedDir };
212
+ }
213
+ //# sourceMappingURL=orphaned-dropbox.js.map
@@ -69,7 +69,7 @@ export interface TrackerCheckFinding {
69
69
  [key: string]: unknown;
70
70
  }
71
71
  export interface TrackerCheckResult {
72
- id: "questions" | "lead_lanes" | "freshness" | "blockers" | "kpis" | "directives" | "orphaned_lane";
72
+ id: "questions" | "lead_lanes" | "freshness" | "blockers" | "kpis" | "directives" | "orphaned_lane" | "orphaned_bare_id_dropbox";
73
73
  result: "ok" | "failing";
74
74
  items: TrackerCheckFinding[];
75
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.318.0",
3
+ "version": "5.319.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {