@indigoai-us/hq-cli 5.318.1-beta.0 → 5.320.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +10 -2
  2. package/assets/scaffold/core/scripts/repo-run-registry.sh +61 -17
  3. package/assets/scaffold/core/scripts/session-journal.sh +40 -2
  4. package/dist/bin/hq-auth-refresh.d.ts +3 -1
  5. package/dist/bin/hq-auth-refresh.js +41 -6
  6. package/dist/command-catalog.generated.d.ts +22 -0
  7. package/dist/command-catalog.generated.js +29 -0
  8. package/dist/commands/cloud.js +0 -1
  9. package/dist/commands/core-hook-state-helpers-native.d.ts +1 -1
  10. package/dist/commands/core-hook-state-helpers-native.js +36 -7
  11. package/dist/commands/core-repo-run-registry-native.d.ts +1 -0
  12. package/dist/commands/core-repo-run-registry-native.js +10 -2
  13. package/dist/commands/core-rrr-worktree-scope-fix-flag.d.ts +4 -0
  14. package/dist/commands/core-rrr-worktree-scope-fix-flag.js +7 -0
  15. package/dist/commands/core-session-journal-native.js +31 -2
  16. package/dist/commands/lanes-checks.js +12 -0
  17. package/dist/commands/lanes-dropbox-repair.d.ts +14 -0
  18. package/dist/commands/lanes-dropbox-repair.js +33 -0
  19. package/dist/commands/lanes.js +44 -5
  20. package/dist/fast-core.js +6 -1
  21. package/dist/index.d.ts +9 -0
  22. package/dist/index.js +135 -63
  23. package/dist/lib/daemon/run.d.ts +2 -0
  24. package/dist/lib/daemon/run.js +10 -1
  25. package/dist/lib/lanes/autopilot.js +9 -2
  26. package/dist/lib/lanes/flag-keys.d.ts +9 -0
  27. package/dist/lib/lanes/flag-keys.js +9 -0
  28. package/dist/lib/lanes/keep-alive-audit.d.ts +1 -1
  29. package/dist/lib/lanes/keep-alive-audit.js +69 -4
  30. package/dist/lib/lanes/orphaned-dropbox.d.ts +24 -0
  31. package/dist/lib/lanes/orphaned-dropbox.js +213 -0
  32. package/dist/lib/lanes/tracker-checks.d.ts +1 -1
  33. package/dist/utils/package-use-lease.d.ts +25 -0
  34. package/dist/utils/package-use-lease.js +213 -0
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,9 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [5.318.1-beta.0] — 2026-10-02
5
+ ## [5.320.0] — 2026-10-02
6
+ - Write session-journal titles as escaped YAML strings so free-form titles remain parseable.
7
+ - Added a default-off hq-flags gate that lets lane list pollers skip remote owner refreshes to reduce poll latency.
8
+ - Publish per-process package-use leases so Desktop updates can wait for active hq-cli work without native locks.
9
+
10
+ ## [5.319.0] — 2026-10-02
6
11
  - Let `hq access` use ACL-checked company presigned reads when STS vend rejects a truncated write-policy scope.
7
- - Let `hq sync now` skip unchanged server-owned cloud placeholders while keeping their warnings visible, so the sync can complete.
12
+ - 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.
13
+ - Add a default-off fix for repo-run-registry worktree-scoped owner matching.
14
+ - Correct keep-alive audit verdicts for pending-work wakes and lanes that were not opted in.
15
+ - Detect and safely redeliver messages stranded in bare-ULID lane dropboxes.
8
16
 
9
17
  ## [5.318.0] — 2026-10-02
10
18
  - `hq lanes autopilot run` now marks missing lead registry entries as stale and includes a command to remove each stale row.
@@ -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')
@@ -43,6 +43,41 @@ slugify() {
43
43
  | cut -c1-40
44
44
  }
45
45
 
