@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +347 -0
  3. package/assets/scaffold/core/scripts/hook-lib.sh +557 -0
  4. package/assets/scaffold/core/scripts/hq-session.sh +251 -0
  5. package/assets/scaffold/core/scripts/lib/session-id.sh +96 -0
  6. package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +52 -0
  7. package/dist/commands/core-checkpoint.js +11 -3
  8. package/dist/commands/core.js +60 -5
  9. package/dist/commands/doctor.d.ts +97 -0
  10. package/dist/commands/doctor.js +228 -0
  11. package/dist/commands/scaffold-fast.d.ts +41 -0
  12. package/dist/commands/scaffold-fast.js +57 -0
  13. package/dist/fast-core.d.ts +16 -0
  14. package/dist/fast-core.js +47 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +10 -1
  17. package/dist/lib/core-utils/soft-timeout.d.ts +55 -0
  18. package/dist/lib/core-utils/soft-timeout.js +205 -0
  19. package/dist/lib/core-utils/timeout-guard.d.ts +62 -0
  20. package/dist/lib/core-utils/timeout-guard.js +207 -0
  21. package/dist/lib/doctor/__testing__/fake-hq-tree.d.ts +194 -0
  22. package/dist/lib/doctor/__testing__/fake-hq-tree.js +357 -0
  23. package/dist/lib/doctor/allowed-divergence.d.ts +72 -0
  24. package/dist/lib/doctor/allowed-divergence.js +134 -0
  25. package/dist/lib/doctor/checks/claude-wiring.d.ts +55 -0
  26. package/dist/lib/doctor/checks/claude-wiring.js +524 -0
  27. package/dist/lib/doctor/checks/codex-wiring.d.ts +45 -0
  28. package/dist/lib/doctor/checks/codex-wiring.js +376 -0
  29. package/dist/lib/doctor/checks/grok-wiring.d.ts +35 -0
  30. package/dist/lib/doctor/checks/grok-wiring.js +186 -0
  31. package/dist/lib/doctor/checks/runtime-probe.d.ts +101 -0
  32. package/dist/lib/doctor/checks/runtime-probe.js +335 -0
  33. package/dist/lib/doctor/compat.d.ts +85 -0
  34. package/dist/lib/doctor/compat.js +102 -0
  35. package/dist/lib/doctor/deep/classify.d.ts +61 -0
  36. package/dist/lib/doctor/deep/classify.js +75 -0
  37. package/dist/lib/doctor/deep/effects.d.ts +107 -0
  38. package/dist/lib/doctor/deep/effects.js +229 -0
  39. package/dist/lib/doctor/deep/executor.d.ts +112 -0
  40. package/dist/lib/doctor/deep/executor.js +369 -0
  41. package/dist/lib/doctor/deep/parity.d.ts +129 -0
  42. package/dist/lib/doctor/deep/parity.js +355 -0
  43. package/dist/lib/doctor/deep/sandbox.d.ts +190 -0
  44. package/dist/lib/doctor/deep/sandbox.js +572 -0
  45. package/dist/lib/doctor/fix/apply.d.ts +119 -0
  46. package/dist/lib/doctor/fix/apply.js +352 -0
  47. package/dist/lib/doctor/fix/backup.d.ts +40 -0
  48. package/dist/lib/doctor/fix/backup.js +64 -0
  49. package/dist/lib/doctor/fix/remediation.d.ts +71 -0
  50. package/dist/lib/doctor/fix/remediation.js +103 -0
  51. package/dist/lib/doctor/fixtures/discover.d.ts +96 -0
  52. package/dist/lib/doctor/fixtures/discover.js +287 -0
  53. package/dist/lib/doctor/fixtures/schema.d.ts +171 -0
  54. package/dist/lib/doctor/fixtures/schema.js +248 -0
  55. package/dist/lib/doctor/hook-gate-profiles.d.ts +55 -0
  56. package/dist/lib/doctor/hook-gate-profiles.js +107 -0
  57. package/dist/lib/doctor/json-output.d.ts +90 -0
  58. package/dist/lib/doctor/json-output.js +76 -0
  59. package/dist/lib/doctor/payload-shapes.d.ts +170 -0
  60. package/dist/lib/doctor/payload-shapes.js +275 -0
  61. package/dist/lib/doctor/platform.d.ts +244 -0
  62. package/dist/lib/doctor/platform.js +490 -0
  63. package/dist/lib/doctor/registry.d.ts +49 -0
  64. package/dist/lib/doctor/registry.js +176 -0
  65. package/dist/lib/doctor/report.d.ts +87 -0
  66. package/dist/lib/doctor/report.js +164 -0
  67. package/dist/lib/doctor/types.d.ts +87 -0
  68. package/dist/lib/doctor/types.js +29 -0
  69. package/dist/main.js +6 -0
  70. package/dist/utils/version-check.js +2 -2
  71. package/dist/utils/version-gate.d.ts +1 -1
  72. package/dist/utils/version-gate.js +1 -1
  73. package/package.json +1 -1
