@indigoai-us/hq-cli 5.307.0 → 5.309.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/assets/scaffold/core/scripts/resolve-company.sh +199 -0
  3. package/dist/command-catalog.generated.d.ts +13 -0
  4. package/dist/command-catalog.generated.js +16 -0
  5. package/dist/commands/core-resolve-company-command.d.ts +4 -0
  6. package/dist/commands/core-resolve-company-command.js +42 -0
  7. package/dist/commands/core-resolve-company-native.d.ts +28 -0
  8. package/dist/commands/core-resolve-company-native.js +408 -0
  9. package/dist/commands/core.js +3 -0
  10. package/dist/commands/lanes-checks.d.ts +7 -1
  11. package/dist/commands/lanes-checks.js +68 -18
  12. package/dist/commands/lanes-report.js +5 -2
  13. package/dist/fast-scaffold.js +12 -0
  14. package/dist/lib/doctor/checks/codex-wiring.d.ts +0 -4
  15. package/dist/lib/doctor/checks/codex-wiring.js +2 -2
  16. package/dist/lib/doctor/checks/runtime-health.d.ts +0 -17
  17. package/dist/lib/doctor/checks/runtime-health.js +4 -4
  18. package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +0 -1
  19. package/dist/lib/doctor/checks/scaffold-cli-floor.js +1 -1
  20. package/dist/lib/doctor/compat.d.ts +0 -16
  21. package/dist/lib/doctor/compat.js +3 -3
  22. package/dist/lib/doctor/platform.d.ts +0 -26
  23. package/dist/lib/doctor/platform.js +5 -5
  24. package/dist/lib/lanes/autopilot.js +122 -102
  25. package/dist/lib/onboarding/errors.d.ts +2 -1
  26. package/dist/lib/onboarding/errors.js +1 -1
  27. package/dist/lib/onboarding/index.d.ts +2 -6
  28. package/dist/lib/onboarding/index.js +2 -5
  29. package/dist/lib/search-index/embed-lock.d.ts +0 -7
  30. package/dist/lib/search-index/embed-lock.js +1 -1
  31. package/dist/lib/search-index/load-gate.d.ts +0 -8
  32. package/dist/lib/search-index/load-gate.js +3 -3
  33. package/dist/lib/search-index/max-doc-bytes.d.ts +2 -3
  34. package/dist/lib/search-index/max-doc-bytes.js +1 -1
  35. package/package.json +1 -1
@@ -41,10 +41,6 @@
41
41
  */
42
42
  import { type AllowedDivergenceLoad } from "../allowed-divergence.js";
43
43
  import type { CheckContext, CheckResult } from "../types.js";
44
- /** Basename of the single-adapter script the current architecture routes to. */
45
- export declare const CODEX_ADAPTER_BASENAME = "hq-codex-hook-adapter.sh";
46
- /** Library the adapter sources to dispatch `.claude/settings.json` live. */
47
- export declare const CODEX_DISPATCH_LIB_RELPATH: string;
48
44
  /**
49
45
  * Lifecycle events the shipped `.codex/config.toml` routes through the adapter.
50
46
  * A missing event means the Claude hooks for that event never run under Codex.
@@ -44,9 +44,9 @@ import * as path from "node:path";
44
44
  import { parse as parseToml } from "smol-toml";
45
45
  import { ALLOWED_DIVERGENCE_RELPATH, loadAllowedDivergence, } from "../allowed-divergence.js";
46
46
  /** Basename of the single-adapter script the current architecture routes to. */
47
- export const CODEX_ADAPTER_BASENAME = "hq-codex-hook-adapter.sh";
47
+ const CODEX_ADAPTER_BASENAME = "hq-codex-hook-adapter.sh";
48
48
  /** Library the adapter sources to dispatch `.claude/settings.json` live. */
49
- export const CODEX_DISPATCH_LIB_RELPATH = path.join("core", "scripts", "lib", "hook-adapter-core.sh");
49
+ const CODEX_DISPATCH_LIB_RELPATH = path.join("core", "scripts", "lib", "hook-adapter-core.sh");
50
50
  /**
51
51
  * Lifecycle events the shipped `.codex/config.toml` routes through the adapter.
52
52
  * A missing event means the Claude hooks for that event never run under Codex.
@@ -28,16 +28,6 @@
28
28
  import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
29
29
  /** The id of the AI-runtime-health family. */
30
30
  export declare const RUNTIMES_FAMILY_ID = "runtimes";
31
- /** Common id prefix for every result this family emits. */
32
- export declare const RUNTIMES_PREFIX = "runtimes";
33
- /**
34
- * The deterministic one-line probe prompt. Health is proven by a round-trip
35
- * (exit 0 plus non-empty output), not by exact-matching the reply, so a model
36
- * that answers with anything at all still passes.
37
- */
38
- export declare const PROBE_PROMPT = "Reply with exactly: OK";
39
- /** How long each live probe may run before it is killed and marked UNKNOWN. */
40
- export declare const DEFAULT_PROBE_TIMEOUT_MS = 120000;
41
31
  /** One AI runtime the doctor knows how to find and probe. */
42
32
  export interface RuntimeSpec {
43
33
  /** Stable key used in check ids, e.g. `runtimes.codex.binary`. */
@@ -94,11 +84,4 @@ export interface RuntimeHealthDeps {
94
84
  export declare function checkRuntimeHealth(context: CheckContext, deps?: RuntimeHealthDeps): Promise<CheckResult[]>;
95
85
  /** The registered family object. */
96
86
  export declare const runtimeHealthFamily: CheckFamily;
97
- /**
98
- * Resolve an executable name against PATH with a pure filesystem scan — no
99
- * process is spawned, keeping the doctor's default run a function of on-disk
100
- * shape. First PATH entry containing an executable regular file wins, which is
101
- * exactly the copy a shell would run.
102
- */
103
- export declare function resolveOnPath(binary: string): string | null;
104
87
  //# sourceMappingURL=runtime-health.d.ts.map
@@ -33,15 +33,15 @@ import { planCommandSpawn } from "../../../utils/windows-spawn.js";
33
33
  /** The id of the AI-runtime-health family. */
34
34
  export const RUNTIMES_FAMILY_ID = "runtimes";
35
35
  /** Common id prefix for every result this family emits. */
36
- export const RUNTIMES_PREFIX = "runtimes";
36
+ const RUNTIMES_PREFIX = "runtimes";
37
37
  /**
38
38
  * The deterministic one-line probe prompt. Health is proven by a round-trip
39
39
  * (exit 0 plus non-empty output), not by exact-matching the reply, so a model
40
40
  * that answers with anything at all still passes.
41
41
  */
42
- export const PROBE_PROMPT = "Reply with exactly: OK";
42
+ const PROBE_PROMPT = "Reply with exactly: OK";
43
43
  /** How long each live probe may run before it is killed and marked UNKNOWN. */
44
- export const DEFAULT_PROBE_TIMEOUT_MS = 120_000;
44
+ const DEFAULT_PROBE_TIMEOUT_MS = 120_000;
45
45
  /**
46
46
  * The three runtimes HQ wires hooks for. Codex runs its probe under its own
47
47
  * read-only sandbox and outside-a-repo mode so the probe can never write; the
@@ -243,7 +243,7 @@ export const runtimeHealthFamily = {
243
243
  * shape. First PATH entry containing an executable regular file wins, which is
244
244
  * exactly the copy a shell would run.
245
245
  */
246
- export function resolveOnPath(binary) {
246
+ function resolveOnPath(binary) {
247
247
  const pathVar = process.env.PATH ?? "";
248
248
  for (const dir of pathVar.split(path.delimiter)) {
249
249
  if (!dir)
@@ -1,6 +1,5 @@
1
1
  import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
2
2
  export declare const SCAFFOLD_CLI_FLOOR_FAMILY_ID = "scaffold-cli-floor";
3
- export declare const SCAFFOLD_CLI_FLOOR_FAMILY_TITLE = "Scaffold CLI compatibility";
4
3
  export declare const SCAFFOLD_CLI_FLOOR_CHECK_ID = "scaffold-cli-floor.requires-hq-cli";
5
4
  export declare const scaffoldCliFloorFamily: CheckFamily;
6
5
  export declare function checkScaffoldCliFloor(context: CheckContext): CheckResult[];
@@ -4,7 +4,7 @@ import * as yaml from "js-yaml";
4
4
  import { CLI_VERSION } from "../../../cli-version.js";
5
5
  import { compareScaffoldCliFloor } from "../../scaffold-cli-floor.js";
6
6
  export const SCAFFOLD_CLI_FLOOR_FAMILY_ID = "scaffold-cli-floor";
7
- export const SCAFFOLD_CLI_FLOOR_FAMILY_TITLE = "Scaffold CLI compatibility";
7
+ const SCAFFOLD_CLI_FLOOR_FAMILY_TITLE = "Scaffold CLI compatibility";
8
8
  export const SCAFFOLD_CLI_FLOOR_CHECK_ID = "scaffold-cli-floor.requires-hq-cli";
9
9
  export const scaffoldCliFloorFamily = {
10
10
  id: SCAFFOLD_CLI_FLOOR_FAMILY_ID,
@@ -31,22 +31,6 @@ import type { DoctorJsonDocument } from "./json-output.js";
31
31
  export declare const CHECK_HQ_HOOKS_PASS_EXIT = 0;
32
32
  /** Exit code check-hq-hooks.sh returns on a health failure. Preserved. */
33
33
  export declare const CHECK_HQ_HOOKS_FAIL_EXIT = 2;
34
- /** Exit code check-hq-hooks.sh returns on a usage error. Preserved. */
35
- export declare const CHECK_HQ_HOOKS_USAGE_EXIT = 64;
36
- /** The runtime-probe result id: whether hooks actually fired (the ledger). */
37
- export declare const RUNTIME_ENFORCEMENT_CHECK_ID = "hooks.runtime.enforcement";
38
- /**
39
- * The `hq doctor` check ids that correspond to check-hq-hooks.sh's *settings
40
- * load* scope — the subset of the hooks family the wrapper is allowed to fail
41
- * on. Deliberately narrow: it covers a missing/invalid settings file, an
42
- * unquoted `$CLAUDE_PROJECT_DIR` that word-splits on a spaced root, and a hook
43
- * command that runs a script which is not on disk — and nothing else. The many
44
- * richer doctor findings (executable bits, orphaned scripts, three-profile
45
- * membership, Codex/Grok parity, fixture coverage) are intentionally OUT of
46
- * scope, so the wrapper stays byte-for-byte compatible with callers that only
47
- * ever cared whether the hook configuration could load.
48
- */
49
- export declare const CHECK_HQ_HOOKS_SETTINGS_SCOPE: readonly string[];
50
34
  /**
51
35
  * Whether hook dispatch was observed this session. Mirrors the wrapper's
52
36
  * `HQ runtime enforcement: …` line. `NOT CHECKED` is the default when
@@ -31,9 +31,9 @@ export const CHECK_HQ_HOOKS_PASS_EXIT = 0;
31
31
  /** Exit code check-hq-hooks.sh returns on a health failure. Preserved. */
32
32
  export const CHECK_HQ_HOOKS_FAIL_EXIT = 2;
33
33
  /** Exit code check-hq-hooks.sh returns on a usage error. Preserved. */
34
- export const CHECK_HQ_HOOKS_USAGE_EXIT = 64;
34
+ const CHECK_HQ_HOOKS_USAGE_EXIT = 64;
35
35
  /** The runtime-probe result id: whether hooks actually fired (the ledger). */
36
- export const RUNTIME_ENFORCEMENT_CHECK_ID = "hooks.runtime.enforcement";
36
+ const RUNTIME_ENFORCEMENT_CHECK_ID = "hooks.runtime.enforcement";
37
37
  /**
38
38
  * The `hq doctor` check ids that correspond to check-hq-hooks.sh's *settings
39
39
  * load* scope — the subset of the hooks family the wrapper is allowed to fail
@@ -45,7 +45,7 @@ export const RUNTIME_ENFORCEMENT_CHECK_ID = "hooks.runtime.enforcement";
45
45
  * scope, so the wrapper stays byte-for-byte compatible with callers that only
46
46
  * ever cared whether the hook configuration could load.
47
47
  */
48
- export const CHECK_HQ_HOOKS_SETTINGS_SCOPE = [
48
+ const CHECK_HQ_HOOKS_SETTINGS_SCOPE = [
49
49
  "hooks.settings-present",
50
50
  "hooks.settings-required-command-hooks",
51
51
  "hooks.settings-valid-json",
@@ -126,24 +126,8 @@ export declare function claudeCodeFlagEvidence(env: NodeJS.ProcessEnv): Platform
126
126
  * `com.googlecode.iterm2`), so this fires only under a real desktop host.
127
127
  */
128
128
  export declare function bundleIdEvidence(env: NodeJS.ProcessEnv): PlatformEvidence | null;
129
- /**
130
- * Codex runtime environment variables, kept in parity with the canonical shell
131
- * detector `core/scripts/lib/detect-codex.sh`. Presence of any is a strong Codex
132
- * signal. One evidence entry is produced per variable found so a single Codex
133
- * signal can be tested in isolation.
134
- */
135
- export declare const CODEX_ENV_VARS: readonly ["CODEX_SANDBOX", "CODEX_SESSION_ID", "CODEX_EXECUTION_ID", "CODEX_AGENT_ID", "CODEX_THREAD_ID", "CODEX_SHELL", "CODEX_CI", "CODEX_INTERNAL_ORIGINATOR_OVERRIDE", "OPENAI_CODEX"];
136
129
  /** Every Codex environment signal present in `env`, one entry per variable. */
137
130
  export declare function codexEnvEvidence(env: NodeJS.ProcessEnv): PlatformEvidence[];
138
- /**
139
- * Grok runtime environment variables. Restricted to variables the Grok host
140
- * exports into the session (a session id, its workspace root) — HQ's own Grok
141
- * *launcher* variables (`GROK_BIN`, `GROK_CONFIG`, `XAI_API_KEY`, …) are
142
- * excluded on purpose: those can be set inside a Claude or Codex session that is
143
- * merely preparing to launch Grok, and treating them as "we are running under
144
- * Grok" would be a false positive.
145
- */
146
- export declare const GROK_ENV_VARS: readonly ["GROK_SESSION_ID", "GROK_WORKSPACE_ROOT"];
147
131
  /** Every Grok environment signal present in `env`, one entry per variable. */
148
132
  export declare function grokEnvEvidence(env: NodeJS.ProcessEnv): PlatformEvidence[];
149
133
  /**
@@ -157,10 +141,6 @@ export declare function grokEnvEvidence(env: NodeJS.ProcessEnv): PlatformEvidenc
157
141
  * on its environment signals instead, and this function does not guess it.
158
142
  */
159
143
  export declare function ancestryEvidence(ancestors: AncestorProcess[]): PlatformEvidence[];
160
- /** The one command this module ever launches to read process ancestry. */
161
- export declare const PS_COMMAND = "ps";
162
- /** `ps` arguments: every process as `pid ppid comm`, header-suppressed. */
163
- export declare const PS_ARGS: readonly ["-Ao", "pid=,ppid=,comm="];
164
144
  /**
165
145
  * Low-level process-table provider: runs a command and returns its stdout, or
166
146
  * `null` on failure. This is the single seam through which any process is
@@ -186,12 +166,6 @@ export interface ReadAncestryOptions {
186
166
  * environment signals rather than throwing.
187
167
  */
188
168
  export declare function readProcessAncestry(options?: ReadAncestryOptions): AncestorProcess[];
189
- /**
190
- * Collect the environment signals, in a stable order: entrypoint, the Claude
191
- * flag, bundle id, Codex vars, then Grok vars. Process ancestry is collected
192
- * separately (and lazily) by {@link detectPlatform}.
193
- */
194
- export declare function collectEnvEvidence(env: NodeJS.ProcessEnv): PlatformEvidence[];
195
169
  /**
196
170
  * Collect every signal from an environment and an explicit ancestor chain, in a
197
171
  * stable order: environment signals first, then process ancestry. Exposed for
@@ -150,7 +150,7 @@ function looksLikeClaudeDesktopBundle(id) {
150
150
  * signal. One evidence entry is produced per variable found so a single Codex
151
151
  * signal can be tested in isolation.
152
152
  */
153
- export const CODEX_ENV_VARS = [
153
+ const CODEX_ENV_VARS = [
154
154
  "CODEX_SANDBOX",
155
155
  "CODEX_SESSION_ID",
156
156
  "CODEX_EXECUTION_ID",
@@ -185,7 +185,7 @@ export function codexEnvEvidence(env) {
185
185
  * merely preparing to launch Grok, and treating them as "we are running under
186
186
  * Grok" would be a false positive.
187
187
  */
188
- export const GROK_ENV_VARS = ["GROK_SESSION_ID", "GROK_WORKSPACE_ROOT"];
188
+ const GROK_ENV_VARS = ["GROK_SESSION_ID", "GROK_WORKSPACE_ROOT"];
189
189
  /** Every Grok environment signal present in `env`, one entry per variable. */
190
190
  export function grokEnvEvidence(env) {
191
191
  const found = [];
@@ -249,9 +249,9 @@ const DEFAULT_MAX_HOPS = 20;
249
249
  /** Wall-clock bound for the single `ps` snapshot, keeping startup fast. */
250
250
  const PS_TIMEOUT_MS = 250;
251
251
  /** The one command this module ever launches to read process ancestry. */
252
- export const PS_COMMAND = "ps";
252
+ const PS_COMMAND = "ps";
253
253
  /** `ps` arguments: every process as `pid ppid comm`, header-suppressed. */
254
- export const PS_ARGS = ["-Ao", "pid=,ppid=,comm="];
254
+ const PS_ARGS = ["-Ao", "pid=,ppid=,comm="];
255
255
  /**
256
256
  * Read this process's ancestor chain (nearest parent first) by taking one `ps`
257
257
  * snapshot of the process table and walking parent pointers upward. A single
@@ -330,7 +330,7 @@ function defaultPsSpawner(command, args) {
330
330
  * flag, bundle id, Codex vars, then Grok vars. Process ancestry is collected
331
331
  * separately (and lazily) by {@link detectPlatform}.
332
332
  */
333
- export function collectEnvEvidence(env) {
333
+ function collectEnvEvidence(env) {
334
334
  const evidence = [];
335
335
  const entrypoint = entrypointEvidence(env);
336
336
  if (entrypoint)
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { laneRunsOnHost } from "./admission.js";
3
4
  import { appendDropboxMessage } from "./dropbox.js";
4
5
  import { LanesError } from "./errors.js";
5
6
  import { readLanesRegistryFlag } from "./registry-flag.js";
@@ -406,7 +407,7 @@ function recordOperatorEscalation(hqRoot, record) {
406
407
  async function runLeadAutopilotPassUnlocked(input) {
407
408
  const { hqRoot, host, now } = input;
408
409
  const result = { ok: true, host, evaluated: 0, sent: [], started: [], escalated: [], skipped: [] };
409
- const leads = parseRegistry(hqRoot).filter((lead) => lead.host === host || (host === "local" && lead.host === input.hostname));
410
+ const leads = parseRegistry(hqRoot);
410
411
  const lanes = readAllLanes(hqRoot);
411
412
  const byId = new Map(lanes.map((lane) => [lane.lane_id, lane]));
412
413
  const stateFile = path.join(hqRoot, "workspace", "lane-briefs", "autopilot-state.json");
@@ -422,131 +423,150 @@ async function runLeadAutopilotPassUnlocked(input) {
422
423
  continue;
423
424
  }
424
425
  const registeredLocalHost = registered.host === "local" || registered.host === input.hostname;
425
- if ((registeredLocalHost && (lead.host.kind !== "local" || lead.host.id !== input.hostname)) ||
426
- (!registeredLocalHost && (lead.host.kind !== "outpost" || lead.host.id !== registered.host || input.outpostId !== registered.host))) {
426
+ const hostMatchesRegistry = registeredLocalHost
427
+ ? lead.host.kind === "local"
428
+ : lead.host.kind === "outpost" && lead.host.id === registered.host && input.outpostId === registered.host;
429
+ if (!hostMatchesRegistry ||
430
+ !laneRunsOnHost(lead, { hostname: input.hostname, outpostId: input.outpostId })) {
427
431
  result.skipped.push({ lead_lane: lead.lane_id, reason: "lead_host_mismatch" });
428
432
  continue;
429
433
  }
430
- const enabled = await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_FLAG, { company: lead.company });
431
- if (!enabled) {
432
- result.skipped.push({ lead_lane: lead.lane_id, reason: "flag_off" });
433
- continue;
434
- }
435
- const status = readStatus(hqRoot, registered.status, lead.company.slug);
436
- const statusUpdatedAt = typeof status.updated_at === "string" && Number.isFinite(Date.parse(status.updated_at))
437
- ? new Date(Date.parse(status.updated_at)).toISOString()
438
- : undefined;
439
- result.evaluated += 1;
440
- const stories = projectStories(hqRoot, registered.prd, lead.company.slug, lead.project_id);
441
- const children = lanes.filter((lane) => lane.senior.kind === "lane" && lane.senior.id === lead.lane_id);
442
- const planned = planLeadAutopilotWakes({
443
- lead,
444
- children,
445
- stories,
446
- now,
447
- managerStatus: status,
448
- keepAliveResumed: input.keepAliveResumedLeads?.has(lead.lane_id) === true,
449
- });
450
434
  let stateChanged = false;
451
- const activeKeys = new Set(planned.map((wake) => `${lead.lane_id}:${wake.condition}`));
452
- for (const key of Object.keys(state.sent)) {
453
- if (key.startsWith(`${lead.lane_id}:`) && !activeKeys.has(key)) {
454
- delete state.sent[key];
455
- stateChanged = true;
435
+ try {
436
+ const enabled = await (input.readFlag ?? readLanesRegistryFlag)(LEAD_AUTOPILOT_FLAG, { company: lead.company });
437
+ if (!enabled) {
438
+ result.skipped.push({ lead_lane: lead.lane_id, reason: "flag_off" });
439
+ continue;
456
440
  }
457
- }
458
- for (const wake of planned) {
459
- const key = `${lead.lane_id}:${wake.condition}`;
460
- const prior = state.sent[key];
461
- if (prior?.fingerprint === wake.fingerprint) {
462
- if (input.dryRun)
463
- continue;
464
- let current = prior;
465
- const lastWakeAt = Date.parse(prior.last_wake_at ?? prior.at);
466
- if (!Number.isFinite(lastWakeAt)) {
467
- throw new LanesError("lead_autopilot_state_invalid", `Invalid wake time for ${lead.lane_id}`, { lead_lane: lead.lane_id, story_id: wake.story_id });
468
- }
469
- if (statusUpdatedAt === undefined || prior.status_updated_at === undefined) {
470
- throw new LanesError("lead_autopilot_status_activity_unverifiable", `Cannot verify whether lead ${lead.lane_id} acted after its autopilot wake`, { lead_lane: lead.lane_id, story_id: wake.story_id });
471
- }
472
- if (!prior.acted && Date.parse(statusUpdatedAt) > Date.parse(prior.status_updated_at)) {
473
- current = { ...prior, acted: true };
474
- state.sent[key] = current;
441
+ const status = readStatus(hqRoot, registered.status, lead.company.slug);
442
+ const statusUpdatedAt = typeof status.updated_at === "string" && Number.isFinite(Date.parse(status.updated_at))
443
+ ? new Date(Date.parse(status.updated_at)).toISOString()
444
+ : undefined;
445
+ const stories = projectStories(hqRoot, registered.prd, lead.company.slug, lead.project_id);
446
+ const children = lanes.filter((lane) => lane.senior.kind === "lane" && lane.senior.id === lead.lane_id);
447
+ const planned = planLeadAutopilotWakes({
448
+ lead,
449
+ children,
450
+ stories,
451
+ now,
452
+ managerStatus: status,
453
+ keepAliveResumed: input.keepAliveResumedLeads?.has(lead.lane_id) === true,
454
+ });
455
+ const activeKeys = new Set(planned.map((wake) => `${lead.lane_id}:${wake.condition}`));
456
+ for (const key of Object.keys(state.sent)) {
457
+ if (key.startsWith(`${lead.lane_id}:`) && !activeKeys.has(key)) {
458
+ delete state.sent[key];
475
459
  stateChanged = true;
476
460
  }
477
- if (wake.condition === "fill_grant" && input.startWorker && !current.acted && !current.auto_started) {
478
- const firstWakeAt = Date.parse(current.at);
479
- if (!Number.isFinite(firstWakeAt)) {
480
- throw new LanesError("lead_autopilot_state_invalid", `Invalid fill-grant wake time for ${lead.lane_id}`, { lead_lane: lead.lane_id, story_id: wake.story_id });
461
+ }
462
+ for (const wake of planned) {
463
+ const key = `${lead.lane_id}:${wake.condition}`;
464
+ const prior = state.sent[key];
465
+ if (prior?.fingerprint === wake.fingerprint) {
466
+ if (input.dryRun)
467
+ continue;
468
+ let current = prior;
469
+ const lastWakeAt = Date.parse(prior.last_wake_at ?? prior.at);
470
+ if (!Number.isFinite(lastWakeAt)) {
471
+ throw new LanesError("lead_autopilot_state_invalid", `Invalid wake time for ${lead.lane_id}`, { lead_lane: lead.lane_id, story_id: wake.story_id });
472
+ }
473
+ if (statusUpdatedAt === undefined || prior.status_updated_at === undefined) {
474
+ throw new LanesError("lead_autopilot_status_activity_unverifiable", `Cannot verify whether lead ${lead.lane_id} acted after its autopilot wake`, { lead_lane: lead.lane_id, story_id: wake.story_id });
475
+ }
476
+ if (!prior.acted && Date.parse(statusUpdatedAt) > Date.parse(prior.status_updated_at)) {
477
+ current = { ...prior, acted: true };
478
+ state.sent[key] = current;
479
+ stateChanged = true;
481
480
  }
482
- if (now.getTime() - firstWakeAt >= LEAD_AUTOPILOT_FILL_DELAY_MS) {
483
- const story = stories.find((candidate) => candidate.id === wake.story_id && candidate.status === "todo");
484
- if (!story?.briefPath) {
485
- throw new LanesError("lead_autopilot_story_brief_missing", `Cannot auto-start story ${wake.story_id}: its project record has no valid brief path`, { lead_lane: lead.lane_id, story_id: wake.story_id, project_id: lead.project_id });
481
+ if (wake.condition === "fill_grant" && input.startWorker && !current.acted && !current.auto_started) {
482
+ const firstWakeAt = Date.parse(current.at);
483
+ if (!Number.isFinite(firstWakeAt)) {
484
+ throw new LanesError("lead_autopilot_state_invalid", `Invalid fill-grant wake time for ${lead.lane_id}`, { lead_lane: lead.lane_id, story_id: wake.story_id });
486
485
  }
487
- if (lead.state !== "running" && lead.state !== "queued") {
488
- current = { ...current, acted: true };
486
+ if (now.getTime() - firstWakeAt >= LEAD_AUTOPILOT_FILL_DELAY_MS) {
487
+ const story = stories.find((candidate) => candidate.id === wake.story_id && candidate.status === "todo");
488
+ if (!story?.briefPath) {
489
+ throw new LanesError("lead_autopilot_story_brief_missing", `Cannot auto-start story ${wake.story_id}: its project record has no valid brief path`, { lead_lane: lead.lane_id, story_id: wake.story_id, project_id: lead.project_id });
490
+ }
491
+ if (lead.state !== "running" && lead.state !== "queued") {
492
+ current = { ...current, acted: true };
493
+ state.sent[key] = current;
494
+ stateChanged = true;
495
+ result.skipped.push({ lead_lane: lead.lane_id, reason: "auto_start_lead_not_active" });
496
+ }
497
+ else {
498
+ const started = await input.startWorker({ lead, story });
499
+ current = { ...current, auto_started: true, worker_lane: started.lane_id };
500
+ state.sent[key] = current;
501
+ stateChanged = true;
502
+ result.started.push({ lead_lane: lead.lane_id, story_id: story.id, worker_lane: started.lane_id });
503
+ }
504
+ }
505
+ }
506
+ if (!current.acted && !current.auto_started && current.escalated_at === undefined && now.getTime() - lastWakeAt >= LEAD_AUTOPILOT_FILL_DELAY_MS) {
507
+ const wakeCount = current.wake_count ?? 1;
508
+ if (wakeCount < 2) {
509
+ appendDropboxMessage(hqRoot, lead.lane_id, {
510
+ at: now.toISOString(),
511
+ message_id: `lead-autopilot:${wake.fingerprint}:wake-2`,
512
+ kind: "system_notice",
513
+ text: wake.prompt,
514
+ });
515
+ current = { ...current, wake_count: 2, last_wake_at: now.toISOString() };
489
516
  state.sent[key] = current;
490
517
  stateChanged = true;
491
- result.skipped.push({ lead_lane: lead.lane_id, reason: "auto_start_lead_not_active" });
518
+ result.sent.push(wake);
492
519
  }
493
520
  else {
494
- const started = await input.startWorker({ lead, story });
495
- current = { ...current, auto_started: true, worker_lane: started.lane_id };
521
+ const escalation = { team: registered.team, lead_lane: lead.lane_id, condition: wake.condition, story_id: wake.story_id, at: now.toISOString(), fingerprint: wake.fingerprint };
522
+ recordOperatorEscalation(hqRoot, escalation);
523
+ current = { ...current, escalated_at: now.toISOString() };
496
524
  state.sent[key] = current;
497
525
  stateChanged = true;
498
- result.started.push({ lead_lane: lead.lane_id, story_id: story.id, worker_lane: started.lane_id });
526
+ result.escalated.push(escalation);
499
527
  }
500
528
  }
529
+ continue;
501
530
  }
502
- if (!current.acted && !current.auto_started && current.escalated_at === undefined && now.getTime() - lastWakeAt >= LEAD_AUTOPILOT_FILL_DELAY_MS) {
503
- const wakeCount = current.wake_count ?? 1;
504
- if (wakeCount < 2) {
505
- appendDropboxMessage(hqRoot, lead.lane_id, {
506
- at: now.toISOString(),
507
- message_id: `lead-autopilot:${wake.fingerprint}:wake-2`,
508
- kind: "system_notice",
509
- text: wake.prompt,
510
- });
511
- current = { ...current, wake_count: 2, last_wake_at: now.toISOString() };
512
- state.sent[key] = current;
513
- stateChanged = true;
514
- result.sent.push(wake);
515
- }
516
- else {
517
- const escalation = { team: registered.team, lead_lane: lead.lane_id, condition: wake.condition, story_id: wake.story_id, at: now.toISOString(), fingerprint: wake.fingerprint };
518
- recordOperatorEscalation(hqRoot, escalation);
519
- current = { ...current, escalated_at: now.toISOString() };
520
- state.sent[key] = current;
521
- stateChanged = true;
522
- result.escalated.push(escalation);
523
- }
531
+ if (!input.dryRun) {
532
+ appendDropboxMessage(hqRoot, lead.lane_id, {
533
+ at: now.toISOString(),
534
+ message_id: `lead-autopilot:${wake.fingerprint}`,
535
+ kind: "system_notice",
536
+ text: wake.prompt,
537
+ });
538
+ state.sent[key] = {
539
+ fingerprint: wake.fingerprint,
540
+ at: now.toISOString(),
541
+ ...(statusUpdatedAt ? { status_updated_at: statusUpdatedAt } : {}),
542
+ wake_count: 1,
543
+ last_wake_at: now.toISOString(),
544
+ };
545
+ stateChanged = true;
524
546
  }
525
- continue;
547
+ result.sent.push(wake);
526
548
  }
527
- if (!input.dryRun) {
528
- appendDropboxMessage(hqRoot, lead.lane_id, {
529
- at: now.toISOString(),
530
- message_id: `lead-autopilot:${wake.fingerprint}`,
531
- kind: "system_notice",
532
- text: wake.prompt,
533
- });
534
- state.sent[key] = {
535
- fingerprint: wake.fingerprint,
536
- at: now.toISOString(),
537
- ...(statusUpdatedAt ? { status_updated_at: statusUpdatedAt } : {}),
538
- wake_count: 1,
539
- last_wake_at: now.toISOString(),
540
- };
541
- stateChanged = true;
542
- }
543
- result.sent.push(wake);
549
+ if (!input.dryRun && stateChanged)
550
+ writeState(stateFile, state);
551
+ result.evaluated += 1;
552
+ }
553
+ catch (error) {
554
+ if (!input.dryRun && stateChanged)
555
+ writeState(stateFile, state);
556
+ if (!(error instanceof LanesError) || LEAD_AUTOPILOT_PASS_FATAL_ERRORS.has(error.error))
557
+ throw error;
558
+ result.skipped.push({ lead_lane: lead.lane_id, reason: error.error });
544
559
  }
545
- if (!input.dryRun && stateChanged)
546
- writeState(stateFile, state);
547
560
  }
548
561
  return result;
549
562
  }
563
+ // Tenant-boundary and path-safety failures stop the whole pass; other per-lead
564
+ // data errors skip only that lead.
565
+ const LEAD_AUTOPILOT_PASS_FATAL_ERRORS = new Set([
566
+ "lead_autopilot_tenant_mismatch",
567
+ "lead_autopilot_status_path_invalid",
568
+ "lead_autopilot_escalation_record_invalid",
569
+ ]);
550
570
  const LEAD_AUTOPILOT_STALE_LOCK_MAX_AGE_MS = 60 * 60_000;
551
571
  function processStartIdentity(pid) {
552
572
  try {
@@ -4,7 +4,7 @@
4
4
  * Each failure class maps to a distinct step in the onboarding flow,
5
5
  * making it easy for the installer to show targeted recovery hints.
6
6
  */
7
- export declare class OnboardingError extends Error {
7
+ declare class OnboardingError extends Error {
8
8
  readonly step: string;
9
9
  readonly cause?: Error | undefined;
10
10
  constructor(message: string, step: string, cause?: Error | undefined);
@@ -30,4 +30,5 @@ export declare class InviteAcceptError extends OnboardingError {
30
30
  export declare class StsVerifyError extends OnboardingError {
31
31
  constructor(message: string, cause?: Error);
32
32
  }
33
+ export {};
33
34
  //# sourceMappingURL=errors.d.ts.map
@@ -4,7 +4,7 @@
4
4
  * Each failure class maps to a distinct step in the onboarding flow,
5
5
  * making it easy for the installer to show targeted recovery hints.
6
6
  */
7
- export class OnboardingError extends Error {
7
+ class OnboardingError extends Error {
8
8
  step;
9
9
  cause;
10
10
  constructor(message, step, cause) {
@@ -7,11 +7,7 @@
7
7
  * invite/accept into create-company and join-company flows with
8
8
  * checkpoint/resume.
9
9
  */
10
- export { createCompanyFlow, joinCompanyFlow, resumeOnboarding, onboardingContract, } from "./orchestrator.js";
11
- export { readCheckpoint, writeCheckpoint, deleteCheckpoint, getCheckpointPath, isStepComplete, } from "./checkpoint.js";
12
- export { OnboardingError, PersonCreationError, CompanyCreationError, ProvisioningError, MembershipBootstrapError, FirstSyncError, InviteAcceptError, StsVerifyError, } from "./errors.js";
13
- export type { OnboardingInput, CreateCompanyInput, JoinCompanyInput, OnboardingConfig, OnboardingResult, OnboardingProgress, OnboardingStep, StepStatus, ProgressCallback, OnboardingCheckpoint, DesktopInstallerContract, HqConfig, } from "./types.js";
10
+ export { readCheckpoint } from "./checkpoint.js";
11
+ export type { OnboardingCheckpoint } from "./types.js";
14
12
  export { runOnboardCli } from "./cli/onboard.js";
15
- export type { OnboardCliOptions, OnboardCliResult } from "./cli/onboard.js";
16
- export { formatProgress, formatSummary, formatError, validateSlug, validateEmail } from "./cli/prompts.js";
17
13
  //# sourceMappingURL=index.d.ts.map
@@ -7,10 +7,7 @@
7
7
  * invite/accept into create-company and join-company flows with
8
8
  * checkpoint/resume.
9
9
  */
10
- export { createCompanyFlow, joinCompanyFlow, resumeOnboarding, onboardingContract, } from "./orchestrator.js";
11
- export { readCheckpoint, writeCheckpoint, deleteCheckpoint, getCheckpointPath, isStepComplete, } from "./checkpoint.js";
12
- export { OnboardingError, PersonCreationError, CompanyCreationError, ProvisioningError, MembershipBootstrapError, FirstSyncError, InviteAcceptError, StsVerifyError, } from "./errors.js";
13
- // CLI
10
+ // The CLI command and identity guard are the only consumers of this barrel.
11
+ export { readCheckpoint } from "./checkpoint.js";
14
12
  export { runOnboardCli } from "./cli/onboard.js";
15
- export { formatProgress, formatSummary, formatError, validateSlug, validateEmail } from "./cli/prompts.js";
16
13
  //# sourceMappingURL=index.js.map
@@ -11,13 +11,6 @@ export type EmbedLockProcessEvents = Pick<NodeJS.Process, 'once'> & {
11
11
  removeListener?: NodeJS.Process['removeListener'];
12
12
  };
13
13
  export type EmbedLockRunResult = 'ran' | 'skipped' | 'busy' | 'unavailable';
14
- /**
15
- * Serializes QMD writes started through HQ-managed hq-cli paths. A bare `qmd
16
- * update` / `qmd embed` (or another tool that does not call this module) cannot
17
- * participate, because qmd is a third-party package and does not know about
18
- * HQ's lock protocol.
19
- */
20
- export declare const EMBED_LOCK_NAME = "qmd-reindex-bg.lock";
21
14
  export declare function defaultEmbedLockDependencies(env?: NodeJS.ProcessEnv): EmbedLockDependencies;
22
15
  export declare function lockRoot(home: string): string;
23
16
  export declare function lockPath(home: string): string;
@@ -6,7 +6,7 @@ import * as path from 'node:path';
6
6
  * participate, because qmd is a third-party package and does not know about
7
7
  * HQ's lock protocol.
8
8
  */
9
- export const EMBED_LOCK_NAME = 'qmd-reindex-bg.lock';
9
+ const EMBED_LOCK_NAME = 'qmd-reindex-bg.lock';
10
10
  export function defaultEmbedLockDependencies(env = process.env) {
11
11
  return {
12
12
  env,
@@ -22,10 +22,6 @@ export type LoadGateDependencies = {
22
22
  freemem: () => number;
23
23
  totalmem: () => number;
24
24
  };
25
- type CpuCounters = {
26
- total: number;
27
- idle: number;
28
- };
29
25
  export type EmbedLoadDecision = {
30
26
  action: 'embed';
31
27
  forced: false;
@@ -44,11 +40,8 @@ export declare class LoadGateConfigurationError extends Error {
44
40
  constructor(value: string);
45
41
  }
46
42
  export declare function defaultLoadGateDependencies(env?: NodeJS.ProcessEnv): LoadGateDependencies;
47
- export declare function parseProcStatAggregate(contents: string): CpuCounters | undefined;
48
- export declare function cpuBusyPercent(before: CpuCounters, after: CpuCounters): number | undefined;
49
43
  /** Sample aggregate CPU busy time over 200 ms, falling back only without /proc/stat. */
50
44
  export declare function measureCpuPercent(dependencies: LoadGateDependencies): number;
51
- export declare function memAvailableFraction(contents: string): number | undefined;
52
45
  /** Prefer Linux MemAvailable, because MemFree alone treats reclaimable cache as used memory. */
53
46
  export declare function measureMemoryPercent(dependencies: LoadGateDependencies): number;
54
47
  export declare function parseLoadThreshold(env: NodeJS.ProcessEnv): number | undefined;
@@ -61,5 +54,4 @@ export declare function loadGateStatePath(home: string): string;
61
54
  export declare function checkEmbedLoad(home: string, dependencies: LoadGateDependencies): EmbedLoadDecision;
62
55
  /** Reset the starvation counter only after qmd embed has actually returned successfully. */
63
56
  export declare function recordSuccessfulEmbed(home: string, dependencies: LoadGateDependencies): void;
64
- export {};
65
57
  //# sourceMappingURL=load-gate.d.ts.map