46
+ yaml_double_quoted() {
47
+ local value="$1" output='"' character next codepoint hex
48
+ local LC_ALL=C index
49
+ for ((index = 0; index < ${#value}; index++)); do
50
+ character="${value:index:1}"
51
+ if [[ "$character" == $'\xc2' ]] && (( index + 1 < ${#value} )); then
52
+ next="${value:index+1:1}"
53
+ printf -v codepoint '%d' "'${next}"
54
+ if (( codepoint >= 128 && codepoint <= 159 )); then
55
+ printf -v hex '%04x' "$codepoint"
56
+ output+="\\u$hex"
57
+ index=$((index + 1))
58
+ continue
59
+ fi
60
+ fi
61
+ case "$character" in
62
+ \\) output+='\\' ;;
63
+ '"') output+='\"' ;;
64
+ $'\n') output+='\n' ;;
65
+ $'\r') output+='\r' ;;
66
+ $'\t') output+='\t' ;;
67
+ *)
68
+ if [[ "$character" == [[:cntrl:]] ]]; then
69
+ printf -v codepoint '%d' "'${character}"
70
+ printf -v hex '%02x' "$codepoint"
71
+ output+="\\x$hex"
72
+ else
73
+ output+="$character"
74
+ fi
75
+ ;;
76
+ esac
77
+ done
78
+ printf '%s"' "$output"
79
+ }
80
+
46
81
  # Counters are per-session. A date-global counter lets concurrent Claude and
47
82
  # Codex tasks trip (and reset) each other's journal reminders.