@@ -0,0 +1,176 @@
1
+ /**
2
+ * The doctor engine — an ordered set of check families — and the first family
3
+ * (hooks).
4
+ *
5
+ * The engine is deliberately ignorant of any specific family: `register`
6
+ * accepts any object implementing {@link CheckFamily}, and `run` iterates them
7
+ * generically. A new family (vault, sync, MCP, secrets, qmd freshness, …) is
8
+ * added by registering it — never by editing this class — which is exactly the
9
+ * property US-002 requires and later stories rely on.
10
+ *
11
+ * The hooks family here is intentionally a skeleton: it establishes the family
12
+ * so the wiring/parity/runtime checks (US-004+) have somewhere to live, and
13
+ * reports only the presence and validity of `.claude/settings.json`. Those
14
+ * later stories add per-item results to this family without touching the engine.
15
+ */
16
+ import * as fs from "node:fs";
17
+ import * as path from "node:path";
18
+ import { checkCodexWiring } from "./checks/codex-wiring.js";
19
+ import { checkGrokWiring } from "./checks/grok-wiring.js";
20
+ import { checkRuntimeProbe } from "./checks/runtime-probe.js";
21
+ import { fixtureCoverageFamily } from "./fixtures/discover.js";
22
+ import { checkClaudeWiring } from "./checks/claude-wiring.js";
23
+ /**
24
+ * An ordered, id-keyed set of check families. Registration order is preserved
25
+ * (Map iteration order), and re-registering an id replaces the family in place
26
+ * rather than appending a duplicate.
27
+ */
28
+ export class DoctorRegistry {
29
+ familiesById = new Map();
30
+ /** Register a family. Re-registering the same id replaces it, keeping order. */
31
+ register(family) {
32
+ this.familiesById.set(family.id, family);
33
+ }
34
+ /** The registered families, in registration order. */
35
+ families() {
36
+ return [...this.familiesById.values()];
37
+ }
38
+ /** Whether a family with this id is registered. */
39
+ has(id) {
40
+ return this.familiesById.has(id);
41
+ }
42
+ /** Run every family in order and collect grouped results. */
43
+ async run(context) {
44
+ const runs = [];
45
+ for (const family of this.families()) {
46
+ const results = await family.run(context);
47
+ runs.push({ family: { id: family.id, title: family.title }, results });
48
+ }
49
+ return runs;
50
+ }
51
+ }
52
+ /** The id of the hooks family — the first, and for now only, check family. */
53
+ export const HOOKS_FAMILY_ID = "hooks";
54
+ /**
55
+ * The hooks check family. US-002 shipped the skeleton (Claude settings presence
56
+ * and validity). US-005 adds the Codex and Grok wiring/parity tiers, which run
57
+ * for every platform regardless of host. US-004+ extend the Claude tier with the
58
+ * full settings/registration wiring checks. Each tier appends its own results;
59
+ * the engine (DoctorRegistry) is untouched.
60
+ */
61
+ export const hooksFamily = {
62
+ id: HOOKS_FAMILY_ID,
63
+ title: "Hook wiring",
64
+ run: (context) => Promise.resolve(runHooksFamily(context)),
65
+ };
66
+ function runHooksFamily(context) {
67
+ const results = [];
68
+ results.push(...runClaudeSettingsCheck(context));
69
+ // Codex and Grok wiring/parity (US-005) run for every platform regardless of
70
+ // host. Each tier is wrapped so a parse failure in one cannot crash the whole
71
+ // doctor run — a thrown tier degrades to an UNKNOWN result instead.
72
+ results.push(...safeTier("hooks.codex", () => checkCodexWiring(context)));
73
+ results.push(...safeTier("hooks.grok", () => checkGrokWiring(context)));
74
+ // The runtime probe (US-006) is the live tier: it reads the policy-trigger
75
+ // ledger to report whether hooks actually FIRED on the detected host, catching
76
+ // an app/SDK runtime that loads hook config correctly and then never dispatches
77
+ // it. Runs last so the wiring picture is reported before the runtime verdict.
78
+ results.push(...safeTier("hooks.runtime", () => checkRuntimeProbe(context)));
79
+ return results;
80
+ }
81
+ /** Run one hooks tier, converting an unexpected throw into an UNKNOWN result. */
82
+ function safeTier(idPrefix, run) {
83
+ try {
84
+ return run();
85
+ }
86
+ catch (error) {
87
+ return [
88
+ {
89
+ status: "UNKNOWN",
90
+ checkId: `${idPrefix}.error`,
91
+ message: `${idPrefix} checks could not run: ${error.message}`,
92
+ },
93
+ ];
94
+ }
95
+ }
96
+ function runClaudeSettingsCheck(context) {
97
+ const settingsPath = path.join(context.hqRoot, ".claude", "settings.json");
98
+ if (!fs.existsSync(settingsPath)) {
99
+ return [
100
+ {
101
+ status: "FAIL",
102
+ checkId: "hooks.settings-present",
103
+ target: settingsPath,
104
+ message: ".claude/settings.json is missing — no Claude hooks can be wired.",
105
+ remediation: "Restore it, e.g. `hq rescue -y --paths .claude`.",
106
+ },
107
+ ];
108
+ }
109
+ let parsed;
110
+ try {
111
+ parsed = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
112
+ }
113
+ catch {
114
+ return [
115
+ {
116
+ status: "FAIL",
117
+ checkId: "hooks.settings-valid-json",
118
+ target: settingsPath,
119
+ message: ".claude/settings.json is present but is not valid JSON.",
120
+ },
121
+ ];
122
+ }
123
+ const count = countHookRegistrations(parsed);
124
+ return [
125
+ {
126
+ status: "PASS",
127
+ checkId: "hooks.settings-present",
128
+ target: settingsPath,
129
+ message: `.claude/settings.json is present and valid (${count} hook registration${count === 1 ? "" : "s"}).`,
130
+ },
131
+ // US-004+: the settings file is present and valid, so the full Claude wiring
132
+ // checks (script existence and executability, orphaned scripts, three-profile
133
+ // membership, unquoted $CLAUDE_PROJECT_DIR, and core/hooks/<Event>/ executable
134
+ // bits) extend the hooks family here without touching the engine.
135
+ ...checkClaudeWiring(context),
136
+ ];
137
+ }
138
+ /**
139
+ * Count command-hook registrations across every lifecycle event in a parsed
140
+ * `.claude/settings.json`. Tolerant of malformed shapes — any non-conforming
141
+ * branch contributes zero rather than throwing.
142
+ */
143
+ function countHookRegistrations(settings) {
144
+ if (!settings || typeof settings !== "object")
145
+ return 0;
146
+ const hooks = settings.hooks;
147
+ if (!hooks || typeof hooks !== "object")
148
+ return 0;
149
+ let count = 0;
150
+ for (const entries of Object.values(hooks)) {
151
+ if (!Array.isArray(entries))
152
+ continue;
153
+ for (const entry of entries) {
154
+ const inner = entry?.hooks;
155
+ if (Array.isArray(inner))
156
+ count += inner.length;
157
+ }
158
+ }
159
+ return count;
160
+ }
161
+ /**
162
+ * Build a registry pre-loaded with the default families. Hooks is the first
163
+ * family; later families are registered here as they are implemented, each
164
+ * one line, with no engine change.
165
+ */
166
+ export function createDefaultRegistry() {
167
+ const registry = new DoctorRegistry();
168
+ registry.register(hooksFamily);
169
+ // Fixture coverage (US-007): reports UNTESTED for every registered hook with
170
+ // no fixture and prints the tested/total coverage line. A separate family so
171
+ // it needs no change to the hooks tier, per the registry's extensibility
172
+ // contract.
173
+ registry.register(fixtureCoverageFamily);
174
+ return registry;
175
+ }
176
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Reporting for `hq doctor`: status counting, the exit-code contract, and the
3
+ * TTY-aware plain-text renderer.
4
+ *
5
+ * Three properties here are load-bearing and deliberately decoupled from any
6
+ * one check family:
7
+ *
8
+ * 1. The exit code is a function of the *results*, not of any family's
9
+ * internal logic: exit 1 iff some result is FAIL or UNKNOWN. WARN,
10
+ * UNTESTED, NA, and KNOWN-DEFECT never fail the command — a doctor that
11
+ * exits non-zero on a healthy install trains users to ignore it, which is
12
+ * the exact failure mode `hq doctor` exists to prevent.
13
+ *
14
+ * 2. UNTESTED and KNOWN-DEFECT are always visible and always counted
15
+ * *separately* from PASS. A wired-but-never-exercised hook (UNTESTED) or a
16
+ * tracked-but-unfixed defect (KNOWN-DEFECT) must never blend into the pass
17
+ * pile, because that is how a correct-looking setup reads as working
18
+ * enforcement when it is not.
19
+ *
20
+ * 3. Colour is opt-in per render (`color`), never auto-detected here. The
21
+ * command layer decides from `process.stdout.isTTY`; when colour is off the
22
+ * output carries no ANSI escape sequences at all, so a run piped to a file
23
+ * or consumed by CI has the same information content as an interactive one.
24
+ *
25
+ * The platform field is a placeholder in this story (US-015). Host platform
26
+ * detection is US-003; until it lands, {@link UNKNOWN_PLATFORM} is emitted and
27
+ * US-003 injects the real value through the same `platform` option.
28
+ */
29
+ import { type CheckResult, type DoctorStatus, type FamilyRun } from "./types.js";
30
+ /**
31
+ * The detected agent platform and the evidence for the verdict. A structural
32
+ * placeholder in US-015: host platform detection (US-003) produces the real
33
+ * value and passes it through unchanged. `evidence` is intentionally `unknown`
34
+ * so US-003 can attach whatever signal shape it settles on without a breaking
35
+ * change here.
36
+ */
37
+ export interface DoctorPlatform {
38
+ /** Platform id, e.g. `claude-code-cli`, `codex`, `grok`, or `unknown`. */
39
+ id: string;
40
+ /** Evidence that led to the verdict. Shape owned by US-003. */
41
+ evidence?: unknown;
42
+ }
43
+ /** The placeholder platform emitted until host detection (US-003) lands. */
44
+ export declare const UNKNOWN_PLATFORM: DoctorPlatform;
45
+ /** Per-status tallies across every result of a run. */
46
+ export type StatusCounts = Record<DoctorStatus, number>;
47
+ /**
48
+ * The statuses that fail the command. This is the whole exit-code contract:
49
+ * only FAIL and UNKNOWN are here. UNKNOWN counts as failing because "could not
50
+ * be determined" must never be mistaken for "verified working".
51
+ */
52
+ export declare const FAILING_STATUSES: readonly DoctorStatus[];
53
+ /** Whether a status fails the command per {@link FAILING_STATUSES}. */
54
+ export declare function isFailingStatus(status: DoctorStatus): boolean;
55
+ /** Flatten grouped family runs into a single ordered result list. */
56
+ export declare function flattenFamilies(families: FamilyRun[]): CheckResult[];
57
+ /** A zeroed {@link StatusCounts} with an entry for every status. */
58
+ export declare function emptyStatusCounts(): StatusCounts;
59
+ /** Tally results by status across all families. */
60
+ export declare function summarize(families: FamilyRun[]): StatusCounts;
61
+ /**
62
+ * The process exit code for a run: 1 if any result is FAIL or UNKNOWN, else 0.
63
+ * Independent of rendering so callers and tests can assert the contract without
64
+ * parsing output.
65
+ */
66
+ export declare function computeExitCode(families: FamilyRun[]): number;
67
+ /** Options for {@link renderText}. */
68
+ export interface RenderTextOptions {
69
+ /** The resolved HQ root, printed in the header. */
70
+ hqRoot: string;
71
+ /** Grouped per-family results, rendered in order. */
72
+ families: FamilyRun[];
73
+ /** Detected platform. Defaults to {@link UNKNOWN_PLATFORM}. */
74
+ platform?: DoctorPlatform;
75
+ /** Also print every PASS result. Default: false. */
76
+ verbose?: boolean;
77
+ /** Emit ANSI colour. Default: false (no escape sequences). */
78
+ color?: boolean;
79
+ }
80
+ /**
81
+ * Render the plain-text report. Results are grouped by family; PASS results are
82
+ * hidden unless `verbose`, while every non-PASS result — including UNTESTED and
83
+ * KNOWN-DEFECT — is always shown. A summary block reports per-status counts and
84
+ * states the exit outcome.
85
+ */
86
+ export declare function renderText(options: RenderTextOptions): string;
87
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Reporting for `hq doctor`: status counting, the exit-code contract, and the
3
+ * TTY-aware plain-text renderer.
4
+ *
5
+ * Three properties here are load-bearing and deliberately decoupled from any
6
+ * one check family:
7
+ *
8
+ * 1. The exit code is a function of the *results*, not of any family's
9
+ * internal logic: exit 1 iff some result is FAIL or UNKNOWN. WARN,
10
+ * UNTESTED, NA, and KNOWN-DEFECT never fail the command — a doctor that
11
+ * exits non-zero on a healthy install trains users to ignore it, which is
12
+ * the exact failure mode `hq doctor` exists to prevent.
13
+ *
14
+ * 2. UNTESTED and KNOWN-DEFECT are always visible and always counted
15
+ * *separately* from PASS. A wired-but-never-exercised hook (UNTESTED) or a
16
+ * tracked-but-unfixed defect (KNOWN-DEFECT) must never blend into the pass
17
+ * pile, because that is how a correct-looking setup reads as working
18
+ * enforcement when it is not.
19
+ *
20
+ * 3. Colour is opt-in per render (`color`), never auto-detected here. The
21
+ * command layer decides from `process.stdout.isTTY`; when colour is off the
22
+ * output carries no ANSI escape sequences at all, so a run piped to a file
23
+ * or consumed by CI has the same information content as an interactive one.
24
+ *
25
+ * The platform field is a placeholder in this story (US-015). Host platform
26
+ * detection is US-003; until it lands, {@link UNKNOWN_PLATFORM} is emitted and
27
+ * US-003 injects the real value through the same `platform` option.
28
+ */
29
+ import { Chalk } from "chalk";
30
+ import { DOCTOR_STATUSES, } from "./types.js";
31
+ import { FIXTURE_COVERAGE_CHECK_ID } from "./fixtures/discover.js";
32
+ /** The placeholder platform emitted until host detection (US-003) lands. */
33
+ export const UNKNOWN_PLATFORM = { id: "unknown" };
34
+ /**
35
+ * The statuses that fail the command. This is the whole exit-code contract:
36
+ * only FAIL and UNKNOWN are here. UNKNOWN counts as failing because "could not
37
+ * be determined" must never be mistaken for "verified working".
38
+ */
39
+ export const FAILING_STATUSES = ["FAIL", "UNKNOWN"];
40
+ /** Whether a status fails the command per {@link FAILING_STATUSES}. */
41
+ export function isFailingStatus(status) {
42
+ return FAILING_STATUSES.includes(status);
43
+ }
44
+ /** Flatten grouped family runs into a single ordered result list. */
45
+ export function flattenFamilies(families) {
46
+ return families.flatMap((run) => run.results);
47
+ }
48
+ /** A zeroed {@link StatusCounts} with an entry for every status. */
49
+ export function emptyStatusCounts() {
50
+ const counts = {};
51
+ for (const status of DOCTOR_STATUSES)
52
+ counts[status] = 0;
53
+ return counts;
54
+ }
55
+ /** Tally results by status across all families. */
56
+ export function summarize(families) {
57
+ const counts = emptyStatusCounts();
58
+ for (const result of flattenFamilies(families))
59
+ counts[result.status] += 1;
60
+ return counts;
61
+ }
62
+ /**
63
+ * The process exit code for a run: 1 if any result is FAIL or UNKNOWN, else 0.
64
+ * Independent of rendering so callers and tests can assert the contract without
65
+ * parsing output.
66
+ */
67
+ export function computeExitCode(families) {
68
+ return flattenFamilies(families).some((result) => isFailingStatus(result.status))
69
+ ? 1
70
+ : 0;
71
+ }
72
+ /** Width the status label is padded to so result lines align. */
73
+ const STATUS_LABEL_WIDTH = Math.max(...DOCTOR_STATUSES.map((s) => s.length));
74
+ /**
75
+ * Build a status colouriser. When `color` is false a level-0 Chalk instance is
76
+ * used, which returns its input verbatim — guaranteeing zero ANSI output on the
77
+ * non-TTY path regardless of the ambient environment.
78
+ */
79
+ function buildPainter(color) {
80
+ const chalk = new Chalk({ level: color ? 1 : 0 });
81
+ const paint = {
82
+ PASS: (text) => chalk.green(text),
83
+ FAIL: (text) => chalk.red(text),
84
+ WARN: (text) => chalk.yellow(text),
85
+ UNTESTED: (text) => chalk.cyan(text),
86
+ NA: (text) => chalk.gray(text),
87
+ UNKNOWN: (text) => chalk.magenta(text),
88
+ "KNOWN-DEFECT": (text) => chalk.blue(text),
89
+ };
90
+ return (status, text) => paint[status](text);
91
+ }
92
+ /**
93
+ * Render the plain-text report. Results are grouped by family; PASS results are
94
+ * hidden unless `verbose`, while every non-PASS result — including UNTESTED and
95
+ * KNOWN-DEFECT — is always shown. A summary block reports per-status counts and
96
+ * states the exit outcome.
97
+ */
98
+ export function renderText(options) {
99
+ const { hqRoot, families } = options;
100
+ const platform = options.platform ?? UNKNOWN_PLATFORM;
101
+ const verbose = options.verbose ?? false;
102
+ const paint = buildPainter(options.color ?? false);
103
+ const pad = " ".repeat(STATUS_LABEL_WIDTH);
104
+ const lines = [];
105
+ lines.push(`hq doctor — HQ root: ${hqRoot}`);
106
+ lines.push(`Platform: ${platform.id}`);
107
+ for (const run of families) {
108
+ lines.push("");
109
+ lines.push(`${run.family.title} (${run.family.id})`);
110
+ if (run.results.length === 0) {
111
+ lines.push(" (no results)");
112
+ continue;
113
+ }
114
+ const visible = run.results.filter((result) => verbose || result.status !== "PASS");
115
+ if (visible.length === 0) {
116
+ const n = run.results.length;
117
+ lines.push(` all ${n} check${n === 1 ? "" : "s"} passing (use --verbose to list)`);
118
+ continue;
119
+ }
120
+ for (const result of visible) {
121
+ const label = paint(result.status, result.status.padEnd(STATUS_LABEL_WIDTH));
122
+ const target = result.target ? ` [${result.target}]` : "";
123
+ lines.push(` ${label} ${result.checkId}${target}: ${result.message}`);
124
+ if (result.remediation) {
125
+ lines.push(` ${pad} remediation: ${result.remediation}`);
126
+ }
127
+ }
128
+ }
129
+ lines.push("");
130
+ lines.push(renderSummary(families, paint));
131
+ return lines.join("\n") + "\n";
132
+ }
133
+ /**
134
+ * The summary block: a per-status count line in canonical status order, an
135
+ * explicit note that UNTESTED and KNOWN-DEFECT are never counted as passing,
136
+ * and the exit outcome. Rendered from {@link summarize}/{@link computeExitCode}
137
+ * so it can never disagree with the machine-readable contract.
138
+ */
139
+ function renderSummary(families, paint) {
140
+ const counts = summarize(families);
141
+ const total = DOCTOR_STATUSES.reduce((sum, status) => sum + counts[status], 0);
142
+ const parts = DOCTOR_STATUSES.map((status) => `${paint(status, status)} ${counts[status]}`);
143
+ const lines = [
144
+ `Summary (${total} check${total === 1 ? "" : "s"}): ${parts.join(" ")}`,
145
+ `UNTESTED (${counts.UNTESTED}) and KNOWN-DEFECT (${counts["KNOWN-DEFECT"]}) ` +
146
+ `are reported but never counted as passing.`,
147
+ ];
148
+ // Surface the fixture-coverage ratio (US-007) in the summary — the forcing
149
+ // function that keeps a mostly-unfixtured doctor from reading as a clean bill
150
+ // of health. Emitted by the fixture-coverage family; absent only when that
151
+ // family did not run.
152
+ const coverage = flattenFamilies(families).find((result) => result.checkId === FIXTURE_COVERAGE_CHECK_ID);
153
+ if (coverage)
154
+ lines.push(coverage.message);
155
+ if (computeExitCode(families) === 0) {
156
+ lines.push("Result: OK — no FAIL or UNKNOWN results (exit 0).");
157
+ }
158
+ else {
159
+ lines.push(`Result: ATTENTION — ${counts.FAIL} FAIL, ${counts.UNKNOWN} UNKNOWN ` +
160
+ `(exit 1).`);
161
+ }
162
+ return lines.join("\n");
163
+ }
164
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Core doctor types: the status vocabulary and the check-registry contract.
3
+ *
4
+ * The status vocabulary is the load-bearing design decision of `hq doctor`.
5
+ * UNTESTED (wired but never exercised), NA (untestable on this platform),
6
+ * UNKNOWN (could not be determined), and KNOWN-DEFECT (tracked and unfixed) must
7
+ * never collapse into PASS — that separation is what keeps the tool honest and
8
+ * stops a correct-looking configuration from reading as working enforcement.
9
+ *
10
+ * `DoctorStatus` is derived from the `DOCTOR_STATUSES` tuple, so the union is
11
+ * closed: any consumer that assigns a value outside the tuple is a compile
12
+ * error, and the vocabulary can only grow by editing the tuple in one place.
13
+ */
14
+ /** The complete, fixed status vocabulary, in canonical display order. */
15
+ export declare const DOCTOR_STATUSES: readonly ["PASS", "FAIL", "WARN", "UNTESTED", "NA", "UNKNOWN", "KNOWN-DEFECT"];
16
+ /**
17
+ * A doctor result status. Closed union derived from {@link DOCTOR_STATUSES};
18
+ * the type system rejects any other string at every call site.
19
+ */
20
+ export type DoctorStatus = (typeof DOCTOR_STATUSES)[number];
21
+ /** Runtime type guard: true only for a member of the fixed vocabulary. */
22
+ export declare function isDoctorStatus(value: unknown): value is DoctorStatus;
23
+ /** A single per-item result produced by a check family. */
24
+ export interface CheckResult {
25
+ /** Status drawn from the fixed vocabulary. */
26
+ status: DoctorStatus;
27
+ /** Stable machine id for this item, e.g. `hooks.settings-present`. */
28
+ checkId: string;
29
+ /** What was inspected — a path, hook id, etc. Optional. */
30
+ target?: string;
31
+ /** Human-readable one-line explanation of the result. */
32
+ message: string;
33
+ /** Optional remediation hint (a command or instruction). */
34
+ remediation?: string;
35
+ }
36
+ /**
37
+ * The context handed to every check family's `run`. The resolved HQ root is
38
+ * exposed here so no family re-implements tree discovery; later stories extend
39
+ * this interface (e.g. detected platform, session id) without touching the
40
+ * engine, since adding an optional field is non-breaking.
41
+ */
42
+ export interface CheckContext {
43
+ /** Absolute, resolved HQ tree root every check reads from. */
44
+ hqRoot: string;
45
+ /**
46
+ * The detected host platform (id, and optionally the evidence behind it).
47
+ * Host-specific live checks — the US-006 runtime probe — key their
48
+ * UNKNOWN/FAIL behaviour off this. Absent means host detection has not run and
49
+ * is treated as `unknown`. Kept structurally minimal so the engine does not
50
+ * depend on the platform module.
51
+ */
52
+ platform?: {
53
+ id: string;
54
+ evidence?: unknown;
55
+ };
56
+ /**
57
+ * The session id to scope runtime/ledger checks to (US-006 `--session-id`),
58
+ * so an older session's ledger cannot be mistaken for the current runtime.
59
+ * Absent means "any session's ledger counts".
60
+ */
61
+ sessionId?: string;
62
+ }
63
+ /**
64
+ * A check family: an id, a human title, and an async `run` returning per-item
65
+ * results. Adding a new family (vault, sync, MCP, secrets, qmd freshness, …)
66
+ * means implementing this interface and registering the object — the engine
67
+ * needs no changes.
68
+ */
69
+ export interface CheckFamily {
70
+ /** Stable family id, e.g. `hooks`. Unique within a registry. */
71
+ id: string;
72
+ /** Human-readable family title for grouped output. */
73
+ title: string;
74
+ /** Run this family's checks against the resolved tree. */
75
+ run: (context: CheckContext) => Promise<CheckResult[]>;
76
+ }
77
+ /** The grouped results of running one family. */
78
+ export interface FamilyRun {
79
+ /** Identifying metadata for the family that produced these results. */
80
+ family: {
81
+ id: string;
82
+ title: string;
83
+ };
84
+ /** The per-item results the family returned. */
85
+ results: CheckResult[];
86
+ }
87
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Core doctor types: the status vocabulary and the check-registry contract.
3
+ *
4
+ * The status vocabulary is the load-bearing design decision of `hq doctor`.
5
+ * UNTESTED (wired but never exercised), NA (untestable on this platform),
6
+ * UNKNOWN (could not be determined), and KNOWN-DEFECT (tracked and unfixed) must
7
+ * never collapse into PASS — that separation is what keeps the tool honest and
8
+ * stops a correct-looking configuration from reading as working enforcement.
9
+ *
10
+ * `DoctorStatus` is derived from the `DOCTOR_STATUSES` tuple, so the union is
11
+ * closed: any consumer that assigns a value outside the tuple is a compile
12
+ * error, and the vocabulary can only grow by editing the tuple in one place.
13
+ */
14
+ /** The complete, fixed status vocabulary, in canonical display order. */
15
+ export const DOCTOR_STATUSES = [
16
+ "PASS",
17
+ "FAIL",
18
+ "WARN",
19
+ "UNTESTED",
20
+ "NA",
21
+ "UNKNOWN",
22
+ "KNOWN-DEFECT",
23
+ ];
24
+ /** Runtime type guard: true only for a member of the fixed vocabulary. */
25
+ export function isDoctorStatus(value) {
26
+ return (typeof value === "string" &&
27
+ DOCTOR_STATUSES.includes(value));
28
+ }
29
+ //# sourceMappingURL=types.js.map
package/dist/main.js CHANGED
@@ -60,6 +60,7 @@ import { registerDbCommand } from "./commands/db.js";
60
60
  import { registerCoreCommands } from "./commands/core.js";
