@indigoai-us/hq-cli 5.318.1-beta.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 +5 -2
- package/assets/scaffold/core/scripts/repo-run-registry.sh +61 -17
- package/dist/command-catalog.generated.d.ts +22 -0
- package/dist/command-catalog.generated.js +29 -0
- package/dist/commands/cloud.js +0 -1
- package/dist/commands/core-hook-state-helpers-native.d.ts +1 -1
- package/dist/commands/core-hook-state-helpers-native.js +36 -7
- package/dist/commands/core-repo-run-registry-native.d.ts +1 -0
- package/dist/commands/core-repo-run-registry-native.js +10 -2
- package/dist/commands/core-rrr-worktree-scope-fix-flag.d.ts +4 -0
- package/dist/commands/core-rrr-worktree-scope-fix-flag.js +7 -0
- package/dist/commands/lanes-checks.js +12 -0
- package/dist/commands/lanes-dropbox-repair.d.ts +14 -0
- package/dist/commands/lanes-dropbox-repair.js +33 -0
- package/dist/commands/lanes.js +17 -1
- package/dist/fast-core.js +6 -1
- package/dist/lib/lanes/autopilot.js +9 -2
- package/dist/lib/lanes/flag-keys.d.ts +6 -0
- package/dist/lib/lanes/flag-keys.js +6 -0
- package/dist/lib/lanes/keep-alive-audit.d.ts +1 -1
- package/dist/lib/lanes/keep-alive-audit.js +69 -4
- package/dist/lib/lanes/orphaned-dropbox.d.ts +24 -0
- package/dist/lib/lanes/orphaned-dropbox.js +213 -0
- package/dist/lib/lanes/tracker-checks.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [5.
|
|
5
|
+
## [5.319.0] — 2026-10-02
|
|
6
6
|
- Let `hq access` use ACL-checked company presigned reads when STS vend rejects a truncated write-policy scope.
|
|
7
|
-
-
|
|
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.
|
|
8
11
|
|
|
9
12
|
## [5.318.0] — 2026-10-02
|
|
10
13
|
- `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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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",
|
package/dist/commands/cloud.js
CHANGED
|
@@ -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
|
-
{
|
|
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({
|
|
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, {
|
|
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
|
|
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
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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
|
-
:
|
|
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
|
-
|
|
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 &&
|
|
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 &&
|
|
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
|
}
|