@indigoai-us/hq-cli 5.345.55 → 5.345.56
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 +12 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
- package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
- package/assets/scaffold/core/scripts/session-title.sh +18 -15
- package/dist/command-catalog.generated.d.ts +40 -0
- package/dist/command-catalog.generated.js +51 -0
- package/dist/commands/core-delegate-w1b.js +18 -5
- package/dist/commands/core-derive-trigger-facts-command.js +14 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
- package/dist/commands/core-derive-trigger-facts-native.js +20 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +15 -2
- package/dist/commands/lanes-checks.d.ts +3 -0
- package/dist/commands/lanes-checks.js +34 -0
- package/dist/commands/lanes-session-start.d.ts +2 -7
- package/dist/commands/lanes-session-start.js +9 -29
- package/dist/commands/lanes-shared.d.ts +2 -1
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +117 -16
- package/dist/commands/members.d.ts +4 -0
- package/dist/commands/members.js +23 -2
- package/dist/lanes-drain-hook.js +6 -2
- package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-health.js +47 -23
- package/dist/lib/lanes/dropbox.d.ts +4 -2
- package/dist/lib/lanes/dropbox.js +54 -9
- package/dist/lib/lanes/flag-keys.d.ts +8 -0
- package/dist/lib/lanes/flag-keys.js +8 -0
- package/dist/lib/lanes/hooks-install.d.ts +14 -11
- package/dist/lib/lanes/hooks-install.js +70 -50
- package/dist/lib/lanes/reaper.d.ts +58 -0
- package/dist/lib/lanes/reaper.js +403 -0
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +32 -2
- package/dist/lib/lanes/status-fields.d.ts +12 -0
- package/dist/lib/lanes/status-fields.js +189 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/worker-continuity.d.ts +18 -0
- package/dist/lib/lanes/worker-continuity.js +81 -0
- package/dist/lib/lanes/worker-process.d.ts +4 -0
- package/dist/lib/lanes/worker-process.js +77 -0
- package/dist/main.js +9 -3
- package/dist/utils/cli-telemetry.d.ts +4 -1
- package/dist/utils/cli-telemetry.js +8 -0
- package/dist/utils/jq-windows-compat.d.ts +13 -0
- package/dist/utils/jq-windows-compat.js +48 -0
- package/dist/utils/run-bundled-script.d.ts +3 -0
- package/dist/utils/run-bundled-script.js +9 -80
- package/dist/utils/windows-bash.d.ts +5 -0
- package/dist/utils/windows-bash.js +75 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.56] — 2026-10-09
|
|
6
|
+
- Add `hq lanes reap` to report local lanes whose session senior is confirmed ended, clear stale pid markers on request, and keep process-tree stopping behind the `lanes.reap-stop` hq-flags key. SessionStart runs the report-only sweep detached and stores its report in host-specific user state.
|
|
7
|
+
- Record privacy-limited `hq members invite` success and failure outcomes through the existing CLI feature telemetry event.
|
|
8
|
+
|
|
9
|
+
- Continue a senior's most recent same-company, same-provider worker lane on `hq lanes create`, refuse live duplicate worker lanes, and filter `hq lanes list` by worker.
|
|
10
|
+
|
|
11
|
+
- Pass jq diagnostic filters to Windows batch wrappers as single-line arguments so malformed-ACL parity reports the original jq diagnostic instead of failing on an invalid command line.
|
|
12
|
+
- Keep the bundled derive-trigger-facts helper's closed-stdin error prompt on macOS instead of blocking the hook call.
|
|
13
|
+
- Native delegate diagnostics match the bundled helper's Git Bash jq byte handling, including jq.exe CRLF output when `--binary` is unavailable.
|
|
14
|
+
- Lane drop-box hooks deliver queued messages after tool calls and at a deliverable Stop event. Lane list and show JSON include bounded output, PR, inbox, elapsed time, and worker exit fields.
|
|
15
|
+
- hq doctor now finds a company's sync manifest status instead of reporting the company as unresolved.
|
|
16
|
+
|
|
5
17
|
## [5.345.55] — 2026-10-08
|
|
6
18
|
- Added `hq daemon sync conflicts acknowledge <id>` so the desktop can clear a parked-conflict notice without writing runner-owned sync state.
|
|
7
19
|
- Add `hq import scan`, which runs the HQ import-context scanner on this
|
|
@@ -65,7 +65,16 @@ case "${OSTYPE:-}" in
|
|
|
65
65
|
STDIN_JSON="${STDIN_JSON%$'\n'}"
|
|
66
66
|
done
|
|
67
67
|
;;
|
|
68
|
-
*)
|
|
68
|
+
*)
|
|
69
|
+
# Bash 3.2 on macOS can block while opening /dev/stdin when fd 0 is closed.
|
|
70
|
+
# Detect that case with a builtin redirection so the existing fail-closed
|
|
71
|
+
# behavior returns promptly on every POSIX host.
|
|
72
|
+
if [[ ! -e /dev/stdin ]]; then
|
|
73
|
+
printf '%s: line 68: /dev/stdin: No such file or directory\n' "$0" >&2
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
STDIN_JSON="$(</dev/stdin)" || STDIN_JSON='{}'
|
|
77
|
+
;;
|
|
69
78
|
esac
|
|
70
79
|
[ -n "$STDIN_JSON" ] || STDIN_JSON='{}'
|
|
71
80
|
JQ="$(command -v jq || true)"
|
|
@@ -219,7 +219,8 @@ validate_job_file() {
|
|
|
219
219
|
err "$file" "yaml" "failed to parse YAML"
|
|
220
220
|
return
|
|
221
221
|
fi
|
|
222
|
-
|
|
222
|
+
# Use jq's exit status instead of its text output: jq.exe writes CRLF on Windows.
|
|
223
|
+
if ! jq -es 'length == 1 and (.[0] | type == "object")' <<<"$json" >/dev/null; then
|
|
223
224
|
err "$file" "yaml" "job root must be a mapping"
|
|
224
225
|
return
|
|
225
226
|
fi
|
|
@@ -38,13 +38,14 @@ done
|
|
|
38
38
|
|
|
39
39
|
command -v node >/dev/null 2>&1 || exit 0
|
|
40
40
|
|
|
41
|
-
HQ_ROOT="$HQ_ROOT" SESSION_ID="$SESSION_ID"
|
|
41
|
+
HQ_ROOT="$HQ_ROOT" SESSION_ID="$SESSION_ID" SESSION_CWD="$SESSION_CWD" \
|
|
42
|
+
MSYS2_ARG_CONV_EXCL="*" node - "$COMMAND" <<'JS'
|
|
42
43
|
const fs = require("fs");
|
|
43
44
|
const path = require("path");
|
|
44
45
|
|
|
45
46
|
const hq = process.env.HQ_ROOT || "";
|
|
46
47
|
const sid = process.env.SESSION_ID || "default";
|
|
47
|
-
let command = (process.
|
|
48
|
+
let command = (process.argv[2] || "").trim().replace(/^\/+/, "");
|
|
48
49
|
|
|
49
50
|
const key = sid.replace(/[^A-Za-z0-9._-]/g, "_") || "default";
|
|
50
51
|
const state = path.join(hq, ".claude", "state");
|
|
@@ -133,12 +134,12 @@ if (project) {
|
|
|
133
134
|
if (matchesProject) {
|
|
134
135
|
const st = p.state || "";
|
|
135
136
|
if (st === "IN_PROGRESS") {
|
|
136
|
-
emoji = "
|
|
137
|
+
emoji = "\u25b6\ufe0f";
|
|
137
138
|
} else if (st === "COMPLETED") {
|
|
138
139
|
const ts = p.updatedAt || p.updated_at || "";
|
|
139
140
|
const t = new Date(ts);
|
|
140
141
|
if (!isNaN(t.getTime()) && (Date.now() - t.getTime()) / 1000 < 86400) {
|
|
141
|
-
emoji = "
|
|
142
|
+
emoji = "\u2705"; // only recent completions
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
break;
|
|
@@ -171,15 +172,15 @@ if (project) {
|
|
|
171
172
|
// while /handoff runs; only the model can know when it finished, so it sets 📤.
|
|
172
173
|
// Both are distinct from ✅ "the work shipped" and 🤝 "a person owns it now".
|
|
173
174
|
const MODES = [
|
|
174
|
-
[/^(brainstorm|idea|dream|discover|strategize)$/, "
|
|
175
|
-
[/^(plan|prd|deep-plan|storyboard|architect|review-plan|codebase-design)$/, "
|
|
176
|
-
[/^(run-project|execute-task|ship|land|land-batch|run-pipeline|orchestrate|tdd|deploy)$/, "
|
|
177
|
-
[/^(review|code-review|security-review)$/, "
|
|
178
|
-
[/^(quality-gate|smoke|verify|diagnose|investigate)$/, "
|
|
179
|
-
[/^(handoff|checkpoint-handoff|retro)$/, "
|
|
180
|
-
[/^(delegate|new-hire|new-agent|promote)$/, "
|
|
181
|
-
[/^(dm|hq-slack|meeting-notes|work-broadcast|signals|hq-share)$/, "
|
|
182
|
-
[/^(schedule|job|loop)$/, "
|
|
175
|
+
[/^(brainstorm|idea|dream|discover|strategize)$/, "\u{1f4ad}"],
|
|
176
|
+
[/^(plan|prd|deep-plan|storyboard|architect|review-plan|codebase-design)$/, "\u{1f4d0}"],
|
|
177
|
+
[/^(run-project|execute-task|ship|land|land-batch|run-pipeline|orchestrate|tdd|deploy)$/, "\u{26a1}"],
|
|
178
|
+
[/^(review|code-review|security-review)$/, "\u{1f440}"],
|
|
179
|
+
[/^(quality-gate|smoke|verify|diagnose|investigate)$/, "\u{1f9ea}"],
|
|
180
|
+
[/^(handoff|checkpoint-handoff|retro)$/, "\u{1f4dd}"],
|
|
181
|
+
[/^(delegate|new-hire|new-agent|promote)$/, "\u{1f91d}"],
|
|
182
|
+
[/^(dm|hq-slack|meeting-notes|work-broadcast|signals|hq-share)$/, "\u{1f4ac}"],
|
|
183
|
+
[/^(schedule|job|loop)$/, "\u{1f501}"],
|
|
183
184
|
];
|
|
184
185
|
|
|
185
186
|
const modeFor = (word) => {
|
|
@@ -192,7 +193,7 @@ if (!command) command = "chat";
|
|
|
192
193
|
// Precedence: a finished or running project outranks the command word — "it
|
|
193
194
|
// shipped" and "it is executing right now" beat "you typed /plan an hour ago".
|
|
194
195
|
// `emoji` is set above from orchestrator state (✅ recent completion, ▶️ running).
|
|
195
|
-
const mode = emoji === "
|
|
196
|
+
const mode = emoji === "\u25b6\ufe0f" ? "\u{26a1}" : (emoji || modeFor(command));
|
|
196
197
|
|
|
197
198
|
// --- company ---
|
|
198
199
|
// Always the first text token. Long slugs get an explicit short form from the
|
|
@@ -301,5 +302,7 @@ let title = compose(parts);
|
|
|
301
302
|
if (title.length > MAX && parts.length > 1) title = compose([parts[parts.length - 1]]);
|
|
302
303
|
if (title.length > MAX) title = title.slice(0, MAX - 1).replace(/\s+$/, "") + "…";
|
|
303
304
|
|
|
304
|
-
|
|
305
|
+
// The script is also run under Git Bash with LC_ALL=C from its caller. Write
|
|
306
|
+
// the protocol bytes explicitly so the host locale cannot replace UTF-8 glyphs.
|
|
307
|
+
process.stdout.write(Buffer.from(`${title}\n`, "utf8"));
|
|
305
308
|
JS
|
|
@@ -11008,6 +11008,38 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
11008
11008
|
readonly hidden: false;
|
|
11009
11009
|
}];
|
|
11010
11010
|
readonly subcommands: readonly [];
|
|
11011
|
+
}, {
|
|
11012
|
+
readonly name: "reap";
|
|
11013
|
+
readonly description: "Classify local lanes whose session senior has ended; report only by default";
|
|
11014
|
+
readonly aliases: readonly ["_reap"];
|
|
11015
|
+
readonly hidden: false;
|
|
11016
|
+
readonly helpOption: true;
|
|
11017
|
+
readonly helpCommand: false;
|
|
11018
|
+
readonly usage: "[options]";
|
|
11019
|
+
readonly helpText: readonly [];
|
|
11020
|
+
readonly arguments: readonly [];
|
|
11021
|
+
readonly options: readonly [{
|
|
11022
|
+
readonly flags: "--min-age <minutes>";
|
|
11023
|
+
readonly description: "Minimum lane age in minutes (default: 120)";
|
|
11024
|
+
readonly hidden: false;
|
|
11025
|
+
}, {
|
|
11026
|
+
readonly flags: "--apply-stale-state";
|
|
11027
|
+
readonly description: "Remove stale worker and watcher pid markers";
|
|
11028
|
+
readonly hidden: false;
|
|
11029
|
+
}, {
|
|
11030
|
+
readonly flags: "--stop";
|
|
11031
|
+
readonly description: "Stop candidate process trees when the hq-flags stop key is enabled";
|
|
11032
|
+
readonly hidden: false;
|
|
11033
|
+
}, {
|
|
11034
|
+
readonly flags: "--reason <text>";
|
|
11035
|
+
readonly description: "Required audit reason when --stop is set";
|
|
11036
|
+
readonly hidden: false;
|
|
11037
|
+
}, {
|
|
11038
|
+
readonly flags: "--json";
|
|
11039
|
+
readonly description: "Print machine-readable JSON (default)";
|
|
11040
|
+
readonly hidden: false;
|
|
11041
|
+
}];
|
|
11042
|
+
readonly subcommands: readonly [];
|
|
11011
11043
|
}, {
|
|
11012
11044
|
readonly name: "checks";
|
|
11013
11045
|
readonly description: "Evaluate tracker and lane health checks";
|
|
@@ -11618,6 +11650,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
11618
11650
|
readonly flags: "--worker <name>";
|
|
11619
11651
|
readonly description: "HQ worker or reusable profile id";
|
|
11620
11652
|
readonly hidden: false;
|
|
11653
|
+
}, {
|
|
11654
|
+
readonly flags: "--fresh-thread";
|
|
11655
|
+
readonly description: "Skip worker continuity and start a cold provider session";
|
|
11656
|
+
readonly hidden: false;
|
|
11621
11657
|
}, {
|
|
11622
11658
|
readonly flags: "--provider <name>";
|
|
11623
11659
|
readonly description: "claude | codex | grok";
|
|
@@ -11950,6 +11986,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
11950
11986
|
readonly flags: "--senior <ref>";
|
|
11951
11987
|
readonly description: "Filter by senior ref";
|
|
11952
11988
|
readonly hidden: false;
|
|
11989
|
+
}, {
|
|
11990
|
+
readonly flags: "--worker <id>";
|
|
11991
|
+
readonly description: "Filter by worker id";
|
|
11992
|
+
readonly hidden: false;
|
|
11953
11993
|
}, {
|
|
11954
11994
|
readonly flags: "--lane <id>";
|
|
11955
11995
|
readonly description: "Filter by lane id (repeatable)";
|
|
@@ -13374,6 +13374,47 @@ export const COMMAND_CATALOG = [
|
|
|
13374
13374
|
],
|
|
13375
13375
|
"subcommands": []
|
|
13376
13376
|
},
|
|
13377
|
+
{
|
|
13378
|
+
"name": "reap",
|
|
13379
|
+
"description": "Classify local lanes whose session senior has ended; report only by default",
|
|
13380
|
+
"aliases": [
|
|
13381
|
+
"_reap"
|
|
13382
|
+
],
|
|
13383
|
+
"hidden": false,
|
|
13384
|
+
"helpOption": true,
|
|
13385
|
+
"helpCommand": false,
|
|
13386
|
+
"usage": "[options]",
|
|
13387
|
+
"helpText": [],
|
|
13388
|
+
"arguments": [],
|
|
13389
|
+
"options": [
|
|
13390
|
+
{
|
|
13391
|
+
"flags": "--min-age <minutes>",
|
|
13392
|
+
"description": "Minimum lane age in minutes (default: 120)",
|
|
13393
|
+
"hidden": false
|
|
13394
|
+
},
|
|
13395
|
+
{
|
|
13396
|
+
"flags": "--apply-stale-state",
|
|
13397
|
+
"description": "Remove stale worker and watcher pid markers",
|
|
13398
|
+
"hidden": false
|
|
13399
|
+
},
|
|
13400
|
+
{
|
|
13401
|
+
"flags": "--stop",
|
|
13402
|
+
"description": "Stop candidate process trees when the hq-flags stop key is enabled",
|
|
13403
|
+
"hidden": false
|
|
13404
|
+
},
|
|
13405
|
+
{
|
|
13406
|
+
"flags": "--reason <text>",
|
|
13407
|
+
"description": "Required audit reason when --stop is set",
|
|
13408
|
+
"hidden": false
|
|
13409
|
+
},
|
|
13410
|
+
{
|
|
13411
|
+
"flags": "--json",
|
|
13412
|
+
"description": "Print machine-readable JSON (default)",
|
|
13413
|
+
"hidden": false
|
|
13414
|
+
}
|
|
13415
|
+
],
|
|
13416
|
+
"subcommands": []
|
|
13417
|
+
},
|
|
13377
13418
|
{
|
|
13378
13419
|
"name": "checks",
|
|
13379
13420
|
"description": "Evaluate tracker and lane health checks",
|
|
@@ -14116,6 +14157,11 @@ export const COMMAND_CATALOG = [
|
|
|
14116
14157
|
"description": "HQ worker or reusable profile id",
|
|
14117
14158
|
"hidden": false
|
|
14118
14159
|
},
|
|
14160
|
+
{
|
|
14161
|
+
"flags": "--fresh-thread",
|
|
14162
|
+
"description": "Skip worker continuity and start a cold provider session",
|
|
14163
|
+
"hidden": false
|
|
14164
|
+
},
|
|
14119
14165
|
{
|
|
14120
14166
|
"flags": "--provider <name>",
|
|
14121
14167
|
"description": "claude | codex | grok",
|
|
@@ -14526,6 +14572,11 @@ export const COMMAND_CATALOG = [
|
|
|
14526
14572
|
"description": "Filter by senior ref",
|
|
14527
14573
|
"hidden": false
|
|
14528
14574
|
},
|
|
14575
|
+
{
|
|
14576
|
+
"flags": "--worker <id>",
|
|
14577
|
+
"description": "Filter by worker id",
|
|
14578
|
+
"hidden": false
|
|
14579
|
+
},
|
|
14529
14580
|
{
|
|
14530
14581
|
"flags": "--lane <id>",
|
|
14531
14582
|
"description": "Filter by lane id (repeatable)",
|
|
@@ -11,6 +11,8 @@ import { commandExists, delegateDiagnosticPath, isRecord, jsonAt, jqFallback, jq
|
|
|
11
11
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
12
12
|
import { ACL_SOURCE_DISPLAY_LABELS } from "../utils/files-browse-acl.js";
|
|
13
13
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
14
|
+
import { resolveWindowsBash } from "../utils/windows-bash.js";
|
|
15
|
+
import { createShellCompatibleJqInvoker } from "../utils/jq-windows-compat.js";
|
|
14
16
|
const FLAG = "core-native-delegate";
|
|
15
17
|
const ASSETS = {
|
|
16
18
|
bundle: "core/scripts/hq-delegate-bundle.sh",
|
|
@@ -367,6 +369,13 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
367
369
|
function runHq(args, context, stdout = "capture") {
|
|
368
370
|
return subprocess("hq", args, context.cwd, { stdout });
|
|
369
371
|
}
|
|
372
|
+
const runJq = createShellCompatibleJqInvoker((args, input, cwd, options) => subprocess("jq", args, cwd, { input, ...options }), process.platform, process.env, (cwd) => {
|
|
373
|
+
const bash = resolveWindowsBash(process.env).executable;
|
|
374
|
+
if (!bash)
|
|
375
|
+
return undefined;
|
|
376
|
+
const result = subprocess(bash, ["-c", "printf '%s' \"$OSTYPE\""], cwd, { stderr: "ignore" });
|
|
377
|
+
return result.code === 0 ? result.stdout?.trim() : undefined;
|
|
378
|
+
});
|
|
370
379
|
function commandSubstitutionBytes(value) {
|
|
371
380
|
const bytes = value.stdoutBytes ?? Buffer.from(value.stdout ?? "");
|
|
372
381
|
let end = bytes.length;
|
|
@@ -377,11 +386,15 @@ function commandSubstitutionBytes(value) {
|
|
|
377
386
|
function emitJqParseDiagnostic(input, args, context) {
|
|
378
387
|
if (!commandExists("jq", context.cwd))
|
|
379
388
|
return;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
389
|
+
// cmd.exe cannot pass CR, LF, or NUL inside an argument. jq treats line
|
|
390
|
+
// breaks between tokens as whitespace, and these fixed filters contain no
|
|
391
|
+
// multiline string literals, so send the program as one argument on Windows.
|
|
392
|
+
const jqArgs = [...args];
|
|
393
|
+
const programIndex = jqArgs.length - 1;
|
|
394
|
+
if (programIndex >= 0)
|
|
395
|
+
jqArgs[programIndex] = (jqArgs[programIndex] ?? "").replace(/[\r\n\0]+/gu, " ");
|
|
396
|
+
const result = runJq(jqArgs, input, context.cwd, { stdout: "ignore", stderr: "pipe" });
|
|
397
|
+
const diagnostic = result.stderrBytes ?? Buffer.from(result.stderr ?? "");
|
|
385
398
|
if (diagnostic.length)
|
|
386
399
|
err(diagnostic);
|
|
387
400
|
}
|
|
@@ -13,6 +13,20 @@ export function registerCoreDeriveTriggerFactsCommand(core) {
|
|
|
13
13
|
.helpOption(false)
|
|
14
14
|
.argument("[args...]")
|
|
15
15
|
.action(async (args = [], _options, commandContext) => {
|
|
16
|
+
if (process.env.HQ_CORE_STDIN_CLOSED === "1") {
|
|
17
|
+
const root = liveRoot();
|
|
18
|
+
const result = runBundledScript({
|
|
19
|
+
asset: DERIVE_TRIGGER_FACTS_ASSET,
|
|
20
|
+
args: commandContext.args.length ? commandContext.args : args,
|
|
21
|
+
cwd: root,
|
|
22
|
+
hqRoot: root,
|
|
23
|
+
msysScriptPath: process.platform === "win32",
|
|
24
|
+
closeStdin: true,
|
|
25
|
+
});
|
|
26
|
+
if (result.code !== 0)
|
|
27
|
+
process.exit(result.code);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
16
30
|
if (!await coreNativeHookHelpersEnabled()) {
|
|
17
31
|
const root = liveRoot();
|
|
18
32
|
const stdin = process.platform === "win32" ? await readStdin() : undefined;
|
|
@@ -20,4 +20,5 @@ export declare function commandOnPath(command: string, env: Record<string, strin
|
|
|
20
20
|
/** Run the native handler contract, retaining exact bytes for differential tests. */
|
|
21
21
|
export declare function runDeriveTriggerFacts(options: DeriveTriggerRunOptions): DeriveTriggerRunResult;
|
|
22
22
|
export declare function runDeriveTriggerFactsFast(argv: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined): number;
|
|
23
|
+
export declare function runDeriveTriggerFactsFastResult(argv: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined): DeriveTriggerRunResult;
|
|
23
24
|
//# sourceMappingURL=core-derive-trigger-facts-native.d.ts.map
|
|
@@ -583,11 +583,30 @@ export function runDeriveTriggerFacts(options) {
|
|
|
583
583
|
}
|
|
584
584
|
}
|
|
585
585
|
export function runDeriveTriggerFactsFast(argv, stdin, cwd, hqRoot) {
|
|
586
|
-
const result =
|
|
586
|
+
const result = runDeriveTriggerFactsFastResult(argv, stdin, cwd, hqRoot);
|
|
587
587
|
if (result.stdout.length)
|
|
588
588
|
process.stdout.write(result.stdout);
|
|
589
589
|
if (result.stderr.length)
|
|
590
590
|
process.stderr.write(result.stderr);
|
|
591
591
|
return result.code;
|
|
592
592
|
}
|
|
593
|
+
export function runDeriveTriggerFactsFastResult(argv, stdin, cwd, hqRoot) {
|
|
594
|
+
if (process.env.HQ_CORE_STDIN_CLOSED === "1") {
|
|
595
|
+
const result = runBundledScript({
|
|
596
|
+
asset: DERIVE_TRIGGER_FACTS_ASSET,
|
|
597
|
+
args: [...argv],
|
|
598
|
+
cwd,
|
|
599
|
+
hqRoot: hqRoot ?? cwd,
|
|
600
|
+
msysScriptPath: process.platform === "win32",
|
|
601
|
+
closeStdin: true,
|
|
602
|
+
captureOutput: true,
|
|
603
|
+
});
|
|
604
|
+
return {
|
|
605
|
+
code: result.code,
|
|
606
|
+
stdout: result.stdout ?? Buffer.alloc(0),
|
|
607
|
+
stderr: result.stderr ?? Buffer.alloc(0),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
return runDeriveTriggerFacts({ argv, stdin, cwd, hqRoot: hqRoot ?? cwd });
|
|
611
|
+
}
|
|
593
612
|
//# sourceMappingURL=core-derive-trigger-facts-native.js.map
|
|
@@ -119,6 +119,7 @@ export interface RunDoctorResult {
|
|
|
119
119
|
* read; all other default tiers remain local unless explicitly opted in.
|
|
120
120
|
*/
|
|
121
121
|
export declare function runDoctor(options?: RunDoctorOptions): Promise<RunDoctorResult>;
|
|
122
|
+
export declare function createClientHealthReasonCodeCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
|
|
122
123
|
export declare function createClientHealthCheckStatusCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
|
|
123
124
|
/** Register the top-level `hq doctor` command so it appears in `hq --help`. */
|
|
124
125
|
export declare function registerDoctorCommand(program: Command): void;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -207,7 +207,14 @@ async function resolveClientHealthReasonCodesCheckIds(families, resolveForCompan
|
|
|
207
207
|
continue;
|
|
208
208
|
}
|
|
209
209
|
const scoped = /^sync\.(?:journal|manifest)\.(.+)$/.exec(result.checkId);
|
|
210
|
-
const
|
|
210
|
+
const companyUidScope = scoped
|
|
211
|
+
? /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(scoped[1])
|
|
212
|
+
: null;
|
|
213
|
+
const companySlug = companyUidScope
|
|
214
|
+
? `company:${companyUidScope[1]}`
|
|
215
|
+
: scoped && isCompanyJournalSlug(scoped[1])
|
|
216
|
+
? scoped[1]
|
|
217
|
+
: null;
|
|
211
218
|
const checkIds = checksByCompany.get(companySlug) ?? [];
|
|
212
219
|
checkIds.push(result.checkId);
|
|
213
220
|
checksByCompany.set(companySlug, checkIds);
|
|
@@ -254,12 +261,18 @@ async function resolveClientHealthCheckStatusCheckIds(families, resolveForCompan
|
|
|
254
261
|
}
|
|
255
262
|
return enabledChecks;
|
|
256
263
|
}
|
|
257
|
-
function createClientHealthReasonCodeCompanyResolver(hqRoot) {
|
|
264
|
+
export function createClientHealthReasonCodeCompanyResolver(hqRoot) {
|
|
258
265
|
const companies = readCompaniesManifestMap(hqRoot);
|
|
259
266
|
return async (companySlug) => {
|
|
260
267
|
if (companySlug === null) {
|
|
261
268
|
return isActiveCompanyClientHealthReasonCodesEnabled();
|
|
262
269
|
}
|
|
270
|
+
const scopedCompanyUid = /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(companySlug);
|
|
271
|
+
if (scopedCompanyUid) {
|
|
272
|
+
return clientHealthReasonCodesEnabledForCompany({
|
|
273
|
+
companyUid: scopedCompanyUid[1],
|
|
274
|
+
});
|
|
275
|
+
}
|
|
263
276
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(companySlug)) {
|
|
264
277
|
return false;
|
|
265
278
|
}
|
|
@@ -2,12 +2,15 @@ import type { Command } from "commander";
|
|
|
2
2
|
import { type TrackerCheckTeam } from "../lib/lanes/tracker-checks.js";
|
|
3
3
|
import { type TrackerKind, type TrackerRecord } from "../lib/lanes/tracker-store.js";
|
|
4
4
|
import type { LaneRecord, LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
|
|
5
|
+
import { type ReapDeps } from "../lib/lanes/reaper.js";
|
|
5
6
|
export interface LaneChecksDeps {
|
|
6
7
|
hqRoot: () => string;
|
|
7
8
|
resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
|
|
8
9
|
now: () => Date;
|
|
9
10
|
stopLane?: (laneId: string, reason: string) => Promise<void>;
|
|
10
11
|
sessionState?: (root: string, sessionId: string, now: Date) => string | null;
|
|
12
|
+
stopEnabled?: (lane: LaneRecord) => Promise<boolean> | boolean;
|
|
13
|
+
reaperDeps?: Partial<ReapDeps>;
|
|
11
14
|
}
|
|
12
15
|
type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
|
|
13
16
|
export interface ProjectMapping {
|
|
@@ -13,6 +13,7 @@ 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
15
|
import { findOrphanedBareIdDropboxes } from "../lib/lanes/orphaned-dropbox.js";
|
|
16
|
+
import { parseReapMinAge, reapLocalLanes } from "../lib/lanes/reaper.js";
|
|
16
17
|
export function mappingIncludesStory(mapping, storyId) {
|
|
17
18
|
return (mapping.storyPrefix === undefined || storyId.startsWith(mapping.storyPrefix))
|
|
18
19
|
&& (mapping.excludePrefix === undefined || !storyId.startsWith(mapping.excludePrefix));
|
|
@@ -905,6 +906,39 @@ async function orphanReport(root, company, now, minAgeMinutes, userIdleHours, de
|
|
|
905
906
|
output("orphans.stop", stopped);
|
|
906
907
|
}
|
|
907
908
|
export function registerLaneChecksCommand(parent, deps, wrap) {
|
|
909
|
+
parent.command("reap")
|
|
910
|
+
// CL-5's SessionStart hook launches this alias detached and does not wait for it.
|
|
911
|
+
.alias("_reap")
|
|
912
|
+
.description("Classify local lanes whose session senior has ended; report only by default")
|
|
913
|
+
.option("--min-age <minutes>", "Minimum lane age in minutes (default: 120)")
|
|
914
|
+
.option("--apply-stale-state", "Remove stale worker and watcher pid markers")
|
|
915
|
+
.option("--stop", "Stop candidate process trees when the hq-flags stop key is enabled")
|
|
916
|
+
.option("--reason <text>", "Required audit reason when --stop is set")
|
|
917
|
+
.option("--json", "Print machine-readable JSON (default)")
|
|
918
|
+
.action(wrap(async (options) => {
|
|
919
|
+
const reason = options.reason?.trim();
|
|
920
|
+
if (options.stop && !reason)
|
|
921
|
+
fail("lane_reap_reason_required", "--stop requires a non-empty --reason", {});
|
|
922
|
+
if (!options.stop && options.reason !== undefined)
|
|
923
|
+
fail("lane_reap_reason_requires_stop", "--reason can only be used with --stop", {});
|
|
924
|
+
if (reason)
|
|
925
|
+
assertNoSecretText("stop reason", reason);
|
|
926
|
+
const minAgeMinutes = parseReapMinAge(options.minAge);
|
|
927
|
+
const report = await reapLocalLanes(deps.hqRoot(), {
|
|
928
|
+
minAgeMinutes,
|
|
929
|
+
stop: options.stop === true,
|
|
930
|
+
applyStaleState: options.applyStaleState === true,
|
|
931
|
+
reason: reason ?? "Orphaned lane: owning session is confirmed ended",
|
|
932
|
+
json: true,
|
|
933
|
+
}, {
|
|
934
|
+
...deps.reaperDeps,
|
|
935
|
+
now: deps.now,
|
|
936
|
+
sessionState: deps.sessionState,
|
|
937
|
+
stopEnabled: deps.stopEnabled,
|
|
938
|
+
stopLane: deps.stopLane,
|
|
939
|
+
});
|
|
940
|
+
console.log(JSON.stringify(report, null, 2));
|
|
941
|
+
}));
|
|
908
942
|
parent.command("checks")
|
|
909
943
|
.description("Evaluate tracker and lane health checks")
|
|
910
944
|
.option("--company <slug>", "Select the company by slug")
|
|
@@ -7,16 +7,11 @@ export interface SessionStartOptions {
|
|
|
7
7
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
8
8
|
stderr?: Pick<NodeJS.WriteStream, "write">;
|
|
9
9
|
resolveRoot?: (env: NodeJS.ProcessEnv, cwd?: string) => string;
|
|
10
|
-
startSweep?: (root: string, env: NodeJS.ProcessEnv
|
|
11
|
-
}
|
|
12
|
-
interface SessionStartContext {
|
|
13
|
-
sessionId?: string;
|
|
14
|
-
companySlug?: string;
|
|
10
|
+
startSweep?: (root: string, env: NodeJS.ProcessEnv) => void;
|
|
15
11
|
}
|
|
16
12
|
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
17
|
-
export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn
|
|
13
|
+
export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn): void;
|
|
18
14
|
/** SessionStart-compatible, fail-open conduct bootstrap. */
|
|
19
15
|
export declare function runLaneSessionStart(options?: SessionStartOptions): Promise<void>;
|
|
20
16
|
export declare function registerLaneSessionStartCommand(lanes: Command): void;
|
|
21
|
-
export {};
|
|
22
17
|
//# sourceMappingURL=lanes-session-start.d.ts.map
|
|
@@ -27,22 +27,6 @@ function sessionWasDisabled(root, env, payloadSessionId) {
|
|
|
27
27
|
return /^\s*(?:conduct_mode|conduct_enabled|conduct_default):\s*(?:off|false|no|0)\s*(?:#.*)?$/mi.test(text) ||
|
|
28
28
|
/^\s*conduct_engine:\s*(?:""|''|#.*)?\s*$/mi.test(text);
|
|
29
29
|
}
|
|
30
|
-
function sessionStartContext(root, env, payloadSessionId) {
|
|
31
|
-
const sessionId = payloadSessionId || env.HQ_SESSION_ID;
|
|
32
|
-
let companySlug = "";
|
|
33
|
-
if (sessionId && /^[A-Za-z0-9_-]+$/.test(sessionId)) {
|
|
34
|
-
const meta = path.join(root, "workspace", "sessions", sessionId, "meta.yaml");
|
|
35
|
-
if (fs.existsSync(meta)) {
|
|
36
|
-
const text = fs.readFileSync(meta, "utf8");
|
|
37
|
-
companySlug = /^\s*company_slug:\s*["']?([A-Za-z0-9][A-Za-z0-9-]*)["']?\s*(?:#.*)?$/mi.exec(text)?.[1] ?? "";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (!companySlug)
|
|
41
|
-
companySlug = env.HQ_COMPANY_SLUG?.trim() ?? "";
|
|
42
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(companySlug))
|
|
43
|
-
companySlug = "";
|
|
44
|
-
return { ...(sessionId ? { sessionId } : {}), ...(companySlug ? { companySlug } : {}) };
|
|
45
|
-
}
|
|
46
30
|
function extractConductorBlock(core) {
|
|
47
31
|
const start = core.indexOf("<!-- inject:start -->");
|
|
48
32
|
const end = core.indexOf("<!-- inject:end -->");
|
|
@@ -52,15 +36,12 @@ function extractConductorBlock(core) {
|
|
|
52
36
|
}
|
|
53
37
|
const ORPHAN_SWEEP_LOCK_TTL_MS = 15 * 60_000;
|
|
54
38
|
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
55
|
-
export function startSessionStartOrphanSweep(root, env = process.env, spawnDetached = spawn
|
|
56
|
-
// The orphan command resolves company membership when --company is absent;
|
|
57
|
-
// skip here if this session has no tenant context rather than risk ambiguity.
|
|
58
|
-
if (!context.companySlug)
|
|
59
|
-
return;
|
|
39
|
+
export function startSessionStartOrphanSweep(root, env = process.env, spawnDetached = spawn) {
|
|
60
40
|
const stateRoot = env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
41
|
+
const host = os.hostname().replace(/[^A-Za-z0-9._-]/g, "_") || "unknown-host";
|
|
42
|
+
const stateDir = path.join(stateRoot, "hq-cli", "lanes-session-start", host);
|
|
43
|
+
const lockPath = path.join(stateDir, "orphan-sweep.lock");
|
|
44
|
+
const reportDir = path.join(stateRoot, "hq-cli", "lanes-orphan-reports", host);
|
|
64
45
|
const ownerToken = randomUUID();
|
|
65
46
|
try {
|
|
66
47
|
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
@@ -108,17 +89,17 @@ export function startSessionStartOrphanSweep(root, env = process.env, spawnDetac
|
|
|
108
89
|
}
|
|
109
90
|
const wrapper = [
|
|
110
91
|
"const cp=require('node:child_process'),fs=require('node:fs'),path=require('node:path');",
|
|
111
|
-
"const [cli,lock,root,token,report
|
|
92
|
+
"const [cli,lock,root,token,report]=process.argv.slice(1);",
|
|
112
93
|
"const cleanup=()=>{try{const lease=JSON.parse(fs.readFileSync(lock,'utf8'));if(lease.owner_token===token)fs.rmSync(lock,{force:true})}catch{}};",
|
|
113
94
|
"const start=()=>{let lease;try{lease=JSON.parse(fs.readFileSync(lock,'utf8'))}catch{}if(lease?.owner_token!==token){if(Date.now()<Number(lease?.started_at||0)+1000)return setTimeout(start,10);process.exit(0)}",
|
|
114
95
|
"try{fs.writeFileSync(lock,JSON.stringify({...lease,pid:process.pid}))}catch{}",
|
|
115
96
|
"const tmp=report+'.tmp';let output;try{output=fs.createWriteStream(tmp,{flags:'wx',mode:0o600})}catch{cleanup();process.exit(0)}",
|
|
116
|
-
"const child=cp.spawn(process.execPath,[cli,'lanes','
|
|
97
|
+
"const child=cp.spawn(process.execPath,[cli,'lanes','_reap','--json'],{stdio:['ignore','pipe','ignore'],env:{...process.env,HQ_ROOT:root,HQ_NO_UPDATE_CHECK:'1'}});",
|
|
117
98
|
"child.stdout.pipe(output);child.on('error',()=>{output.destroy();try{fs.rmSync(tmp,{force:true})}catch{}cleanup()});",
|
|
118
99
|
"child.on('exit',(code)=>output.end(()=>{try{if(code===0)fs.renameSync(tmp,report);else fs.rmSync(tmp,{force:true})}catch{}cleanup()}));};start();",
|
|
119
100
|
].join("");
|
|
120
101
|
const reportPath = path.join(reportDir, `orphans-${startedAt}-${ownerToken}.json`);
|
|
121
|
-
const launcher = spawnDetached(process.execPath, ["-e", wrapper, cli, lockPath, root, ownerToken, reportPath
|
|
102
|
+
const launcher = spawnDetached(process.execPath, ["-e", wrapper, cli, lockPath, root, ownerToken, reportPath], {
|
|
122
103
|
detached: true,
|
|
123
104
|
stdio: "ignore",
|
|
124
105
|
env: { ...env, HQ_ROOT: root, HQ_NO_UPDATE_CHECK: "1" },
|
|
@@ -185,8 +166,7 @@ export async function runLaneSessionStart(options = {}) {
|
|
|
185
166
|
});
|
|
186
167
|
if (!flagEnabled)
|
|
187
168
|
return;
|
|
188
|
-
|
|
189
|
-
(options.startSweep ?? ((r, e, c) => startSessionStartOrphanSweep(r, e, spawn, c)))(root, env, context);
|
|
169
|
+
(options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn)))(root, env);
|
|
190
170
|
if (sessionWasDisabled(root, env, payloadSessionId))
|
|
191
171
|
return;
|
|
192
172
|
let enabled = enabledFromSettings(root);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
1
|
+
import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneAdmissionLock, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
2
2
|
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
3
3
|
import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
4
4
|
import type { CpuBusySource } from "../lib/lanes/throttle.js";
|
|
@@ -313,6 +313,7 @@ export interface Ctx {
|
|
|
313
313
|
storyLedgerEnabled: () => boolean;
|
|
314
314
|
isSsmOwnerDispatch: () => boolean;
|
|
315
315
|
withGrantHireLocks: typeof withLaneHireLocks;
|
|
316
|
+
withAdmissionLock: typeof withLaneAdmissionLock;
|
|
316
317
|
mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
317
318
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
318
319
|
}
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/
|
|
|
17
17
|
export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
18
18
|
import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
19
19
|
import { emitLaneDecisionEvents } from "../lib/lanes/mesh-events.js";
|
|
20
|
-
import { MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
20
|
+
import { MAX_LANE_DEPTH, withLaneAdmissionLock, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
21
21
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
22
22
|
import { type RunCompactionFn } from "../lib/lanes/compaction.js";
|
|
23
23
|
import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
|
|
@@ -106,6 +106,8 @@ export interface LanesCommandDeps {
|
|
|
106
106
|
isSsmOwnerDispatch?: () => boolean;
|
|
107
107
|
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|
|
108
108
|
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
109
|
+
/** Test seam proving create admission encloses the worker busy check. */
|
|
110
|
+
withAdmissionLock?: typeof withLaneAdmissionLock;
|
|
109
111
|
/** Test seam for the review-before-merge registry flag. */
|
|
110
112
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
111
113
|
/** Test seam for the repository-merge-hold registry flag. */
|