48
83
  counter_key() {
@@ -140,6 +175,9 @@ case "$cmd" in
140
175
 
141
176
  slug="$(slugify "$title")"
142
177
  [ -z "$slug" ] && slug="entry"
178
+ yaml_title="$(yaml_double_quoted "$title")"
179
+ yaml_slug="$slug"
180
+ [[ "$slug" != *$'\n'* ]] || yaml_slug="$(yaml_double_quoted "$slug")"
143
181
  JOURNAL_WRITE_LOCK=""
144
182
  acquire_journal_write_lock "$dir" || { warn "write: failed to acquire journal lock"; exit 0; }
145
183
  trap release_journal_write_lock EXIT
@@ -161,8 +199,8 @@ case "$cmd" in
161
199
  {
162
200
  printf -- '---\n'
163
201
  printf 'ts: %s\n' "$(now_iso_z)"
164
- printf 'title: %s\n' "$title"
165
- printf 'slug: %s\n' "$slug"
202
+ printf 'title: %s\n' "$yaml_title"
203
+ printf 'slug: %s\n' "$yaml_slug"
166
204
  if [ -n "$files_csv" ]; then
167
205
  printf 'files:\n'
168
206
  old_ifs="$IFS"
@@ -11,7 +11,9 @@
11
11
  * non-interactively.
12
12
  */
13
13
  import "../node-preflight.js";
14
+ import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
14
15
  import "../node-network-compat.js";
15
- import { refreshCachedSession } from "../utils/cognito-session.js";
16
+ import type { refreshCachedSession } from "../utils/cognito-session.js";
16
17
  export declare function runAuthRefresh(refreshSession?: typeof refreshCachedSession): Promise<number>;
18
+ export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease): Promise<number>;
17
19
  //# sourceMappingURL=hq-auth-refresh.d.ts.map
@@ -13,15 +13,16 @@
13
13
  // MUST be first: guard the Node version before any dependency that needs a
14
14
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
15
15
  import "../node-preflight.js";
16
+ import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
16
17
  import "../node-network-compat.js";
17
18
  import { fileURLToPath } from "node:url";
18
19
  import path from "node:path";
19
- import { finishSentrySession, initSentry, Sentry } from "../sentry.js";
20
- import { refreshCachedSession } from "../utils/cognito-session.js";
21
- export async function runAuthRefresh(refreshSession = refreshCachedSession) {
20
+ export async function runAuthRefresh(refreshSession) {
21
+ const { finishSentrySession, Sentry } = await import("../sentry.js");
22
+ const { refreshCachedSession: refresh } = await import("../utils/cognito-session.js");
22
23
  let exitCode = 1;
23
24
  try {
24
- const result = await refreshSession();
25
+ const result = await (refreshSession ?? refresh)();
25
26
  if (result.refreshed) {
26
27
  exitCode = 0;
27
28
  }
@@ -41,8 +42,42 @@ export async function runAuthRefresh(refreshSession = refreshCachedSession) {
41
42
  }
42
43
  return exitCode;
43
44
  }
44
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
45
+ function isPackageUseUpdatePendingError(error) {
46
+ return Boolean(error &&
47
+ typeof error === "object" &&
48
+ error.name === "PackageUseUpdatePendingError");
49
+ }
50
+ function packageUseErrorClass(error) {
51
+ if (!error || typeof error !== "object")
52
+ return "unknown";
53
+ const value = error;
54
+ if (typeof value.code === "string")
55
+ return value.code;
56
+ if (value.cause && typeof value.cause === "object") {
57
+ const cause = value.cause;
58
+ if (typeof cause.code === "string")
59
+ return cause.code;
60
+ if (typeof cause.name === "string")
61
+ return cause.name;
62
+ }
63
+ return typeof value.name === "string" ? value.name : "unknown";
64
+ }
65
+ export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLease) {
66
+ try {
67
+ acquireLease();
68
+ }
69
+ catch (error) {
70
+ if (isPackageUseUpdatePendingError(error)) {
71
+ process.stderr.write(`hq-auth-refresh: ${error instanceof Error ? error.message : "The HQ CLI package is being updated"}\n`);
72
+ return 1;
73
+ }
74
+ process.stderr.write(`hq-auth-refresh: package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
75
+ }
76
+ const { initSentry } = await import("../sentry.js");
45
77
  initSentry();
46
- void runAuthRefresh().then((exitCode) => process.exit(exitCode));
78
+ return runAuthRefresh();
79
+ }
80
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
81
+ void startAuthRefresh().then((exitCode) => process.exit(exitCode));
47
82
  }
48
83
  //# sourceMappingURL=hq-auth-refresh.js.map
@@ -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",
@@ -1697,7 +1697,6 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1697
1697
  hqRoot,
1698
1698
  progressRecorder: trackingProgressRecorder(planLimitTracker, pushProgressCompany),
1699
1699
  skipUnchanged: true,
1700
- serverOwnedPathPolicy: "skip",
1701
1700
  propagateDeletes: true,
1702
1701
  propagateDeletePolicy: resolveDeletePolicy(),
1703
1702
  ...(onConflict ? { onConflict } : {}),
@@ -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
@@ -24,7 +24,35 @@ const counterKey = (value) => {
24
24
  return key.length > 96 ? key.slice(0, 96) : key;
25
25
  };
26
26
  const slugify = (value) => value.toLowerCase()
27
- .replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 40);
27
+ .split("\n")
28
+ .map((line) => line.replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 40))
29
+ .join("\n");
30
+ function yamlDoubleQuoted(value) {
31
+ let escaped = "";
32
+ for (const character of value) {
33
+ const codePoint = character.codePointAt(0) ?? 0;
34
+ if (character === "\\")
35
+ escaped += "\\\\";
36
+ else if (character === '"')
37
+ escaped += '\\"';
38
+ else if (character === "\n")
39
+ escaped += "\\n";
40
+ else if (character === "\r")
41
+ escaped += "\\r";
42
+ else if (character === "\t")
43
+ escaped += "\\t";
44
+ else if (codePoint < 0x20 || codePoint === 0x7f) {
45
+ escaped += `\\x${codePoint.toString(16).padStart(2, "0")}`;
46
+ }
47
+ else if (codePoint >= 0x80 && codePoint <= 0x9f) {
48
+ escaped += `\\u${codePoint.toString(16).padStart(4, "0")}`;
49
+ }
50
+ else {
51
+ escaped += character;
52
+ }
53
+ }
54
+ return `"${escaped}"`;
55
+ }
28
56
  const warning = (message) => { process.stderr.write(`session-journal: ${message}\n`); };
29
57
  const journalDir = (root, date) => path.join(root, "workspace/threads/journal", date);
30
58
  const hasFile = (file) => {
@@ -211,6 +239,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
211
239
  return 0;
212
240
  }
213
241
  const slug = slugify(title) || "entry";
242
+ const yamlSlug = slug.includes("\n") ? yamlDoubleQuoted(slug) : slug;
214
243
  const body = bodyFile && hasFile(bodyFile)
215
244
  ? fs.readFileSync(bodyFile, "utf8")
216
245
  : "## Goal\n\n## Findings\n\n## Decisions\n\n## Next\n";
@@ -226,7 +255,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
226
255
  for (let attempt = 0; attempt < 10_000; attempt += 1) {
227
256
  seq = nextSequence(dir);
228
257
  output = path.join(dir, `${seq}-${slug}.md`);
229
- const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${title}\n`, `slug: ${slug}\n`];
258
+ const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${yamlDoubleQuoted(title)}\n`, `slug: ${yamlSlug}\n`];
230
259
  if (files) {
231
260
  lines.push("files:\n");
232
261
  for (const file of files.split(","))
@@ -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