@indigoai-us/hq-cli 5.98.3 → 5.99.1
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 +43 -0
- package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +347 -0
- package/assets/scaffold/core/scripts/hook-lib.sh +557 -0
- package/assets/scaffold/core/scripts/hq-session.sh +251 -0
- package/assets/scaffold/core/scripts/lib/session-id.sh +96 -0
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +52 -0
- package/dist/commands/core-checkpoint.js +11 -3
- package/dist/commands/core.js +60 -5
- package/dist/commands/doctor.d.ts +97 -0
- package/dist/commands/doctor.js +228 -0
- package/dist/commands/scaffold-fast.d.ts +41 -0
- package/dist/commands/scaffold-fast.js +57 -0
- package/dist/fast-core.d.ts +16 -0
- package/dist/fast-core.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -1
- package/dist/lib/core-utils/soft-timeout.d.ts +55 -0
- package/dist/lib/core-utils/soft-timeout.js +205 -0
- package/dist/lib/core-utils/timeout-guard.d.ts +62 -0
- package/dist/lib/core-utils/timeout-guard.js +207 -0
- package/dist/lib/doctor/__testing__/fake-hq-tree.d.ts +194 -0
- package/dist/lib/doctor/__testing__/fake-hq-tree.js +357 -0
- package/dist/lib/doctor/allowed-divergence.d.ts +72 -0
- package/dist/lib/doctor/allowed-divergence.js +134 -0
- package/dist/lib/doctor/checks/claude-wiring.d.ts +55 -0
- package/dist/lib/doctor/checks/claude-wiring.js +524 -0
- package/dist/lib/doctor/checks/codex-wiring.d.ts +45 -0
- package/dist/lib/doctor/checks/codex-wiring.js +376 -0
- package/dist/lib/doctor/checks/grok-wiring.d.ts +35 -0
- package/dist/lib/doctor/checks/grok-wiring.js +186 -0
- package/dist/lib/doctor/checks/runtime-probe.d.ts +101 -0
- package/dist/lib/doctor/checks/runtime-probe.js +335 -0
- package/dist/lib/doctor/compat.d.ts +85 -0
- package/dist/lib/doctor/compat.js +102 -0
- package/dist/lib/doctor/deep/classify.d.ts +61 -0
- package/dist/lib/doctor/deep/classify.js +75 -0
- package/dist/lib/doctor/deep/effects.d.ts +107 -0
- package/dist/lib/doctor/deep/effects.js +229 -0
- package/dist/lib/doctor/deep/executor.d.ts +112 -0
- package/dist/lib/doctor/deep/executor.js +369 -0
- package/dist/lib/doctor/deep/parity.d.ts +129 -0
- package/dist/lib/doctor/deep/parity.js +355 -0
- package/dist/lib/doctor/deep/sandbox.d.ts +190 -0
- package/dist/lib/doctor/deep/sandbox.js +572 -0
- package/dist/lib/doctor/fix/apply.d.ts +119 -0
- package/dist/lib/doctor/fix/apply.js +352 -0
- package/dist/lib/doctor/fix/backup.d.ts +40 -0
- package/dist/lib/doctor/fix/backup.js +64 -0
- package/dist/lib/doctor/fix/remediation.d.ts +71 -0
- package/dist/lib/doctor/fix/remediation.js +103 -0
- package/dist/lib/doctor/fixtures/discover.d.ts +96 -0
- package/dist/lib/doctor/fixtures/discover.js +287 -0
- package/dist/lib/doctor/fixtures/schema.d.ts +171 -0
- package/dist/lib/doctor/fixtures/schema.js +248 -0
- package/dist/lib/doctor/hook-gate-profiles.d.ts +55 -0
- package/dist/lib/doctor/hook-gate-profiles.js +107 -0
- package/dist/lib/doctor/json-output.d.ts +90 -0
- package/dist/lib/doctor/json-output.js +76 -0
- package/dist/lib/doctor/payload-shapes.d.ts +170 -0
- package/dist/lib/doctor/payload-shapes.js +275 -0
- package/dist/lib/doctor/platform.d.ts +244 -0
- package/dist/lib/doctor/platform.js +490 -0
- package/dist/lib/doctor/registry.d.ts +49 -0
- package/dist/lib/doctor/registry.js +176 -0
- package/dist/lib/doctor/report.d.ts +87 -0
- package/dist/lib/doctor/report.js +164 -0
- package/dist/lib/doctor/types.d.ts +87 -0
- package/dist/lib/doctor/types.js +29 -0
- package/dist/main.js +6 -0
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-gate.d.ts +1 -1
- package/dist/utils/version-gate.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq doctor` — verify HQ's hook guardrails are wired and firing.
|
|
3
|
+
*
|
|
4
|
+
* US-002 builds the command skeleton and the extensible check registry:
|
|
5
|
+
* - HQ root resolution walks up from the working directory looking for the
|
|
6
|
+
* marker directories that identify an HQ tree, and exposes the resolved root
|
|
7
|
+
* to every check.
|
|
8
|
+
* - Running outside any HQ tree exits non-zero with a message naming exactly
|
|
9
|
+
* what it looked for — never a throw, never a false PASS.
|
|
10
|
+
* - The command performs no network calls and needs no authentication; it is
|
|
11
|
+
* purely a function of the on-disk shape of the tree.
|
|
12
|
+
*
|
|
13
|
+
* US-015 adds reporting, `--json`, and the exit-code contract: the exit code is
|
|
14
|
+
* 0 unless some result is FAIL or UNKNOWN (WARN/UNTESTED/NA/KNOWN-DEFECT never
|
|
15
|
+
* fail); `--verbose` also prints PASS results; `--json` emits the versioned,
|
|
16
|
+
* machine-readable document; and text output carries no ANSI when stdout is not
|
|
17
|
+
* a TTY. The rendering and contract live in ../lib/doctor/report.ts and
|
|
18
|
+
* ../lib/doctor/json-output.ts; this command wires them to the CLI.
|
|
19
|
+
*
|
|
20
|
+
* The detected platform is a placeholder (UNKNOWN_PLATFORM) until host platform
|
|
21
|
+
* detection (US-003) lands and injects the real value through `platform`.
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import { createDefaultRegistry, } from "../lib/doctor/registry.js";
|
|
26
|
+
import { computeExitCode, renderText, UNKNOWN_PLATFORM, } from "../lib/doctor/report.js";
|
|
27
|
+
import { buildDoctorJson, renderJson } from "../lib/doctor/json-output.js";
|
|
28
|
+
import { detectPlatform } from "../lib/doctor/platform.js";
|
|
29
|
+
import { DEEP_FAMILY_ID, DEEP_FAMILY_TITLE, runDeepGuardTests, } from "../lib/doctor/deep/executor.js";
|
|
30
|
+
import { PARITY_FAMILY_ID, PARITY_FAMILY_TITLE, runParityReplay, } from "../lib/doctor/deep/parity.js";
|
|
31
|
+
import { applyFixes } from "../lib/doctor/fix/apply.js";
|
|
32
|
+
import * as readline from "node:readline";
|
|
33
|
+
import { runSideEffectTests } from "../lib/doctor/deep/sandbox.js";
|
|
34
|
+
/**
|
|
35
|
+
* Marker directories that identify an HQ tree root. A directory is an HQ root
|
|
36
|
+
* only when it contains ALL of these. `.claude` + `core` are present in both
|
|
37
|
+
* the real HQ tree and the synthetic fixture trees the doctor is tested
|
|
38
|
+
* against, and are absent from an arbitrary directory — so requiring both
|
|
39
|
+
* avoids a false positive on a random repo that merely carries a `.claude/`
|
|
40
|
+
* folder.
|
|
41
|
+
*/
|
|
42
|
+
export const HQ_ROOT_MARKERS = [".claude", "core"];
|
|
43
|
+
/**
|
|
44
|
+
* Walk up from `startDir` (default: the current working directory) looking for
|
|
45
|
+
* the nearest ancestor that contains every {@link HQ_ROOT_MARKERS} entry as a
|
|
46
|
+
* directory. Returns the resolved (realpath'd) root, or `null` when no ancestor
|
|
47
|
+
* qualifies — i.e. the caller is not inside an HQ tree.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveHqRoot(startDir = process.cwd()) {
|
|
50
|
+
let dir;
|
|
51
|
+
try {
|
|
52
|
+
dir = fs.realpathSync(startDir);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
dir = path.resolve(startDir);
|
|
56
|
+
}
|
|
57
|
+
// Walk to the filesystem root. `path.dirname("/") === "/"` is the loop's
|
|
58
|
+
// terminal fixed point.
|
|
59
|
+
for (;;) {
|
|
60
|
+
if (isHqRoot(dir))
|
|
61
|
+
return dir;
|
|
62
|
+
const parent = path.dirname(dir);
|
|
63
|
+
if (parent === dir)
|
|
64
|
+
return null;
|
|
65
|
+
dir = parent;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function isHqRoot(dir) {
|
|
69
|
+
return HQ_ROOT_MARKERS.every((marker) => {
|
|
70
|
+
try {
|
|
71
|
+
return fs.statSync(path.join(dir, marker)).isDirectory();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the HQ root, run every registered check family against it, and render
|
|
80
|
+
* a plain-text summary. Makes no network calls and needs no authentication.
|
|
81
|
+
*/
|
|
82
|
+
export async function runDoctor(options = {}) {
|
|
83
|
+
const write = options.stdout ?? ((chunk) => void process.stdout.write(chunk));
|
|
84
|
+
const writeErr = options.stderr ?? ((chunk) => void process.stderr.write(chunk));
|
|
85
|
+
const hqRoot = options.root ?? resolveHqRoot(options.cwd);
|
|
86
|
+
if (!hqRoot) {
|
|
87
|
+
const from = options.cwd ?? process.cwd();
|
|
88
|
+
writeErr(`hq doctor: not inside an HQ tree.\n` +
|
|
89
|
+
` Searched upward from: ${from}\n` +
|
|
90
|
+
` Looking for a directory containing all of: ${HQ_ROOT_MARKERS.join(", ")}\n` +
|
|
91
|
+
` Run hq doctor from inside your HQ root.\n`);
|
|
92
|
+
return { exitCode: 1, hqRoot: null, families: [] };
|
|
93
|
+
}
|
|
94
|
+
const registry = options.registry ?? createDefaultRegistry();
|
|
95
|
+
const platform = options.platform ?? UNKNOWN_PLATFORM;
|
|
96
|
+
// The detected platform and the session id are exposed to every check so the
|
|
97
|
+
// host-specific runtime probe (US-006) can decide UNKNOWN vs FAIL vs UNTESTED.
|
|
98
|
+
const context = {
|
|
99
|
+
hqRoot,
|
|
100
|
+
platform: { id: platform.id, evidence: platform.evidence },
|
|
101
|
+
sessionId: options.sessionId,
|
|
102
|
+
};
|
|
103
|
+
const families = await registry.run(context);
|
|
104
|
+
// `--deep-test` (US-008): after the read-only tiers, actually fire pure-guard
|
|
105
|
+
// fixtures through the real gate in a sandbox and append the verdicts as their
|
|
106
|
+
// own family. Run only when asked — appending here, not in the default
|
|
107
|
+
// registry, is what keeps `hq doctor` from ever spawning a hook without the
|
|
108
|
+
// flag. Any FAIL/UNKNOWN it produces flows through computeExitCode below.
|
|
109
|
+
if (options.deepTest) {
|
|
110
|
+
const deepResults = await runDeepGuardTests(context);
|
|
111
|
+
// US-009: side-effecting hooks (autocommit, checkpoint, journal, reindex, …)
|
|
112
|
+
// cannot be verified by verdict, so they run in throwaway sandboxes and their
|
|
113
|
+
// fixture cases assert observable effects. Their verdicts join the same deep
|
|
114
|
+
// family and flow through computeExitCode below.
|
|
115
|
+
const effectResults = await runSideEffectTests(context);
|
|
116
|
+
families.push({
|
|
117
|
+
family: { id: DEEP_FAMILY_ID, title: DEEP_FAMILY_TITLE },
|
|
118
|
+
results: [...deepResults, ...effectResults],
|
|
119
|
+
});
|
|
120
|
+
// Cross-platform parity replay (US-010): replay every pure-guard fixture
|
|
121
|
+
// case through the Claude, Codex, and Grok adapters and compare verdicts, so
|
|
122
|
+
// platform drift surfaces as a test result. Also gated behind --deep-test,
|
|
123
|
+
// and it too runs only in its own sandbox — never the live tree.
|
|
124
|
+
const parityResults = await runParityReplay(context);
|
|
125
|
+
families.push({
|
|
126
|
+
family: { id: PARITY_FAMILY_ID, title: PARITY_FAMILY_TITLE },
|
|
127
|
+
results: parityResults,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (options.json) {
|
|
131
|
+
write(renderJson(buildDoctorJson({ hqRoot, families, platform })));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
write(renderText({
|
|
135
|
+
hqRoot,
|
|
136
|
+
families,
|
|
137
|
+
platform,
|
|
138
|
+
verbose: options.verbose ?? false,
|
|
139
|
+
color: options.color ?? false,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
// The exit-code contract: 0 unless some result is FAIL or UNKNOWN. WARN,
|
|
143
|
+
// UNTESTED, NA, and KNOWN-DEFECT are reported but never fail the command.
|
|
144
|
+
return { exitCode: computeExitCode(families), hqRoot, families };
|
|
145
|
+
}
|
|
146
|
+
/** Register the top-level `hq doctor` command so it appears in `hq --help`. */
|
|
147
|
+
export function registerDoctorCommand(program) {
|
|
148
|
+
program
|
|
149
|
+
.command("doctor")
|
|
150
|
+
.description("Verify HQ hook guardrails are wired and firing (read-only, offline).")
|
|
151
|
+
.option("--json", "Emit the machine-readable JSON document (no colour).")
|
|
152
|
+
.option("--verbose", "Also print every PASS result in text output.")
|
|
153
|
+
.option("--no-color", "Disable ANSI colour even on a TTY.")
|
|
154
|
+
.option("--session-id <id>", "Scope the runtime probe's ledger check to this exact session.")
|
|
155
|
+
.option("--deep-test", "Also fire pure-guard hooks through the real gate under all three profiles (sandboxed).")
|
|
156
|
+
.option("--fix", "Apply the allowlisted safe repairs (backs up first; read-only without this flag).")
|
|
157
|
+
.option("--yes", "Skip the interactive --fix confirmation (non-interactive use).")
|
|
158
|
+
.option("--force", "Let --fix run despite uncommitted changes under .claude/, .codex/, or .grok/.")
|
|
159
|
+
.action(async (opts) => {
|
|
160
|
+
// `--fix` is the only write path. It resolves the tree, applies the
|
|
161
|
+
// allowlisted repairs behind a backup + confirmation, and returns its own
|
|
162
|
+
// exit code; the read-only report below never runs in this branch.
|
|
163
|
+
if (opts.fix === true) {
|
|
164
|
+
const hqRoot = resolveHqRoot();
|
|
165
|
+
if (!hqRoot) {
|
|
166
|
+
process.stderr.write(`hq doctor --fix: not inside an HQ tree.\n` +
|
|
167
|
+
` Looking for a directory containing all of: ${HQ_ROOT_MARKERS.join(", ")}\n`);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const fixResult = await applyFixes({
|
|
172
|
+
hqRoot,
|
|
173
|
+
yes: opts.yes === true,
|
|
174
|
+
force: opts.force === true,
|
|
175
|
+
confirm: promptYesNo,
|
|
176
|
+
});
|
|
177
|
+
process.exitCode = fixResult.exitCode;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const json = opts.json === true;
|
|
181
|
+
// Colour only when writing text to an interactive terminal, and never
|
|
182
|
+
// when NO_COLOR is set. A non-TTY (pipe, file, CI) gets no ANSI at all.
|
|
183
|
+
const color = !json &&
|
|
184
|
+
opts.color !== false &&
|
|
185
|
+
process.stdout.isTTY === true &&
|
|
186
|
+
!process.env.NO_COLOR;
|
|
187
|
+
// Detect the host once, here at the CLI boundary, and hand it to the run.
|
|
188
|
+
// The runtime probe (US-006) needs the real host to tell an app/SDK
|
|
189
|
+
// runtime that never dispatches hooks apart from a CLI that does.
|
|
190
|
+
const detection = detectPlatform();
|
|
191
|
+
const platform = {
|
|
192
|
+
id: detection.platform,
|
|
193
|
+
evidence: detection.evidence,
|
|
194
|
+
};
|
|
195
|
+
const result = await runDoctor({
|
|
196
|
+
json,
|
|
197
|
+
verbose: opts.verbose === true,
|
|
198
|
+
color,
|
|
199
|
+
platform,
|
|
200
|
+
sessionId: opts.sessionId,
|
|
201
|
+
deepTest: opts.deepTest === true,
|
|
202
|
+
});
|
|
203
|
+
// Set the exit code rather than calling process.exit, so the CLI's
|
|
204
|
+
// normal shutdown (telemetry flush) still runs. Non-zero means either an
|
|
205
|
+
// out-of-tree run or a FAIL/UNKNOWN result.
|
|
206
|
+
process.exitCode = result.exitCode;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Interactive y/N confirmation for `--fix`. Resolves false on a non-TTY stdin
|
|
211
|
+
* (so a piped run without `--yes` writes nothing) and on anything other than an
|
|
212
|
+
* explicit yes.
|
|
213
|
+
*/
|
|
214
|
+
function promptYesNo() {
|
|
215
|
+
if (!process.stdin.isTTY)
|
|
216
|
+
return Promise.resolve(false);
|
|
217
|
+
const rl = readline.createInterface({
|
|
218
|
+
input: process.stdin,
|
|
219
|
+
output: process.stdout,
|
|
220
|
+
});
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
rl.question("Apply these repairs? [y/N] ", (answer) => {
|
|
223
|
+
rl.close();
|
|
224
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast-path manifest for relocated scaffold scripts, kept deliberately
|
|
3
|
+
* dependency-light.
|
|
4
|
+
*
|
|
5
|
+
* `main.ts` eagerly imports the CLI's entire (~60-module) command graph, so any
|
|
6
|
+
* command routed through it pays seconds of startup — fine for a human typing a
|
|
7
|
+
* command, ruinous for the hot plumbing this hosts: `hq core hq-session` is
|
|
8
|
+
* called several times per skill/hook, and `hq core checkpoint-stop-gate` runs
|
|
9
|
+
* on every turn's Stop. The entrypoint routes those directly to
|
|
10
|
+
* `runBundledScript` instead, and this module holds the tiny parser + table that
|
|
11
|
+
* routing needs WITHOUT importing anything heavy. `core.ts` names the same
|
|
12
|
+
* assets, and a test pins the two lists together so they cannot drift.
|
|
13
|
+
*/
|
|
14
|
+
export type FastScaffoldScript = {
|
|
15
|
+
/** Direct subcommand name — `hq core <name>`. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Path under `assets/scaffold/`, e.g. `core/scripts/foo.sh`. */
|
|
18
|
+
asset: string;
|
|
19
|
+
};
|
|
20
|
+
/** The relocated scripts that qualify for the entrypoint fast path. */
|
|
21
|
+
export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];
|
|
22
|
+
export type ParsedFastCore = {
|
|
23
|
+
/** The bundled asset to run. */
|
|
24
|
+
asset: string;
|
|
25
|
+
/** `--hq-root <path>` value, if the caller passed the group option. */
|
|
26
|
+
hqRootArg?: string;
|
|
27
|
+
/** Everything after the command name, passed to the script verbatim. */
|
|
28
|
+
args: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
|
|
32
|
+
* fast-path script. Returns null for anything else so the caller defers to the
|
|
33
|
+
* full CLI. Mirrors the `core` group's single `--hq-root` option (both the
|
|
34
|
+
* `--hq-root <path>` and `--hq-root=<path>` forms) and its `root: "live"`
|
|
35
|
+
* binding; every remaining token is passed through untouched, exactly as the
|
|
36
|
+
* commander registration does with allowUnknownOption + a variadic argument.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseFastCore(argv: readonly string[]): ParsedFastCore | null;
|
|
39
|
+
/** Whether this invocation is a fast-path scaffold dispatch. */
|
|
40
|
+
export declare function isFastCoreRequest(argv: readonly string[]): boolean;
|
|
41
|
+
//# sourceMappingURL=scaffold-fast.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast-path manifest for relocated scaffold scripts, kept deliberately
|
|
3
|
+
* dependency-light.
|
|
4
|
+
*
|
|
5
|
+
* `main.ts` eagerly imports the CLI's entire (~60-module) command graph, so any
|
|
6
|
+
* command routed through it pays seconds of startup — fine for a human typing a
|
|
7
|
+
* command, ruinous for the hot plumbing this hosts: `hq core hq-session` is
|
|
8
|
+
* called several times per skill/hook, and `hq core checkpoint-stop-gate` runs
|
|
9
|
+
* on every turn's Stop. The entrypoint routes those directly to
|
|
10
|
+
* `runBundledScript` instead, and this module holds the tiny parser + table that
|
|
11
|
+
* routing needs WITHOUT importing anything heavy. `core.ts` names the same
|
|
12
|
+
* assets, and a test pins the two lists together so they cannot drift.
|
|
13
|
+
*/
|
|
14
|
+
/** The relocated scripts that qualify for the entrypoint fast path. */
|
|
15
|
+
export const FAST_SCAFFOLD_SCRIPTS = [
|
|
16
|
+
{ name: "checkpoint-stop-gate", asset: "core/scripts/checkpoint-stop-gate.sh" },
|
|
17
|
+
{ name: "hq-session", asset: "core/scripts/hq-session.sh" },
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
|
|
21
|
+
* fast-path script. Returns null for anything else so the caller defers to the
|
|
22
|
+
* full CLI. Mirrors the `core` group's single `--hq-root` option (both the
|
|
23
|
+
* `--hq-root <path>` and `--hq-root=<path>` forms) and its `root: "live"`
|
|
24
|
+
* binding; every remaining token is passed through untouched, exactly as the
|
|
25
|
+
* commander registration does with allowUnknownOption + a variadic argument.
|
|
26
|
+
*/
|
|
27
|
+
export function parseFastCore(argv) {
|
|
28
|
+
// argv is process.argv: [node, hq, core, ...rest].
|
|
29
|
+
if (argv[2] !== "core")
|
|
30
|
+
return null;
|
|
31
|
+
let i = 3;
|
|
32
|
+
let hqRootArg;
|
|
33
|
+
const head = argv[i];
|
|
34
|
+
if (head === "--hq-root") {
|
|
35
|
+
const value = argv[i + 1];
|
|
36
|
+
if (typeof value !== "string")
|
|
37
|
+
return null;
|
|
38
|
+
hqRootArg = value;
|
|
39
|
+
i += 2;
|
|
40
|
+
}
|
|
41
|
+
else if (typeof head === "string" && head.startsWith("--hq-root=")) {
|
|
42
|
+
hqRootArg = head.slice("--hq-root=".length);
|
|
43
|
+
i += 1;
|
|
44
|
+
}
|
|
45
|
+
const name = argv[i];
|
|
46
|
+
if (typeof name !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
const entry = FAST_SCAFFOLD_SCRIPTS.find((candidate) => candidate.name === name);
|
|
49
|
+
if (!entry)
|
|
50
|
+
return null;
|
|
51
|
+
return { asset: entry.asset, hqRootArg, args: argv.slice(i + 1) };
|
|
52
|
+
}
|
|
53
|
+
/** Whether this invocation is a fast-path scaffold dispatch. */
|
|
54
|
+
export function isFastCoreRequest(argv) {
|
|
55
|
+
return parseFastCore(argv) !== null;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=scaffold-fast.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast-path dispatcher for relocated scaffold scripts.
|
|
3
|
+
*
|
|
4
|
+
* Reached from the entrypoint BEFORE `main.ts` (and its heavy command graph) is
|
|
5
|
+
* ever imported, so a hot `hq core hq-session` / `hq core checkpoint-stop-gate`
|
|
6
|
+
* dispatches in roughly node's own startup time. It imports only the light root
|
|
7
|
+
* resolver and the bundled-script runner. The slower commander registration in
|
|
8
|
+
* `core.ts` remains the complete path for any invocation this does not match.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Run the fast-path scaffold script named in `argv` and exit with its code.
|
|
12
|
+
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
|
13
|
+
* doing anything.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runFastCore(argv: readonly string[]): void;
|
|
16
|
+
//# sourceMappingURL=fast-core.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast-path dispatcher for relocated scaffold scripts.
|
|
3
|
+
*
|
|
4
|
+
* Reached from the entrypoint BEFORE `main.ts` (and its heavy command graph) is
|
|
5
|
+
* ever imported, so a hot `hq core hq-session` / `hq core checkpoint-stop-gate`
|
|
6
|
+
* dispatches in roughly node's own startup time. It imports only the light root
|
|
7
|
+
* resolver and the bundled-script runner. The slower commander registration in
|
|
8
|
+
* `core.ts` remains the complete path for any invocation this does not match.
|
|
9
|
+
*/
|
|
10
|
+
import { resolveLiveRoot } from "./utils/hq-roots.js";
|
|
11
|
+
import { runBundledScript } from "./utils/run-bundled-script.js";
|
|
12
|
+
import { parseFastCore } from "./commands/scaffold-fast.js";
|
|
13
|
+
/**
|
|
14
|
+
* Run the fast-path scaffold script named in `argv` and exit with its code.
|
|
15
|
+
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
|
16
|
+
* doing anything.
|
|
17
|
+
*/
|
|
18
|
+
export function runFastCore(argv) {
|
|
19
|
+
const parsed = parseFastCore(argv);
|
|
20
|
+
if (!parsed)
|
|
21
|
+
return;
|
|
22
|
+
// `root: "live"` — resolve the live HQ root and run the script in it, exactly
|
|
23
|
+
// as core.ts's runEntry does. Fail soft: if no root can be resolved, run the
|
|
24
|
+
// script anyway with the inherited environment (the hook already carries
|
|
25
|
+
// CLAUDE_PROJECT_DIR, and each script has its own root fallback) rather than
|
|
26
|
+
// crashing a Stop hook on a resolution error.
|
|
27
|
+
let hqRoot;
|
|
28
|
+
try {
|
|
29
|
+
hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
hqRoot = undefined;
|
|
33
|
+
}
|
|
34
|
+
// Keep the CHILD in the caller's cwd, unlike the generic "live" dispatch. The
|
|
35
|
+
// relocated scripts read their data tree from the injected root, not cwd, and
|
|
36
|
+
// hq-session's Work Mesh registration serializes $PWD — so changing directory
|
|
37
|
+
// to the HQ root would mislabel a bind made from inside a project/worktree.
|
|
38
|
+
const { code } = runBundledScript({
|
|
39
|
+
asset: parsed.asset,
|
|
40
|
+
args: parsed.args,
|
|
41
|
+
cwd: process.cwd(),
|
|
42
|
+
hqRoot,
|
|
43
|
+
});
|
|
44
|
+
// Propagate verbatim: these scripts use their exit code as their interface.
|
|
45
|
+
process.exit(code);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=fast-core.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./node-preflight.js";
|
|
3
3
|
import "./node-network-compat.js";
|
|
4
|
+
import { isFastCoreRequest } from "./commands/scaffold-fast.js";
|
|
4
5
|
declare function isVersionRequest(argv: readonly string[]): boolean;
|
|
5
6
|
export declare const __test__: {
|
|
6
7
|
isVersionRequest: typeof isVersionRequest;
|
|
8
|
+
isFastCoreRequest: typeof isFastCoreRequest;
|
|
7
9
|
};
|
|
8
10
|
export {};
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import "./node-preflight.js";
|
|
5
5
|
import "./node-network-compat.js";
|
|
6
6
|
import { CLI_VERSION } from "./cli-version.js";
|
|
7
|
+
// Dependency-light: a pure parser + table, no command graph. Safe to load on
|
|
8
|
+
// every path (including --version) without reintroducing the heavy startup.
|
|
9
|
+
import { isFastCoreRequest } from "./commands/scaffold-fast.js";
|
|
7
10
|
function isVersionRequest(argv) {
|
|
8
11
|
const args = argv.slice(2);
|
|
9
12
|
return args.length === 1 && (args[0] === "--version" || args[0] === "-V" || args[0] === "-v");
|
|
@@ -11,6 +14,12 @@ function isVersionRequest(argv) {
|
|
|
11
14
|
if (isVersionRequest(process.argv)) {
|
|
12
15
|
process.stdout.write(`${CLI_VERSION}\n`);
|
|
13
16
|
}
|
|
17
|
+
else if (isFastCoreRequest(process.argv)) {
|
|
18
|
+
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
19
|
+
// dispatches straight to the bundled-script runner, so it never pays for the
|
|
20
|
+
// ~60-module command graph main.ts imports. See src/commands/scaffold-fast.ts.
|
|
21
|
+
void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
|
|
22
|
+
}
|
|
14
23
|
else {
|
|
15
24
|
// Keep the command lifecycle detached from module evaluation, as it was
|
|
16
25
|
// before the fast --version split. Some best-effort teardown work uses
|
|
@@ -19,5 +28,5 @@ else {
|
|
|
19
28
|
// Rejections still become unhandled and preserve a genuine non-zero failure.
|
|
20
29
|
void import("./main.js").then(({ runCli }) => runCli());
|
|
21
30
|
}
|
|
22
|
-
export const __test__ = { isVersionRequest };
|
|
31
|
+
export const __test__ = { isVersionRequest, isFastCoreRequest };
|
|
23
32
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq core soft-timeout <interval> [--hard-cap <spec>] [--label <name>] -- <cmd…>`
|
|
3
|
+
*
|
|
4
|
+
* Run a command under a SOFT timeout: WARN, don't kill. This is the CLI-hosted
|
|
5
|
+
* form of the shell primitive `core/scripts/lib/soft-timeout.sh`, so HQ scripts
|
|
6
|
+
* and hooks can call `hq core soft-timeout …` directly instead of shelling out
|
|
7
|
+
* to a bundled script.
|
|
8
|
+
*
|
|
9
|
+
* Finding 2.1 (team harness analysis 2026-08-10 — 1,393 forced-kill events):
|
|
10
|
+
* a fixed deadline SIGTERMs an operation still making progress and loses the
|
|
11
|
+
* in-flight work. The fix is warn-and-continue — on each interval the command
|
|
12
|
+
* keeps running and a SOFT-TIMEOUT notice is written to stderr so whoever is
|
|
13
|
+
* watching decides (wait / background / kill), instead of the runner deciding
|
|
14
|
+
* by killing progressing work.
|
|
15
|
+
*
|
|
16
|
+
* Contract (identical to the shell primitive):
|
|
17
|
+
* - `<interval>` — warn every interval. Bare number = seconds; 90s / 2m / 1h.
|
|
18
|
+
* - `--hard-cap <spec>` — OPTIONAL safety ceiling for unattended callers.
|
|
19
|
+
* Pure soft has no cap and never terminates. At the cap the command is
|
|
20
|
+
* SIGTERM'd, then SIGKILL'd after a short grace, and the exit code is 124.
|
|
21
|
+
* - `--label <name>` — name shown in the notice (default: the command basename).
|
|
22
|
+
* - Exit status: the command's own, verbatim — except 124 on a hard-cap.
|
|
23
|
+
* A soft warning alone never changes the exit status.
|
|
24
|
+
*/
|
|
25
|
+
import { spawn } from "node:child_process";
|
|
26
|
+
/** Parse a duration spec (Ns / Nm / Nh / bare N=seconds) to integer seconds. */
|
|
27
|
+
export declare function parseDurationSecs(spec: string): number | null;
|
|
28
|
+
export interface ParsedSoftTimeoutArgs {
|
|
29
|
+
intervalSecs: number;
|
|
30
|
+
hardCapSecs: number | null;
|
|
31
|
+
label: string | null;
|
|
32
|
+
command: string[];
|
|
33
|
+
}
|
|
34
|
+
/** Parse the `hq core soft-timeout` argv. Throws a plain Error on misuse. */
|
|
35
|
+
export declare function parseSoftTimeoutArgs(argv: string[]): ParsedSoftTimeoutArgs;
|
|
36
|
+
export interface RunSoftTimeoutDeps {
|
|
37
|
+
spawnFn?: typeof spawn;
|
|
38
|
+
stderr?: NodeJS.WritableStream;
|
|
39
|
+
now?: () => Date;
|
|
40
|
+
/** Signal a whole process group by (leader) pid. Defaults to process.kill(-pid). */
|
|
41
|
+
killGroup?: (pid: number, sig: NodeJS.Signals) => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Execute the command under the soft timeout. Resolves to the exit code the
|
|
45
|
+
* process should exit with. Never rejects on a timeout — a warning is only a
|
|
46
|
+
* stderr line. Rejects only on spawn failure.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runSoftTimeout(parsed: ParsedSoftTimeoutArgs, deps?: RunSoftTimeoutDeps): Promise<number>;
|
|
49
|
+
/**
|
|
50
|
+
* Entry point for the `hq core soft-timeout` command. Parses argv, runs, and
|
|
51
|
+
* returns the exit code (never throws for a timeout; throws only on misuse or
|
|
52
|
+
* spawn failure, which the caller maps to a non-zero exit).
|
|
53
|
+
*/
|
|
54
|
+
export declare function softTimeoutCommand(argv: string[]): Promise<number>;
|
|
55
|
+
//# sourceMappingURL=soft-timeout.d.ts.map
|