@indigoai-us/hq-cli 5.345.84 → 5.345.86
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 +11 -0
- package/dist/command-catalog.generated.d.ts +41 -0
- package/dist/command-catalog.generated.js +50 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/lanes-checks.js +32 -2
- package/dist/commands/lanes-session-start.d.ts +1 -1
- package/dist/commands/lanes-session-start.js +57 -7
- package/dist/commands/scan.d.ts +23 -0
- package/dist/commands/scan.js +59 -0
- package/dist/lib/lanes/flag-keys.d.ts +8 -0
- package/dist/lib/lanes/flag-keys.js +8 -0
- package/dist/lib/lanes/legacy-pool.d.ts +35 -0
- package/dist/lib/lanes/legacy-pool.js +209 -0
- package/dist/lib/lanes/reaper.d.ts +13 -0
- package/dist/lib/lanes/reaper.js +43 -5
- package/dist/lib/secret-scan/detect.d.ts +27 -0
- package/dist/lib/secret-scan/detect.js +129 -0
- package/dist/lib/secret-scan/gitleaks-rules.generated.d.ts +6 -0
- package/dist/lib/secret-scan/gitleaks-rules.generated.js +3602 -0
- package/dist/lib/secret-scan/go-regex.d.ts +22 -0
- package/dist/lib/secret-scan/go-regex.js +250 -0
- package/dist/lib/secret-scan/rules.d.ts +31 -0
- package/dist/lib/secret-scan/rules.js +84 -0
- package/dist/lib/secret-scan/staged.d.ts +17 -0
- package/dist/lib/secret-scan/staged.js +112 -0
- package/dist/lib/secret-scan/types.d.ts +34 -0
- package/dist/lib/secret-scan/types.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.86] — 2026-10-11
|
|
6
|
+
- New `hq scan secrets`: an offline secret scanner for the pre-commit gate. It
|
|
7
|
+
checks the staged changes (or given files) against the gitleaks default rule
|
|
8
|
+
set, vendored from gitleaks v8.30.1 under its MIT license, plus HQ's own
|
|
9
|
+
token patterns. It needs no network, account or API quota, and reports the
|
|
10
|
+
rule, file and line without printing the value. Exit codes: 0 clean, 1
|
|
11
|
+
possible secret, 2 the scan could not run.
|
|
12
|
+
|
|
13
|
+
## [5.345.85] — 2026-10-11
|
|
14
|
+
- Session-start orphan reports and reap leases are keyed by HQ root, report ended-session waiting lanes, and surface the latest completed orphan count in startup context even after a skipped sweep. `hq lanes reap --legacy` also identifies abandoned legacy conduct pool loops and only stops verified process identities when `--stop --reason` is supplied.
|
|
15
|
+
|
|
5
16
|
## [5.345.84] — 2026-10-11
|
|
6
17
|
- `hq secrets exec` and `hq secrets env` with `HQ_API_KEY` now refuse a
|
|
7
18
|
`--company` (or `HQ_COMPANY_UID`) that the key was not minted for. They
|
|
@@ -5375,6 +5375,43 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5375
5375
|
}];
|
|
5376
5376
|
readonly subcommands: readonly [];
|
|
5377
5377
|
}];
|
|
5378
|
+
}, {
|
|
5379
|
+
readonly name: "scan";
|
|
5380
|
+
readonly description: "Local, offline scanners";
|
|
5381
|
+
readonly aliases: readonly [];
|
|
5382
|
+
readonly hidden: false;
|
|
5383
|
+
readonly helpOption: true;
|
|
5384
|
+
readonly helpCommand: true;
|
|
5385
|
+
readonly usage: "[options] [command]";
|
|
5386
|
+
readonly helpText: readonly [];
|
|
5387
|
+
readonly arguments: readonly [];
|
|
5388
|
+
readonly options: readonly [];
|
|
5389
|
+
readonly subcommands: readonly [{
|
|
5390
|
+
readonly name: "secrets";
|
|
5391
|
+
readonly description: "Scan staged changes (or files) for secrets; exits 1 on a finding";
|
|
5392
|
+
readonly aliases: readonly [];
|
|
5393
|
+
readonly hidden: false;
|
|
5394
|
+
readonly helpOption: true;
|
|
5395
|
+
readonly helpCommand: false;
|
|
5396
|
+
readonly usage: "[options] [files...]";
|
|
5397
|
+
readonly helpText: readonly [];
|
|
5398
|
+
readonly arguments: readonly [{
|
|
5399
|
+
readonly name: "files";
|
|
5400
|
+
readonly required: false;
|
|
5401
|
+
readonly variadic: true;
|
|
5402
|
+
readonly description: "Scan these files instead of the staged changes";
|
|
5403
|
+
}];
|
|
5404
|
+
readonly options: readonly [{
|
|
5405
|
+
readonly flags: "--staged";
|
|
5406
|
+
readonly description: "Scan the staged changes (default when no files are given)";
|
|
5407
|
+
readonly hidden: false;
|
|
5408
|
+
}, {
|
|
5409
|
+
readonly flags: "--json";
|
|
5410
|
+
readonly description: "Print findings as JSON";
|
|
5411
|
+
readonly hidden: false;
|
|
5412
|
+
}];
|
|
5413
|
+
readonly subcommands: readonly [];
|
|
5414
|
+
}];
|
|
5378
5415
|
}, {
|
|
5379
5416
|
readonly name: "reindex";
|
|
5380
5417
|
readonly description: "Surface namespaced skills, materialize legacy knowledge repos, regenerate the workers registry, and trust HQ hooks for Codex, Grok, and Claude Code";
|
|
@@ -11941,6 +11978,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
11941
11978
|
readonly flags: "--stop";
|
|
11942
11979
|
readonly description: "Stop candidate process trees when the hq-flags stop key is enabled";
|
|
11943
11980
|
readonly hidden: false;
|
|
11981
|
+
}, {
|
|
11982
|
+
readonly flags: "--legacy";
|
|
11983
|
+
readonly description: "Include legacy conduct pool loop lanes in the report";
|
|
11984
|
+
readonly hidden: false;
|
|
11944
11985
|
}, {
|
|
11945
11986
|
readonly flags: "--reason <text>";
|
|
11946
11987
|
readonly description: "Required audit reason when --stop is set";
|
|
@@ -6535,6 +6535,51 @@ export const COMMAND_CATALOG = [
|
|
|
6535
6535
|
}
|
|
6536
6536
|
]
|
|
6537
6537
|
},
|
|
6538
|
+
{
|
|
6539
|
+
"name": "scan",
|
|
6540
|
+
"description": "Local, offline scanners",
|
|
6541
|
+
"aliases": [],
|
|
6542
|
+
"hidden": false,
|
|
6543
|
+
"helpOption": true,
|
|
6544
|
+
"helpCommand": true,
|
|
6545
|
+
"usage": "[options] [command]",
|
|
6546
|
+
"helpText": [],
|
|
6547
|
+
"arguments": [],
|
|
6548
|
+
"options": [],
|
|
6549
|
+
"subcommands": [
|
|
6550
|
+
{
|
|
6551
|
+
"name": "secrets",
|
|
6552
|
+
"description": "Scan staged changes (or files) for secrets; exits 1 on a finding",
|
|
6553
|
+
"aliases": [],
|
|
6554
|
+
"hidden": false,
|
|
6555
|
+
"helpOption": true,
|
|
6556
|
+
"helpCommand": false,
|
|
6557
|
+
"usage": "[options] [files...]",
|
|
6558
|
+
"helpText": [],
|
|
6559
|
+
"arguments": [
|
|
6560
|
+
{
|
|
6561
|
+
"name": "files",
|
|
6562
|
+
"required": false,
|
|
6563
|
+
"variadic": true,
|
|
6564
|
+
"description": "Scan these files instead of the staged changes"
|
|
6565
|
+
}
|
|
6566
|
+
],
|
|
6567
|
+
"options": [
|
|
6568
|
+
{
|
|
6569
|
+
"flags": "--staged",
|
|
6570
|
+
"description": "Scan the staged changes (default when no files are given)",
|
|
6571
|
+
"hidden": false
|
|
6572
|
+
},
|
|
6573
|
+
{
|
|
6574
|
+
"flags": "--json",
|
|
6575
|
+
"description": "Print findings as JSON",
|
|
6576
|
+
"hidden": false
|
|
6577
|
+
}
|
|
6578
|
+
],
|
|
6579
|
+
"subcommands": []
|
|
6580
|
+
}
|
|
6581
|
+
]
|
|
6582
|
+
},
|
|
6538
6583
|
{
|
|
6539
6584
|
"name": "reindex",
|
|
6540
6585
|
"description": "Surface namespaced skills, materialize legacy knowledge repos, regenerate the workers registry, and trust HQ hooks for Codex, Grok, and Claude Code",
|
|
@@ -14522,6 +14567,11 @@ export const COMMAND_CATALOG = [
|
|
|
14522
14567
|
"description": "Stop candidate process trees when the hq-flags stop key is enabled",
|
|
14523
14568
|
"hidden": false
|
|
14524
14569
|
},
|
|
14570
|
+
{
|
|
14571
|
+
"flags": "--legacy",
|
|
14572
|
+
"description": "Include legacy conduct pool loop lanes in the report",
|
|
14573
|
+
"hidden": false
|
|
14574
|
+
},
|
|
14525
14575
|
{
|
|
14526
14576
|
"flags": "--reason <text>",
|
|
14527
14577
|
"description": "Required audit reason when --stop is set",
|
|
@@ -358,6 +358,12 @@ export declare const REGISTRATION_PLAN: readonly [{
|
|
|
358
358
|
readonly parent: "program";
|
|
359
359
|
readonly module: "./commands/import-scan.js";
|
|
360
360
|
readonly exportName: "registerImportCommand";
|
|
361
|
+
}, {
|
|
362
|
+
readonly type: "registrar";
|
|
363
|
+
readonly root: "scan";
|
|
364
|
+
readonly parent: "program";
|
|
365
|
+
readonly module: "./commands/scan.js";
|
|
366
|
+
readonly exportName: "registerScanCommand";
|
|
361
367
|
}, {
|
|
362
368
|
readonly type: "registrar";
|
|
363
369
|
readonly root: "reindex";
|
|
@@ -62,6 +62,7 @@ export const REGISTRATION_PLAN = [
|
|
|
62
62
|
{ type: "registrar", root: "signals", parent: "program", module: "./commands/signals.js", exportName: "registerSignalsCommand" },
|
|
63
63
|
{ type: "registrar", root: "integrations", parent: "program", module: "./commands/integrations.js", exportName: "registerIntegrationsCommand" },
|
|
64
64
|
{ type: "registrar", root: "import", parent: "program", module: "./commands/import-scan.js", exportName: "registerImportCommand" },
|
|
65
|
+
{ type: "registrar", root: "scan", parent: "program", module: "./commands/scan.js", exportName: "registerScanCommand" },
|
|
65
66
|
{ type: "registrar", root: "reindex", parent: "program", module: "./commands/reindex.js", exportName: "registerReindexCommand" },
|
|
66
67
|
{ type: "registrar", root: "rescue", parent: "program", module: "./commands/rescue.js", exportName: "registerRescueCommand" },
|
|
67
68
|
{ type: "registrar", root: "mcp", parent: "program", module: "./commands/mcp-status.js", exportName: "registerMcpCommand" },
|
|
@@ -14,6 +14,9 @@ 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
16
|
import { parseReapMinAge, reapLocalLanes } from "../lib/lanes/reaper.js";
|
|
17
|
+
import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
18
|
+
import { LANE_REAP_STOP_FLAG, LANE_ORPHAN_SWEEP_LEGACY_FLAG, LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG, LANE_ORPHAN_SWEEP_WAITING_FLAG } from "../lib/lanes/flag-keys.js";
|
|
19
|
+
import { findLegacyPoolLanes } from "../lib/lanes/legacy-pool.js";
|
|
17
20
|
export function mappingIncludesStory(mapping, storyId) {
|
|
18
21
|
return (mapping.storyPrefix === undefined || storyId.startsWith(mapping.storyPrefix))
|
|
19
22
|
&& (mapping.excludePrefix === undefined || !storyId.startsWith(mapping.excludePrefix));
|
|
@@ -849,6 +852,24 @@ async function orphanReport(root, company, now, minAgeMinutes, userIdleHours, de
|
|
|
849
852
|
assertNoSecretText("stop reason", reason);
|
|
850
853
|
const unscoped = options.unscoped === true;
|
|
851
854
|
const orphans = orphanFindings(root, company, now, minAgeMinutes, deps, undefined, unscoped, userIdleHours);
|
|
855
|
+
const legacyEnabled = await readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_LEGACY_FLAG, { env: process.env, defaultValue: true });
|
|
856
|
+
const legacyOrphans = legacyEnabled
|
|
857
|
+
? findLegacyPoolLanes(root, { ...deps.reaperDeps, sessionState: deps.sessionState, now: () => now }).filter((finding) => unscoped ? !finding.company_slug : !company || finding.company_slug === company.slug)
|
|
858
|
+
: [];
|
|
859
|
+
const legacyRows = legacyOrphans.map((finding) => ({
|
|
860
|
+
lane_id: finding.lane_id,
|
|
861
|
+
senior: { kind: "session", id: finding.session_id },
|
|
862
|
+
senior_state: "ended",
|
|
863
|
+
reason: finding.reason,
|
|
864
|
+
company_source: "legacy_conduct_pool",
|
|
865
|
+
...(finding.company_slug ? { company: finding.company_slug } : {}),
|
|
866
|
+
age_minutes: finding.age_minutes,
|
|
867
|
+
host: { kind: "local", id: "legacy" },
|
|
868
|
+
legacy: true,
|
|
869
|
+
pid: finding.pid,
|
|
870
|
+
run_dir: finding.run_dir,
|
|
871
|
+
stoppable: finding.reason === "legacy_conduct_session_gone",
|
|
872
|
+
}));
|
|
852
873
|
const output = (action, stopped) => {
|
|
853
874
|
if (options.human) {
|
|
854
875
|
const companyText = unscoped ? "unscoped" : company?.slug ?? "unknown";
|
|
@@ -866,7 +887,10 @@ async function orphanReport(root, company, now, minAgeMinutes, userIdleHours, de
|
|
|
866
887
|
`${finding.host.kind}:${finding.host.id}`,
|
|
867
888
|
].join("\t"));
|
|
868
889
|
}
|
|
869
|
-
|
|
890
|
+
for (const finding of legacyOrphans) {
|
|
891
|
+
console.log([finding.lane_id, `session:${finding.session_id}`, "ended", finding.reason, "legacy_conduct_pool", finding.age_minutes, "", "local:legacy"].join("\t"));
|
|
892
|
+
}
|
|
893
|
+
if (orphans.length + legacyOrphans.length === 0)
|
|
870
894
|
console.log("(none)");
|
|
871
895
|
if (stopped !== undefined) {
|
|
872
896
|
console.log("stopped:");
|
|
@@ -884,7 +908,7 @@ async function orphanReport(root, company, now, minAgeMinutes, userIdleHours, de
|
|
|
884
908
|
min_age_minutes: minAgeMinutes,
|
|
885
909
|
user_idle_hours: userIdleHours,
|
|
886
910
|
...(reason === undefined ? {} : { reason }),
|
|
887
|
-
orphans,
|
|
911
|
+
orphans: [...orphans, ...legacyRows],
|
|
888
912
|
...(stopped === undefined ? {} : { stopped }),
|
|
889
913
|
}, null, 2));
|
|
890
914
|
};
|
|
@@ -913,6 +937,7 @@ export function registerLaneChecksCommand(parent, deps, wrap) {
|
|
|
913
937
|
.option("--min-age <minutes>", "Minimum lane age in minutes (default: 120)")
|
|
914
938
|
.option("--apply-stale-state", "Remove stale worker and watcher pid markers")
|
|
915
939
|
.option("--stop", "Stop candidate process trees when the hq-flags stop key is enabled")
|
|
940
|
+
.option("--legacy", "Include legacy conduct pool loop lanes in the report")
|
|
916
941
|
.option("--reason <text>", "Required audit reason when --stop is set")
|
|
917
942
|
.option("--json", "Print machine-readable JSON (default)")
|
|
918
943
|
.action(wrap(async (options) => {
|
|
@@ -928,6 +953,7 @@ export function registerLaneChecksCommand(parent, deps, wrap) {
|
|
|
928
953
|
minAgeMinutes,
|
|
929
954
|
stop: options.stop === true,
|
|
930
955
|
applyStaleState: options.applyStaleState === true,
|
|
956
|
+
legacy: options.legacy === true,
|
|
931
957
|
reason: reason ?? "Orphaned lane: owning session is confirmed ended",
|
|
932
958
|
json: true,
|
|
933
959
|
}, {
|
|
@@ -936,6 +962,10 @@ export function registerLaneChecksCommand(parent, deps, wrap) {
|
|
|
936
962
|
sessionState: deps.sessionState,
|
|
937
963
|
stopEnabled: deps.stopEnabled,
|
|
938
964
|
stopLane: deps.stopLane,
|
|
965
|
+
rootScopedStateEnabled: () => readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG, { env: process.env, defaultValue: true }),
|
|
966
|
+
waitingSeniorEnabled: () => readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_WAITING_FLAG, { env: process.env, defaultValue: true }),
|
|
967
|
+
legacyEnabled: () => readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_LEGACY_FLAG, { env: process.env, defaultValue: true }),
|
|
968
|
+
legacyStopEnabled: () => readLanesRegistryFlagFailOpen(LANE_REAP_STOP_FLAG, { env: process.env, defaultValue: true }),
|
|
939
969
|
});
|
|
940
970
|
console.log(JSON.stringify(report, null, 2));
|
|
941
971
|
}));
|
|
@@ -10,7 +10,7 @@ export interface SessionStartOptions {
|
|
|
10
10
|
startSweep?: (root: string, env: NodeJS.ProcessEnv) => void;
|
|
11
11
|
}
|
|
12
12
|
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
13
|
-
export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn): void;
|
|
13
|
+
export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, rootScoped?: boolean): void;
|
|
14
14
|
/** SessionStart-compatible, fail-open conduct bootstrap. */
|
|
15
15
|
export declare function runLaneSessionStart(options?: SessionStartOptions): Promise<void>;
|
|
16
16
|
export declare function registerLaneSessionStartCommand(lanes: Command): void;
|
|
@@ -2,10 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
7
7
|
import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
8
|
-
import { LANE_CONDUCT_SESSION_START_FLAG } from "../lib/lanes/flag-keys.js";
|
|
8
|
+
import { LANE_CONDUCT_SESSION_START_FLAG, LANE_ORPHAN_SWEEP_CONTEXT_FLAG, LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG } from "../lib/lanes/flag-keys.js";
|
|
9
9
|
const CODEX_DEFAULT_KEYS = ["conduct_engine", "conduct_child_model", "conduct_child_effort", "conduct_default_source"];
|
|
10
10
|
function readSessionMetaValue(text, key) {
|
|
11
11
|
const line = text.split(/\r?\n/).find((entry) => new RegExp(`^\\s*${key}:`).test(entry));
|
|
@@ -115,12 +115,55 @@ function extractConductorBlock(core) {
|
|
|
115
115
|
return core.slice(start + "<!-- inject:start -->".length, end).replace(/^\r?\n/, "").replace(/\r?\n$/, "");
|
|
116
116
|
}
|
|
117
117
|
const ORPHAN_SWEEP_LOCK_TTL_MS = 15 * 60_000;
|
|
118
|
+
function rootKeyFor(root) {
|
|
119
|
+
let resolved = path.resolve(root);
|
|
120
|
+
try {
|
|
121
|
+
resolved = fs.realpathSync(root);
|
|
122
|
+
}
|
|
123
|
+
catch { /* the canonicalized path remains the fallback */ }
|
|
124
|
+
return createHash("sha256").update(resolved).digest("hex").slice(0, 12);
|
|
125
|
+
}
|
|
126
|
+
function readOrphanCount(root, env, rootScoped, onError) {
|
|
127
|
+
try {
|
|
128
|
+
const stateRoot = env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
|
|
129
|
+
const host = os.hostname().replace(/[^A-Za-z0-9._-]/g, "_") || "unknown-host";
|
|
130
|
+
const rootKey = rootKeyFor(root);
|
|
131
|
+
const prefix = rootScoped ? `orphans-${rootKey}-` : "orphans-";
|
|
132
|
+
const dir = path.join(stateRoot, "hq-cli", "lanes-orphan-reports", host);
|
|
133
|
+
const candidates = fs.readdirSync(dir)
|
|
134
|
+
.filter((name) => name.startsWith(prefix) && name.endsWith(".json"))
|
|
135
|
+
.map((name) => ({ name, mtime: fs.statSync(path.join(dir, name)).mtimeMs }))
|
|
136
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
137
|
+
// Reports accumulate, so read newest first and stop at the first one that
|
|
138
|
+
// actually swept; skipped and unreadable reports are passed over.
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
let report;
|
|
141
|
+
try {
|
|
142
|
+
report = JSON.parse(fs.readFileSync(path.join(dir, candidate.name), "utf8"));
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
onError?.(error);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (report.skipped)
|
|
149
|
+
continue;
|
|
150
|
+
return Array.isArray(report.lanes) ? report.lanes.filter((lane) => lane && typeof lane === "object" && lane.orphaned === true).length : 0;
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
onError?.(error);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
118
159
|
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
119
|
-
export function startSessionStartOrphanSweep(root, env = process.env, spawnDetached = spawn) {
|
|
160
|
+
export function startSessionStartOrphanSweep(root, env = process.env, spawnDetached = spawn, rootScoped = true) {
|
|
120
161
|
const stateRoot = env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
|
|
121
162
|
const host = os.hostname().replace(/[^A-Za-z0-9._-]/g, "_") || "unknown-host";
|
|
163
|
+
const rootKey = rootKeyFor(root);
|
|
164
|
+
const rootSuffix = rootScoped ? `-${rootKey}` : "";
|
|
122
165
|
const stateDir = path.join(stateRoot, "hq-cli", "lanes-session-start", host);
|
|
123
|
-
const lockPath = path.join(stateDir,
|
|
166
|
+
const lockPath = path.join(stateDir, `orphan-sweep${rootSuffix}.lock`);
|
|
124
167
|
const reportDir = path.join(stateRoot, "hq-cli", "lanes-orphan-reports", host);
|
|
125
168
|
const ownerToken = randomUUID();
|
|
126
169
|
try {
|
|
@@ -174,11 +217,11 @@ export function startSessionStartOrphanSweep(root, env = process.env, spawnDetac
|
|
|
174
217
|
"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)}",
|
|
175
218
|
"try{fs.writeFileSync(lock,JSON.stringify({...lease,pid:process.pid}))}catch{}",
|
|
176
219
|
"const tmp=report+'.tmp';let output;try{output=fs.createWriteStream(tmp,{flags:'wx',mode:0o600})}catch{cleanup();process.exit(0)}",
|
|
177
|
-
"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'}});",
|
|
220
|
+
"const child=cp.spawn(process.execPath,[cli,'lanes','_reap','--json','--legacy'],{stdio:['ignore','pipe','ignore'],env:{...process.env,HQ_ROOT:root,HQ_NO_UPDATE_CHECK:'1'}});",
|
|
178
221
|
"child.stdout.pipe(output);child.on('error',()=>{output.destroy();try{fs.rmSync(tmp,{force:true})}catch{}cleanup()});",
|
|
179
222
|
"child.on('exit',(code)=>output.end(()=>{try{if(code===0)fs.renameSync(tmp,report);else fs.rmSync(tmp,{force:true})}catch{}cleanup()}));};start();",
|
|
180
223
|
].join("");
|
|
181
|
-
const reportPath = path.join(reportDir, `orphans-${startedAt}-${ownerToken}.json`);
|
|
224
|
+
const reportPath = path.join(reportDir, `orphans-${rootScoped ? `${rootKey}-` : ""}${startedAt}-${ownerToken}.json`);
|
|
182
225
|
const launcher = spawnDetached(process.execPath, ["-e", wrapper, cli, lockPath, root, ownerToken, reportPath], {
|
|
183
226
|
detached: true,
|
|
184
227
|
stdio: "ignore",
|
|
@@ -245,7 +288,14 @@ export async function runLaneSessionStart(options = {}) {
|
|
|
245
288
|
});
|
|
246
289
|
if (!flagEnabled)
|
|
247
290
|
return;
|
|
248
|
-
|
|
291
|
+
const rootScoped = await readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG, { env, defaultValue: true });
|
|
292
|
+
(options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn, rootScoped)))(root, env);
|
|
293
|
+
const contextEnabled = await readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_CONTEXT_FLAG, { env, defaultValue: true });
|
|
294
|
+
if (contextEnabled) {
|
|
295
|
+
const count = readOrphanCount(root, env, rootScoped, (error) => debug(`hq lanes session-start: orphan report read failed (${error instanceof Error ? error.name : "Error"})`));
|
|
296
|
+
if (count > 0)
|
|
297
|
+
stdout.write(`HQ orphaned lanes: ${count}; inspect with hq lanes orphans\n`);
|
|
298
|
+
}
|
|
249
299
|
if (sessionWasDisabled(root, env, payload.session_id ?? payload.sessionId))
|
|
250
300
|
return;
|
|
251
301
|
const autoConduct = env.HQ_AUTO_CONDUCT ?? "";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq scan secrets` — offline secret scanner (the HQ pre-commit gate).
|
|
3
|
+
*
|
|
4
|
+
* hq scan secrets scan the staged changes of the current repo
|
|
5
|
+
* hq scan secrets <file...> scan whole files instead
|
|
6
|
+
* hq scan secrets --json findings as JSON
|
|
7
|
+
*
|
|
8
|
+
* Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
|
|
9
|
+
* HQ's own token patterns. No network, no account, no quota.
|
|
10
|
+
*
|
|
11
|
+
* Exit codes: 0 clean, 1 possible secret found, 2 the scan could not run.
|
|
12
|
+
* A pre-commit hook blocks on any non-zero exit, so the gate fails closed.
|
|
13
|
+
* Output names the rule, file and line; secret values are never printed.
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
interface ScanSecretsOptions {
|
|
17
|
+
json?: boolean;
|
|
18
|
+
staged?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function runScanSecrets(paths: string[], opts: ScanSecretsOptions, cwd?: string): number;
|
|
21
|
+
export declare function registerScanCommand(program: Command): void;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq scan secrets` — offline secret scanner (the HQ pre-commit gate).
|
|
3
|
+
*
|
|
4
|
+
* hq scan secrets scan the staged changes of the current repo
|
|
5
|
+
* hq scan secrets <file...> scan whole files instead
|
|
6
|
+
* hq scan secrets --json findings as JSON
|
|
7
|
+
*
|
|
8
|
+
* Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
|
|
9
|
+
* HQ's own token patterns. No network, no account, no quota.
|
|
10
|
+
*
|
|
11
|
+
* Exit codes: 0 clean, 1 possible secret found, 2 the scan could not run.
|
|
12
|
+
* A pre-commit hook blocks on any non-zero exit, so the gate fails closed.
|
|
13
|
+
* Output names the rule, file and line; secret values are never printed.
|
|
14
|
+
*/
|
|
15
|
+
import { ScanInputError, scanFiles, scanStaged } from "../lib/secret-scan/staged.js";
|
|
16
|
+
function report(findings, scope) {
|
|
17
|
+
if (!findings.length)
|
|
18
|
+
return;
|
|
19
|
+
const n = findings.length;
|
|
20
|
+
process.stderr.write(`HQ secret scan: ${n} possible secret${n === 1 ? "" : "s"} in ${scope} (values hidden)\n`);
|
|
21
|
+
for (const f of findings) {
|
|
22
|
+
process.stderr.write(` ${f.file}:${f.line} ${f.ruleId} ${f.description}\n`);
|
|
23
|
+
}
|
|
24
|
+
process.stderr.write("\nRemove the secret from the change, then stage it again. Keep real credentials in the HQ vault\n" +
|
|
25
|
+
"(hq secrets, hq run). A known false positive can carry a `gitleaks:allow` comment on that line.\n");
|
|
26
|
+
}
|
|
27
|
+
export function runScanSecrets(paths, opts, cwd = process.cwd()) {
|
|
28
|
+
let findings;
|
|
29
|
+
try {
|
|
30
|
+
findings = paths.length && !opts.staged ? scanFiles(paths) : scanStaged(cwd);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const msg = err instanceof ScanInputError ? err.message : `scan failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
34
|
+
if (opts.json)
|
|
35
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: msg })}\n`);
|
|
36
|
+
process.stderr.write(`HQ secret scan: ${msg}\n`);
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
process.stdout.write(`${JSON.stringify({ ok: findings.length === 0, findings })}\n`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
report(findings, paths.length && !opts.staged ? "the given files" : "staged changes");
|
|
44
|
+
}
|
|
45
|
+
return findings.length ? 1 : 0;
|
|
46
|
+
}
|
|
47
|
+
export function registerScanCommand(program) {
|
|
48
|
+
const scan = program.command("scan").description("Local, offline scanners");
|
|
49
|
+
scan
|
|
50
|
+
.command("secrets")
|
|
51
|
+
.description("Scan staged changes (or files) for secrets; exits 1 on a finding")
|
|
52
|
+
.argument("[files...]", "Scan these files instead of the staged changes")
|
|
53
|
+
.option("--staged", "Scan the staged changes (default when no files are given)")
|
|
54
|
+
.option("--json", "Print findings as JSON")
|
|
55
|
+
.action((files, opts) => {
|
|
56
|
+
process.exitCode = runScanSecrets(files ?? [], opts);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -46,4 +46,12 @@ export declare const LANE_WORKER_CONTINUITY_FLAG = "lanes.worker-continuity";
|
|
|
46
46
|
export declare const LANE_CONDUCT_LOOP_FLAG = "lanes.conduct-loop";
|
|
47
47
|
export declare const LANE_ENVELOPE_REPAIR_GUIDANCE_FLAG = "lanes.envelope-repair-guidance";
|
|
48
48
|
export declare const LANE_LOOP_HANDOFF_REPAIR_FLAG = "lanes.loop-handoff-repair";
|
|
49
|
+
/** Adds the latest root-scoped orphan count to SessionStart context. */
|
|
50
|
+
export declare const LANE_ORPHAN_SWEEP_CONTEXT_FLAG = "lanes.orphan-sweep-context-line";
|
|
51
|
+
/** Separates lane reap leases and reports by resolved HQ root. */
|
|
52
|
+
export declare const LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG = "lanes.orphan-sweep-root-keying";
|
|
53
|
+
/** Reports awaiting-input lanes whose owning session ended. */
|
|
54
|
+
export declare const LANE_ORPHAN_SWEEP_WAITING_FLAG = "lanes.orphan-sweep-waiting-senior";
|
|
55
|
+
/** Reports and optionally stops abandoned legacy conduct pool loops. */
|
|
56
|
+
export declare const LANE_ORPHAN_SWEEP_LEGACY_FLAG = "lanes.orphan-sweep-legacy-pool";
|
|
49
57
|
//# sourceMappingURL=flag-keys.d.ts.map
|
|
@@ -46,4 +46,12 @@ export const LANE_WORKER_CONTINUITY_FLAG = "lanes.worker-continuity";
|
|
|
46
46
|
export const LANE_CONDUCT_LOOP_FLAG = "lanes.conduct-loop";
|
|
47
47
|
export const LANE_ENVELOPE_REPAIR_GUIDANCE_FLAG = "lanes.envelope-repair-guidance";
|
|
48
48
|
export const LANE_LOOP_HANDOFF_REPAIR_FLAG = "lanes.loop-handoff-repair";
|
|
49
|
+
/** Adds the latest root-scoped orphan count to SessionStart context. */
|
|
50
|
+
export const LANE_ORPHAN_SWEEP_CONTEXT_FLAG = "lanes.orphan-sweep-context-line";
|
|
51
|
+
/** Separates lane reap leases and reports by resolved HQ root. */
|
|
52
|
+
export const LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG = "lanes.orphan-sweep-root-keying";
|
|
53
|
+
/** Reports awaiting-input lanes whose owning session ended. */
|
|
54
|
+
export const LANE_ORPHAN_SWEEP_WAITING_FLAG = "lanes.orphan-sweep-waiting-senior";
|
|
55
|
+
/** Reports and optionally stops abandoned legacy conduct pool loops. */
|
|
56
|
+
export const LANE_ORPHAN_SWEEP_LEGACY_FLAG = "lanes.orphan-sweep-legacy-pool";
|
|
49
57
|
//# sourceMappingURL=flag-keys.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface LegacyPoolFinding {
|
|
2
|
+
lane_id: string;
|
|
3
|
+
legacy: true;
|
|
4
|
+
orphaned: boolean;
|
|
5
|
+
reason: string;
|
|
6
|
+
age_minutes: number;
|
|
7
|
+
session_id: string;
|
|
8
|
+
company_slug?: string;
|
|
9
|
+
pid: number;
|
|
10
|
+
process_starttime: number | null;
|
|
11
|
+
loop_started_at: string;
|
|
12
|
+
run_dir: string;
|
|
13
|
+
would_stop?: boolean;
|
|
14
|
+
stopped?: boolean;
|
|
15
|
+
stop_error?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface LegacyPoolDeps {
|
|
18
|
+
processes?: () => Array<{
|
|
19
|
+
pid: number;
|
|
20
|
+
ppid: number;
|
|
21
|
+
args: string;
|
|
22
|
+
}>;
|
|
23
|
+
isAlive?: (pid: number) => boolean;
|
|
24
|
+
readStarttime?: (pid: number) => number | null;
|
|
25
|
+
signal?: (pid: number, signal: NodeJS.Signals) => void;
|
|
26
|
+
snapshotProcess?: (pid: number) => {
|
|
27
|
+
args: string;
|
|
28
|
+
startedAt: number | null;
|
|
29
|
+
} | null;
|
|
30
|
+
sessionState?: (root: string, sessionId: string, now: Date) => string | null;
|
|
31
|
+
now?: () => Date;
|
|
32
|
+
}
|
|
33
|
+
export declare function findLegacyPoolLanes(root: string, deps?: LegacyPoolDeps): LegacyPoolFinding[];
|
|
34
|
+
export declare function stopLegacyPoolLane(root: string, finding: LegacyPoolFinding, deps?: LegacyPoolDeps): boolean;
|
|
35
|
+
//# sourceMappingURL=legacy-pool.d.ts.map
|