@indigoai-us/hq-cli 5.276.0 → 5.277.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.
- package/CHANGELOG.md +76 -0
- package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
- package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
- package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
- package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
- package/dist/command-catalog.generated.d.ts +239 -1
- package/dist/command-catalog.generated.js +302 -1
- package/dist/commands/core-delegate.d.ts +6 -0
- package/dist/commands/core-delegate.js +987 -0
- package/dist/commands/core.js +10 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +173 -1
- package/dist/commands/dm.d.ts +2 -0
- package/dist/commands/dm.js +11 -4
- package/dist/commands/lanes-accounts.js +44 -2
- package/dist/commands/lanes-message.js +69 -12
- package/dist/commands/lanes-resume.js +2 -0
- package/dist/commands/lanes-shared.d.ts +3 -0
- package/dist/commands/lanes-shared.js +12 -2
- package/dist/commands/lanes.d.ts +5 -1
- package/dist/commands/lanes.js +399 -47
- package/dist/commands/secrets.d.ts +16 -1
- package/dist/commands/secrets.js +77 -48
- package/dist/commands/sync-mode.d.ts +11 -0
- package/dist/commands/sync-mode.js +30 -0
- package/dist/fast-scaffold.d.ts +17 -0
- package/dist/fast-scaffold.js +91 -0
- package/dist/index.js +17 -1
- package/dist/lanes-drain-hook.js +154 -7
- package/dist/lib/agent-kit/run/inbox.js +3 -2
- package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
- package/dist/lib/checkpoint-ontology/apply.js +60 -3
- package/dist/lib/checkpoint-ontology/plan.d.ts +13 -0
- package/dist/lib/checkpoint-ontology/plan.js +13 -0
- package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
- package/dist/lib/checkpoint-ontology/tree.js +34 -0
- package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
- package/dist/lib/core-utils/jq-json-serializer.js +10 -0
- package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
- package/dist/lib/core-utils/secret-patterns.js +33 -0
- package/dist/lib/daemon/catalog.d.ts +18 -2
- package/dist/lib/daemon/catalog.js +18 -2
- package/dist/lib/daemon/config.d.ts +6 -0
- package/dist/lib/daemon/config.js +15 -2
- package/dist/lib/daemon/requests.d.ts +4 -0
- package/dist/lib/daemon/requests.js +2 -0
- package/dist/lib/daemon/run.js +94 -8
- package/dist/lib/daemon/supervisor.d.ts +8 -0
- package/dist/lib/daemon/supervisor.js +101 -1
- package/dist/lib/daemon/sync-runner.d.ts +58 -0
- package/dist/lib/daemon/sync-runner.js +277 -0
- package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +46 -8
- package/dist/lib/lanes/config.js +15 -0
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +3 -0
- package/dist/lib/lanes/keep-alive.js +3 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
- package/dist/lib/lanes/outpost-dispatch.js +11 -2
- package/dist/lib/lanes/spawn.js +17 -10
- package/dist/lib/lanes/store.js +45 -1
- package/dist/lib/lanes/types.d.ts +30 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -2
- package/dist/secrets-env-fast-path.d.ts +5 -0
- package/dist/secrets-env-fast-path.js +16 -0
- package/dist/secrets-env-fast-route.d.ts +18 -0
- package/dist/secrets-env-fast-route.js +54 -0
- 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
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
rl.
|
|
169
|
-
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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
|
-
|
|
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
|
package/dist/fast-scaffold.d.ts
CHANGED
|
@@ -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
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -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
|
package/dist/lanes-drain-hook.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
process.
|
|
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
|
-
|
|
142
|
-
|
|
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;
|