61
61
  import { registerSearchCommand } from "./commands/search.js";
62
62
  import { registerIndexCommand } from "./commands/index-cmd.js";
63
+ import { registerDoctorCommand } from "./commands/doctor.js";
63
64
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
64
65
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
65
66
  import { networkTransportErrorMessage } from "./utils/network-transport-error.js";
@@ -237,6 +238,11 @@ registerCoreCommands(program);
237
238
  // which converges scaffold-owned files and hooks rather than search data.
238
239
  registerSearchCommand(program);
239
240
  registerIndexCommand(program);
241
+ // Hook guardrail diagnostics (top-level — `hq doctor`). Read-only, offline
242
+ // verification that HQ's hooks are wired and firing, backed by an extensible
243
+ // check registry so later check families (vault, sync, MCP, …) plug in without
244
+ // engine changes.
245
+ registerDoctorCommand(program);
240
246
  program.hook("preAction", async () => {
241
247
  await emitCliSessionStarted();
242
248
  });
@@ -5,8 +5,8 @@ import semver from "semver";
5
5
  import { CLI_VERSION } from "../cli-version.js";
6
6
  const PACKAGE_NAME = "@indigoai-us/hq-cli";
7
7
  const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
8
- const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
9
- const CACHE_TTL_JITTER_MS = 60 * 60 * 1000;
8
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1h — catch fresh releases within the hour
9
+ const CACHE_TTL_JITTER_MS = 5 * 60 * 1000; // up to 5m early, to spread a fleet's refreshes off a single instant
10
10
  const FETCH_TIMEOUT_MS = 3_000;
11
11
  const REFRESH_LOCK_STALE_MS = 10 * 60 * 1000;
12
12
  function cachePath() {
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Why both?
9
9
  * - `version-check.ts` answers "is there something newer?" by polling npm
10
- * directly. It's a soft hint, lives on a 24h cache, and never blocks.
10
+ * directly. It's a soft hint, lives on a 1h cache, and never blocks.
11
11
  * - `version-gate.ts` answers "is the team currently allowing your version
12
12
  * to run?" via an authoritative hq-pro endpoint. The server can yank a
13
13
  * known-bad release without waiting for the npm `latest` dist-tag move.
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Why both?
9
9
  * - `version-check.ts` answers "is there something newer?" by polling npm
10
- * directly. It's a soft hint, lives on a 24h cache, and never blocks.
10
+ * directly. It's a soft hint, lives on a 1h cache, and never blocks.
11
11
  * - `version-gate.ts` answers "is the team currently allowing your version
12
12
  * to run?" via an authoritative hq-pro endpoint. The server can yank a
13
13
  * known-bad release without waiting for the npm `latest` dist-tag move.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.98.3",
3
+ "version": "5.99.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {