@indigoai-us/hq-cli 5.276.0 → 5.278.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 (83) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  3. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  4. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  7. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  8. package/dist/command-catalog.generated.d.ts +239 -1
  9. package/dist/command-catalog.generated.js +302 -1
  10. package/dist/commands/core-delegate.d.ts +6 -0
  11. package/dist/commands/core-delegate.js +987 -0
  12. package/dist/commands/core.js +10 -0
  13. package/dist/commands/daemon.d.ts +3 -0
  14. package/dist/commands/daemon.js +173 -1
  15. package/dist/commands/dm.d.ts +2 -0
  16. package/dist/commands/dm.js +11 -4
  17. package/dist/commands/lanes-accounts.js +44 -2
  18. package/dist/commands/lanes-message.js +69 -12
  19. package/dist/commands/lanes-resume.js +2 -0
  20. package/dist/commands/lanes-shared.d.ts +3 -0
  21. package/dist/commands/lanes-shared.js +12 -2
  22. package/dist/commands/lanes.d.ts +5 -1
  23. package/dist/commands/lanes.js +399 -47
  24. package/dist/commands/secrets.d.ts +16 -1
  25. package/dist/commands/secrets.js +77 -48
  26. package/dist/commands/sync-mode.d.ts +11 -0
  27. package/dist/commands/sync-mode.js +30 -0
  28. package/dist/fast-scaffold.d.ts +17 -0
  29. package/dist/fast-scaffold.js +91 -0
  30. package/dist/index.js +17 -1
  31. package/dist/lanes-drain-hook.js +154 -7
  32. package/dist/lib/agent-kit/run/inbox.js +3 -2
  33. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  34. package/dist/lib/checkpoint-ontology/apply.js +111 -3
  35. package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
  36. package/dist/lib/checkpoint-ontology/plan.js +23 -0
  37. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  38. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  39. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  40. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  41. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  42. package/dist/lib/core-utils/secret-patterns.js +33 -0
  43. package/dist/lib/daemon/catalog.d.ts +18 -2
  44. package/dist/lib/daemon/catalog.js +18 -2
  45. package/dist/lib/daemon/config.d.ts +6 -0
  46. package/dist/lib/daemon/config.js +15 -2
  47. package/dist/lib/daemon/requests.d.ts +4 -0
  48. package/dist/lib/daemon/requests.js +2 -0
  49. package/dist/lib/daemon/run.js +94 -8
  50. package/dist/lib/daemon/supervisor.d.ts +8 -0
  51. package/dist/lib/daemon/supervisor.js +101 -1
  52. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  53. package/dist/lib/daemon/sync-runner.js +277 -0
  54. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  55. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  56. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  57. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  58. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  59. package/dist/lib/lanes/claude-balance.js +46 -8
  60. package/dist/lib/lanes/config.d.ts +2 -1
  61. package/dist/lib/lanes/config.js +31 -0
  62. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  63. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  64. package/dist/lib/lanes/index.d.ts +1 -1
  65. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  66. package/dist/lib/lanes/keep-alive.js +3 -0
  67. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  68. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  69. package/dist/lib/lanes/reconcile.d.ts +9 -0
  70. package/dist/lib/lanes/reconcile.js +127 -2
  71. package/dist/lib/lanes/runaway-tool-guard.d.ts +38 -0
  72. package/dist/lib/lanes/runaway-tool-guard.js +284 -0
  73. package/dist/lib/lanes/spawn.js +17 -10
  74. package/dist/lib/lanes/store.js +45 -1
  75. package/dist/lib/lanes/types.d.ts +37 -1
  76. package/dist/lib/lanes/types.js +2 -2
  77. package/dist/lib/lanes/watcher.d.ts +2 -0
  78. package/dist/lib/lanes/watcher.js +13 -2
  79. package/dist/secrets-env-fast-path.d.ts +5 -0
  80. package/dist/secrets-env-fast-path.js +16 -0
  81. package/dist/secrets-env-fast-route.d.ts +18 -0
  82. package/dist/secrets-env-fast-route.js +54 -0
  83. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { Command } from "commander";
1
+ import type { Command } from "commander";
2
2
  import { writeSync } from "node:fs";
3
3
  import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
4
4
  export type { VaultApiOptions } from "../utils/vault-api.js";
@@ -88,5 +88,20 @@ export declare function loadRevealedSecrets(token: string, companyUid: string, k
88
88
  * `exit` and `write` are injectable for tests; production uses the real ones.
89
89
  */
90
90
  export declare function emitEnvExportsAndExit(payload: string, exit?: (code: number) => never, write?: typeof writeSync): never;
91
+ /**
92
+ * Shared `secrets env` action for Commander and the default-off lightweight
93
+ * entrypoint. The fast entrypoint still runs the same credential, usage-audit,
94
+ * server authorization, cache-fallback, redaction, and output code.
95
+ */
96
+ export declare function runSecretsEnvCommand(opts: {
97
+ only: string[];
98
+ script?: string;
99
+ scriptId?: string;
100
+ }, parentScope: {
101
+ company?: string;
102
+ personal?: boolean;
103
+ }, options?: {
104
+ fallbackOnAuthFailure?: boolean;
105
+ }): Promise<"handled" | "fallback">;
91
106
  export declare function registerSecretsCommand(program: Command): void;
92
107
  //# sourceMappingURL=secrets.d.ts.map
@@ -1,10 +1,8 @@
1
1
  import chalk from "chalk";
2
- import * as readline from "node:readline";
3
- import { spawn } from "node:child_process";
4
2
  import { writeSync } from "node:fs";
5
3
  import * as nodePath from "node:path";
6
4
  import { cognitoCachePrincipalUid, ensureCognitoToken } from "../utils/cognito-session.js";
7
- import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
5
+ import { isAuthError } from "../utils/auth-error.js";
8
6
  import { DEFAULT_SECRETS_CACHE_TTL_MS, hasCacheEntry, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
9
7
  import { computeSha256 } from "../utils/integrity.js";
10
8
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
@@ -12,7 +10,6 @@ import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, format
12
10
  import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from "../utils/vault-api.js";
13
11
  import { HQ_API_KEY_PREFIX, formatApiKeyCapabilityDenial, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
14
12
  import { CACHED_SECRETS_SERVER_UNAVAILABLE_MESSAGE, isSecretLoadUnavailable, isUnavailableSecretLoadHttpStatus, SecretLoadUnavailableError, } from "../utils/secret-load-availability.js";
15
- import { SandboxRunnerClient, } from "../utils/sandbox-runner-client.js";
16
13
  export { vaultApiFetch, getCompanyUid, getEntityUid };
17
14
  /**
18
15
  * Mirrors `RESOURCE_SURFACES` in hq-pro
@@ -159,14 +156,16 @@ function parseDuration(input) {
159
156
  return null;
160
157
  }
161
158
  function confirmPrompt(message) {
162
- const rl = readline.createInterface({
163
- input: process.stdin,
164
- output: process.stdout,
165
- });
166
- return new Promise((resolve) => {
167
- rl.question(`${message} [y/N] `, (answer) => {
168
- rl.close();
169
- resolve(answer.trim().toLowerCase() === "y");
159
+ return import("node:readline").then(({ createInterface }) => {
160
+ const rl = createInterface({
161
+ input: process.stdin,
162
+ output: process.stdout,
163
+ });
164
+ return new Promise((resolve) => {
165
+ rl.question(`${message} [y/N] `, (answer) => {
166
+ rl.close();
167
+ resolve(answer.trim().toLowerCase() === "y");
168
+ });
170
169
  });
171
170
  });
172
171
  }
@@ -821,6 +820,61 @@ export function emitEnvExportsAndExit(payload, exit = process.exit, write = writ
821
820
  }
822
821
  return exit(0);
823
822
  }
823
+ /**
824
+ * Shared `secrets env` action for Commander and the default-off lightweight
825
+ * entrypoint. The fast entrypoint still runs the same credential, usage-audit,
826
+ * server authorization, cache-fallback, redaction, and output code.
827
+ */
828
+ export async function runSecretsEnvCommand(opts, parentScope, options = {}) {
829
+ let secretLoadStarted = false;
830
+ try {
831
+ const redact = process.stdout.isTTY;
832
+ if (redact) {
833
+ console.error(chalk.yellow("stdout is a terminal — values redacted. Use: source <(hq secrets env --only KEY1,KEY2)"));
834
+ }
835
+ const keys = parseSecretNameList(opts.only);
836
+ const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets env", secretsCredentialOptions());
837
+ const companyUid = cred.kind === "api-key"
838
+ ? "__api_key__"
839
+ : await getEntityUid(cred.token, scopeOpts(parentScope), {
840
+ allowOfflineScopeFallback: true,
841
+ });
842
+ const usage = cred.kind === "cognito"
843
+ ? await buildSecretUsage("env", opts.script, opts.scriptId)
844
+ : undefined;
845
+ secretLoadStarted = true;
846
+ const revealed = await loadRevealedSecrets(cred.token, companyUid, keys, usage, { allowOfflineFallback: true });
847
+ let payload = "";
848
+ for (const key of keys) {
849
+ const value = revealed.get(key);
850
+ // loadRevealedSecrets throws on any unresolved key, so a miss here is
851
+ // unreachable — guard rather than print an `undefined` export line.
852
+ if (value === undefined) {
853
+ throw new Error(`Failed to fetch secret '${key}': not returned by vault`);
854
+ }
855
+ const out = redact ? "[REDACTED]" : value;
856
+ payload += `export ${key}=${shellSingleQuote(out)}\n`;
857
+ }
858
+ // Write synchronously and exit unconditionally — never depend on the
859
+ // event loop draining or the stdout write callback firing. See
860
+ // emitEnvExportsAndExit for the full rationale (orphaned-process leak).
861
+ emitEnvExportsAndExit(payload);
862
+ return "handled";
863
+ }
864
+ catch (err) {
865
+ if (options.fallbackOnAuthFailure &&
866
+ !secretLoadStarted &&
867
+ (isAuthError(err) ||
868
+ (err instanceof Error &&
869
+ (/^No valid HQ session\b/.test(err.message) ||
870
+ /^HQ_API_KEY must start with /.test(err.message))))) {
871
+ return "fallback";
872
+ }
873
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
874
+ process.exit(1);
875
+ return "handled";
876
+ }
877
+ }
824
878
  export function registerSecretsCommand(program) {
825
879
  const secrets = program
826
880
  .command("secrets")
@@ -1445,6 +1499,7 @@ export function registerSecretsCommand(program) {
1445
1499
  const token = await requireCognitoTokenForSecrets("secrets sandbox", "secrets:read");
1446
1500
  const scope = scopeOpts(mergeScopeOpts(secrets.opts(), opts));
1447
1501
  const companyUid = await getEntityUid(token, scope);
1502
+ const { SandboxRunnerClient } = await import("../utils/sandbox-runner-client.js");
1448
1503
  const client = new SandboxRunnerClient();
1449
1504
  const started = await client.startJob(token, {
1450
1505
  companyUid,
@@ -1504,9 +1559,14 @@ export function registerSecretsCommand(program) {
1504
1559
  : undefined, { allowOfflineFallback: true });
1505
1560
  // API-key auth has no signed-in person identity for the CLI telemetry
1506
1561
  // endpoint. Do not attribute that read to an unrelated cached session.
1507
- const revealed = cred.kind === "cognito"
1508
- ? await withCliFeatureUsed("secret_read", readSecrets, companyUid)
1509
- : await readSecrets();
1562
+ let revealed;
1563
+ if (cred.kind === "cognito") {
1564
+ const { withCliFeatureUsed } = await import("../utils/cli-telemetry.js");
1565
+ revealed = await withCliFeatureUsed("secret_read", readSecrets, companyUid);
1566
+ }
1567
+ else {
1568
+ revealed = await readSecrets();
1569
+ }
1510
1570
  const secretEnv = {};
1511
1571
  for (const key of keys) {
1512
1572
  const value = revealed.get(key);
@@ -1518,6 +1578,7 @@ export function registerSecretsCommand(program) {
1518
1578
  secretEnv[key] = value;
1519
1579
  }
1520
1580
  const [childCmd, ...childCmdArgs] = childArgs;
1581
+ const { spawn } = await import("node:child_process");
1521
1582
  const child = spawn(childCmd, childCmdArgs, {
1522
1583
  stdio: "inherit",
1523
1584
  env: { ...process.env, ...secretEnv },
@@ -1545,39 +1606,7 @@ export function registerSecretsCommand(program) {
1545
1606
  .option("--script <path>", "Attach local script identity for script-locked secrets")
1546
1607
  .option("--script-id <id>", "Stable script identifier approved by policy")
1547
1608
  .action(async (opts) => {
1548
- try {
1549
- const redact = process.stdout.isTTY;
1550
- if (redact) {
1551
- console.error(chalk.yellow("stdout is a terminal — values redacted. Use: source <(hq secrets env --only KEY1,KEY2)"));
1552
- }
1553
- const keys = parseSecretNameList(opts.only);
1554
- const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets env", secretsCredentialOptions());
1555
- const companyUid = cred.kind === "api-key"
1556
- ? "__api_key__"
1557
- : await getEntityUid(cred.token, scopeOpts(secrets.opts()), { allowOfflineScopeFallback: true });
1558
- const revealed = await loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
1559
- ? await buildSecretUsage("env", opts.script, opts.scriptId)
1560
- : undefined, { allowOfflineFallback: true });
1561
- let payload = "";
1562
- for (const key of keys) {
1563
- const value = revealed.get(key);
1564
- // loadRevealedSecrets throws on any unresolved key, so a miss here is
1565
- // unreachable — guard rather than print an `undefined` export line.
1566
- if (value === undefined) {
1567
- throw new Error(`Failed to fetch secret '${key}': not returned by vault`);
1568
- }
1569
- const out = redact ? "[REDACTED]" : value;
1570
- payload += `export ${key}=${shellSingleQuote(out)}\n`;
1571
- }
1572
- // Write synchronously and exit unconditionally — never depend on the
1573
- // event loop draining or the stdout write callback firing. See
1574
- // emitEnvExportsAndExit for the full rationale (orphaned-process leak).
1575
- emitEnvExportsAndExit(payload);
1576
- }
1577
- catch (err) {
1578
- console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
1579
- process.exit(1);
1580
- }
1609
+ await runSecretsEnvCommand(opts, secrets.opts());
1581
1610
  });
1582
1611
  secrets
1583
1612
  .command("generate-link <name>")
@@ -70,6 +70,15 @@ export interface SetSyncModeResult {
70
70
  export interface ShowSyncModesOptions {
71
71
  vaultClient: SyncModeVaultClient;
72
72
  }
73
+ export interface GetSyncModeOptions {
74
+ companySlug: string;
75
+ vaultClient: SyncModeVaultClient;
76
+ }
77
+ export interface GetSyncModeResult {
78
+ membershipId: string;
79
+ companySlug: string;
80
+ config: MembershipSyncConfig;
81
+ }
73
82
  export interface ShowSyncModesRow {
74
83
  companySlug: string;
75
84
  companyName?: string;
@@ -100,6 +109,8 @@ export declare function setSyncMode(options: SetSyncModeOptions): Promise<SetSyn
100
109
  * stable table output.
101
110
  */
102
111
  export declare function showSyncModes(options: ShowSyncModesOptions): Promise<ShowSyncModesRow[]>;
112
+ /** Read one company's effective sync mode without hiding API read failures. */
113
+ export declare function getSyncModeForCompany(options: GetSyncModeOptions): Promise<GetSyncModeResult>;
103
114
  /**
104
115
  * Render the `--show` table as plain text. No external dep — hq-cli doesn't
105
116
  * use cli-table3, so we hand-format columns to match the existing chalk +
@@ -163,6 +163,36 @@ export async function showSyncModes(options) {
163
163
  rows.sort((a, b) => a.companySlug.localeCompare(b.companySlug));
164
164
  return rows;
165
165
  }
166
+ /** Read one company's effective sync mode without hiding API read failures. */
167
+ export async function getSyncModeForCompany(options) {
168
+ const { companySlug, vaultClient } = options;
169
+ const memberships = await vaultClient.listMyMemberships();
170
+ const enriched = await Promise.all(memberships.map(async (membership) => {
171
+ try {
172
+ const entity = await vaultClient.entity.get(membership.companyUid);
173
+ return { ok: true, membership, slug: entity.slug };
174
+ }
175
+ catch (error) {
176
+ return { ok: false, membership, error };
177
+ }
178
+ }));
179
+ const match = enriched.find((row) => row.ok && row.slug === companySlug);
180
+ if (!match) {
181
+ const failedLookup = enriched.find((row) => !row.ok);
182
+ if (failedLookup && !failedLookup.ok)
183
+ throw failedLookup.error;
184
+ throw new Error(`No membership found for company '${companySlug}'.`);
185
+ }
186
+ for (const failedLookup of enriched) {
187
+ if (!failedLookup.ok) {
188
+ const detail = failedLookup.error instanceof Error ? failedLookup.error.message : String(failedLookup.error);
189
+ console.error(`getSyncModeForCompany: entity lookup failed for membership ${failedLookup.membership.membershipKey} while resolving '${companySlug}': ${detail}`);
190
+ }
191
+ }
192
+ const membershipId = match.membership.membershipKey;
193
+ const config = await vaultClient.getMembershipSyncConfig(membershipId);
194
+ return { membershipId, companySlug, config };
195
+ }
166
196
  // ── Table rendering (used by CLI action, separable for tests) ──────────────
167
197
  /**
168
198
  * Render the `--show` table as plain text. No external dep — hq-cli doesn't
@@ -36,6 +36,23 @@ export type ParsedFastCore = {
36
36
  /** Everything after the command name, passed to the script verbatim. */
37
37
  args: string[];
38
38
  };
39
+ export type ParsedSecretsEnvFastRequest = {
40
+ options: {
41
+ only: string[];
42
+ script?: string;
43
+ scriptId?: string;
44
+ };
45
+ scope: {
46
+ company?: string;
47
+ personal?: boolean;
48
+ };
49
+ };
50
+ /**
51
+ * Parse the small, non-ambiguous `hq secrets env` surface for the gated entry
52
+ * fast path. Anything this parser does not understand stays with Commander in
53
+ * the full CLI, which remains the source of truth for help and errors.
54
+ */
55
+ export declare function parseSecretsEnvFastRequest(argv: readonly string[]): ParsedSecretsEnvFastRequest | null;
39
56
  /**
40
57
  * Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
41
58
  * fast-path script. Returns null for anything else so the caller defers to the
@@ -21,6 +21,97 @@ export const FAST_SCAFFOLD_SCRIPTS = [
21
21
  flagKey: "hq-core-native-fast-route",
22
22
  },
23
23
  ];
24
+ /**
25
+ * Parse the small, non-ambiguous `hq secrets env` surface for the gated entry
26
+ * fast path. Anything this parser does not understand stays with Commander in
27
+ * the full CLI, which remains the source of truth for help and errors.
28
+ */
29
+ export function parseSecretsEnvFastRequest(argv) {
30
+ if (argv[2] !== "secrets")
31
+ return null;
32
+ let index = 3;
33
+ const scope = {};
34
+ let hasCompanyScope = false;
35
+ let hasPersonalScope = false;
36
+ while (index < argv.length && argv[index] !== "env") {
37
+ const arg = argv[index];
38
+ if (arg === "--company") {
39
+ const value = argv[index + 1];
40
+ if (!value || value.startsWith("-") || hasPersonalScope)
41
+ return null;
42
+ hasCompanyScope = true;
43
+ scope.company = value;
44
+ index += 2;
45
+ continue;
46
+ }
47
+ if (arg?.startsWith("--company=")) {
48
+ const value = arg.slice("--company=".length);
49
+ if (!value || hasPersonalScope)
50
+ return null;
51
+ hasCompanyScope = true;
52
+ scope.company = value;
53
+ index += 1;
54
+ continue;
55
+ }
56
+ if (arg === "--personal") {
57
+ if (hasCompanyScope)
58
+ return null;
59
+ hasPersonalScope = true;
60
+ scope.personal = true;
61
+ index += 1;
62
+ continue;
63
+ }
64
+ return null;
65
+ }
66
+ if (argv[index] !== "env")
67
+ return null;
68
+ index += 1;
69
+ const options = { only: [] };
70
+ let hasOnly = false;
71
+ while (index < argv.length) {
72
+ const arg = argv[index];
73
+ if (arg === "-h" || arg === "--help" || arg === "--")
74
+ return null;
75
+ let option;
76
+ let value;
77
+ if (arg === "--only" || arg === "--script" || arg === "--script-id") {
78
+ option = arg;
79
+ value = argv[index + 1];
80
+ if (value === undefined || value.startsWith("-"))
81
+ return null;
82
+ index += 2;
83
+ }
84
+ else if (arg?.startsWith("--only=")) {
85
+ option = "--only";
86
+ value = arg.slice("--only=".length);
87
+ index += 1;
88
+ }
89
+ else if (arg?.startsWith("--script=")) {
90
+ option = "--script";
91
+ value = arg.slice("--script=".length);
92
+ index += 1;
93
+ }
94
+ else if (arg?.startsWith("--script-id=")) {
95
+ option = "--script-id";
96
+ value = arg.slice("--script-id=".length);
97
+ index += 1;
98
+ }
99
+ else {
100
+ return null;
101
+ }
102
+ if (option === "--only") {
103
+ hasOnly = true;
104
+ options.only.push(...(value ?? "").split(",").map((key) => key.trim()).filter(Boolean));
105
+ }
106
+ else if (option === "--script") {
107
+ options.script = value;
108
+ }
109
+ else if (option === "--script-id") {
110
+ options.scriptId = value;
111
+ }
112
+ }
113
+ return hasOnly ? { options, scope } : null;
114
+ }
24
115
  /**
25
116
  * Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
26
117
  * fast-path script. Returns null for anything else so the caller defers to the
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import "./node-network-compat.js";
6
6
  import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
7
7
  // Dependency-light: a pure parser + table, no command graph. Safe to load on
8
8
  // every path (including --version) without reintroducing the heavy startup.
9
- import { isFastCoreRequest, parseFastCore } from "./fast-scaffold.js";
9
+ import { isFastCoreRequest, parseFastCore, parseSecretsEnvFastRequest, } from "./fast-scaffold.js";
10
10
  // Also dependency-light: registers a process-level rejection boundary; its only
11
11
  // heavy import (main.js) is lazy and reached only when a rejection fires.
12
12
  import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
@@ -45,6 +45,22 @@ else if (parseFastCore(process.argv)?.entry.kind === "native") {
45
45
  }
46
46
  });
47
47
  }
48
+ else if (parseSecretsEnvFastRequest(process.argv)) {
49
+ const request = parseSecretsEnvFastRequest(process.argv);
50
+ void import("./secrets-env-fast-route.js").then(async (route) => {
51
+ await route.dispatchSecretsEnvFastRequest(request, {
52
+ isEnabled: () => route.secretsEnvFastRouteEnabled(request),
53
+ runFast: async () => {
54
+ const { runSecretsEnvFastPath } = await import("./secrets-env-fast-path.js");
55
+ return runSecretsEnvFastPath(request);
56
+ },
57
+ runCurrent: async () => {
58
+ const { runCli } = await import("./main.js");
59
+ await runCli();
60
+ },
61
+ });
62
+ });
63
+ }
48
64
  else if (isFastCoreRequest(process.argv)) {
49
65
  // Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
50
66
  // dispatches straight to the bundled-script runner, so it never pays for the
@@ -1,4 +1,6 @@
1
- import { runDrainHook } from "./lib/lanes/dropbox.js";
1
+ import { randomBytes } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
2
4
  function parseHookInput(text) {
3
5
  if (text.trim() === "")
4
6
  return {};
@@ -24,6 +26,116 @@ function parseHookInput(text) {
24
26
  ...(typeof value.hook_event_name === "string" ? { hook_event_name: value.hook_event_name } : {}),
25
27
  };
26
28
  }
29
+ function laneIdIsSafe(laneId) {
30
+ return laneId !== "" && laneId.trim() === laneId &&
31
+ !laneId.includes("/") && !laneId.includes("\\") &&
32
+ !laneId.includes("\0") && laneId !== "." && laneId !== ".." &&
33
+ !laneId.includes("..");
34
+ }
35
+ function fileExists(filePath) {
36
+ try {
37
+ fs.statSync(filePath);
38
+ return true;
39
+ }
40
+ catch (err) {
41
+ if (err.code === "ENOENT")
42
+ return false;
43
+ throw err;
44
+ }
45
+ }
46
+ function listVisibleNames(dir) {
47
+ try {
48
+ return fs.readdirSync(dir).filter((name) => !name.startsWith("."));
49
+ }
50
+ catch (err) {
51
+ if (err.code === "ENOENT")
52
+ return [];
53
+ throw err;
54
+ }
55
+ }
56
+ /** Return true whenever the full drainer may have work or a condition to report. */
57
+ function dropboxMayNeedDrain(box) {
58
+ const inbox = path.join(box, "inbox.jsonl");
59
+ if (fileExists(inbox))
60
+ return true;
61
+ const draining = path.join(box, "draining");
62
+ if (listVisibleNames(draining).length > 0)
63
+ return true;
64
+ const inflightRoot = path.join(box, "inflight");
65
+ for (const name of listVisibleNames(inflightRoot)) {
66
+ const inflight = path.join(inflightRoot, name);
67
+ const stat = fs.statSync(inflight);
68
+ if (!stat.isDirectory())
69
+ return true;
70
+ if (listVisibleNames(inflight).length > 0)
71
+ return true;
72
+ }
73
+ return false;
74
+ }
75
+ function replayMayNeedWork(box, turnId, input) {
76
+ if (!/^[A-Za-z0-9-]{1,80}$/.test(turnId))
77
+ return false;
78
+ if (input.transcript_path)
79
+ return true;
80
+ let text;
81
+ const statePath = path.join(box, "message-compaction-replay.json");
82
+ try {
83
+ text = fs.readFileSync(statePath, "utf8");
84
+ }
85
+ catch {
86
+ // Keep the existing named replay error behavior for missing or unreadable state.
87
+ return true;
88
+ }
89
+ try {
90
+ const parsed = JSON.parse(text);
91
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
92
+ return true;
93
+ const state = parsed;
94
+ if (state.schema_version !== 1 ||
95
+ state.turn_id !== turnId ||
96
+ (state.last_compaction_marker !== null &&
97
+ (!Number.isSafeInteger(state.last_compaction_marker) || Number(state.last_compaction_marker) < 0)) ||
98
+ (state.transcript_offset !== undefined &&
99
+ (!Number.isSafeInteger(state.transcript_offset) || Number(state.transcript_offset) < 0)) ||
100
+ (state.transcript_path !== undefined && typeof state.transcript_path !== "string") ||
101
+ state.flag_enabled !== undefined) {
102
+ return true;
103
+ }
104
+ return false;
105
+ }
106
+ catch {
107
+ return true;
108
+ }
109
+ }
110
+ function recordToolActivity(hqRoot, laneId) {
111
+ try {
112
+ const activityPath = path.join(hqRoot, "workspace", "lanes-runs", laneId, "logs", "tool-activity.json");
113
+ fs.mkdirSync(path.dirname(activityPath), { recursive: true });
114
+ const record = {
115
+ at: new Date().toISOString(),
116
+ hook_pid: process.pid,
117
+ hook_event: "PreToolUse",
118
+ };
119
+ const tempPath = `${activityPath}.${process.pid}.${process.hrtime.bigint()}.${randomBytes(4).toString("hex")}.tmp`;
120
+ fs.writeFileSync(tempPath, `${JSON.stringify(record)}\n`, "utf8");
121
+ fs.renameSync(tempPath, activityPath);
122
+ return "";
123
+ }
124
+ catch (err) {
125
+ return `hq-lanes-drain: failed to record tool activity: ${err instanceof Error ? err.message : String(err)}\n`;
126
+ }
127
+ }
128
+ function finish(result, inputError) {
129
+ if (result.stdout)
130
+ process.stdout.write(result.stdout);
131
+ if (inputError || result.stderr)
132
+ process.stderr.write(`${inputError}${result.stderr}`);
133
+ process.exitCode = result.exitCode;
134
+ }
135
+ async function runFullPath(input, inputError) {
136
+ const { runDrainHook } = await import("./lib/lanes/dropbox.js");
137
+ finish(await runDrainHook(process.env, input), inputError);
138
+ }
27
139
  async function main() {
28
140
  const chunks = [];
29
141
  for await (const chunk of process.stdin)
@@ -37,12 +149,47 @@ async function main() {
37
149
  catch (err) {
38
150
  inputError = `hq-lanes-drain: ${err instanceof Error ? err.message : String(err)}\n`;
39
151
  }
40
- const result = await runDrainHook(process.env, input);
41
- if (result.stdout)
42
- process.stdout.write(result.stdout);
43
- if (inputError || result.stderr)
44
- process.stderr.write(`${inputError}${result.stderr}`);
45
- process.exitCode = result.exitCode;
152
+ const laneId = process.env.HQ_LANE_ID ?? "";
153
+ if (laneId === "") {
154
+ finish({ exitCode: 0, stdout: "", stderr: "" }, inputError);
155
+ return;
156
+ }
157
+ const hqRoot = process.env.HQ_ROOT ?? "";
158
+ if (hqRoot === "" || !laneIdIsSafe(laneId)) {
159
+ await runFullPath(input, inputError);
160
+ return;
161
+ }
162
+ const box = path.join(hqRoot, "workspace", "lanes-runs", laneId, "dropbox");
163
+ let noWork = false;
164
+ try {
165
+ noWork = !dropboxMayNeedDrain(box) &&
166
+ !replayMayNeedWork(box, process.env.HQ_LANE_TURN_ID ?? "", input);
167
+ }
168
+ catch {
169
+ // Filesystem uncertainty must use the full path so it retains its named errors.
170
+ }
171
+ if (!noWork) {
172
+ await runFullPath(input, inputError);
173
+ return;
174
+ }
175
+ let stderr = recordToolActivity(hqRoot, laneId);
176
+ const directories = [
177
+ box,
178
+ path.join(box, "draining"),
179
+ path.join(box, "delivered"),
180
+ path.join(box, "inflight"),
181
+ ];
182
+ try {
183
+ for (const directory of directories)
184
+ fs.mkdirSync(directory, { recursive: true });
185
+ }
186
+ catch (err) {
187
+ const message = `Failed to open drop box ${box}: ${err instanceof Error ? err.message : String(err)}`;
188
+ stderr += `hq-lanes-drain: ${message}\n`;
189
+ finish({ exitCode: 1, stdout: "", stderr }, inputError);
190
+ return;
191
+ }
192
+ finish({ exitCode: 0, stdout: "", stderr }, inputError);
46
193
  }
47
194
  main().catch((err) => {
48
195
  process.stderr.write(`hq-lanes-drain: hook_runner_failed: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -138,8 +138,9 @@ export async function pollInboxOnce(deps) {
138
138
  if (res.status !== 200) {
139
139
  writeComponentStatus(deps.paths, "inbox", "error");
140
140
  const url = res.url ?? agentInboxUrl(deps.apiBaseUrl, deps.agentUid);
141
- const bodyText = truncateBody(res.bodyText ?? (res.body === undefined ? "" : JSON.stringify(res.body)));
142
- deps.log("warn", `inbox poll GET ${url} → ${res.status}${bodyText ? ` body=${bodyText}` : ""}`);
141
+ // Non-OK bodies are not constrained to an error envelope and could carry
142
+ // inbox items. Keep the status and route, but do not copy the body to logs.
143
+ deps.log("warn", `inbox poll GET ${url} → ${res.status}`);
143
144
  return { ok: false, mirrored: 0 };
144
145
  }
145
146
  writeComponentStatus(deps.paths, "inbox", "ok");
@@ -53,6 +53,12 @@ export interface ApplyReport {
53
53
  canonical_name: string;
54
54
  path: string;
55
55
  }>;
56
+ /** Present only when `checkpoint.ontology.dominant-sibling-link` was on: knowledge links moved from a weak sibling to the dominant one. */
57
+ dominant_sibling_redirects?: Array<{
58
+ input: string;
59
+ from: string;
60
+ to: string;
61
+ }>;
56
62
  rejected: Array<{
57
63
  input: string;
58
64
  type: string;