@indigoai-us/hq-cli 5.345.134 → 5.345.135
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 +7 -0
- package/dist/command-catalog.generated.d.ts +45 -0
- package/dist/command-catalog.generated.js +56 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +3 -1
- package/dist/commands/core-bucket-c-handoff-knowledge-native.js +8 -4
- package/dist/commands/core-handoff.d.ts +2 -0
- package/dist/commands/core-handoff.js +3 -1
- package/dist/commands/core-pipeline-workflow-runner-native.js +7 -5
- package/dist/commands/fingerprint.d.ts +10 -0
- package/dist/commands/fingerprint.js +191 -0
- package/dist/commands/lanes.js +9 -1
- package/dist/lib/fingerprint/fixtures.d.ts +19 -0
- package/dist/lib/fingerprint/fixtures.js +61 -0
- package/dist/lib/fingerprint/format.d.ts +50 -0
- package/dist/lib/fingerprint/format.js +105 -0
- package/dist/lib/fingerprint/offline.d.ts +16 -0
- package/dist/lib/fingerprint/offline.js +44 -0
- package/dist/lib/fingerprint/registry-client.d.ts +74 -0
- package/dist/lib/fingerprint/registry-client.js +141 -0
- package/dist/lib/fingerprint/resolve-local.d.ts +14 -0
- package/dist/lib/fingerprint/resolve-local.js +67 -0
- package/dist/lib/fingerprint/self-core.d.ts +139 -0
- package/dist/lib/fingerprint/self-core.js +371 -0
- package/dist/lib/fingerprint/self.d.ts +44 -0
- package/dist/lib/fingerprint/self.js +191 -0
- package/dist/lib/lanes/spawn.d.ts +19 -1
- package/dist/lib/lanes/spawn.js +50 -6
- package/dist/lib/lanes/story-ledger.d.ts +1 -0
- package/dist/lib/lanes/story-ledger.js +3 -1
- package/dist/lib/lanes/types.d.ts +12 -0
- package/dist/lib/lanes/types.js +12 -1
- package/dist/utils/cli-network-lifecycle.d.ts +2 -0
- package/dist/utils/cli-network-lifecycle.js +5 -1
- package/dist/utils/cognito-session.d.ts +2 -21
- package/dist/utils/cognito-session.js +3 -84
- package/dist/utils/person-token-cache.d.ts +32 -0
- package/dist/utils/person-token-cache.js +95 -0
- package/dist/utils/vault-api.d.ts +21 -1
- package/dist/utils/vault-api.js +54 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.135] — 2026-10-15
|
|
6
|
+
- `hq fingerprint self` prints the current session's HQ fingerprint, and `hq fingerprint <value>` resolves a fingerprint to its local session record. The commit-msg hook calls `hq fingerprint self --json --offline`, which stays offline and always exits 0.
|
|
7
|
+
- Worker lanes get their own `HQ_SESSION_ID`, `HQ_LANE_ID`, `HQ_PARENT_SESSION_ID` and `HQ_COMPANY` with every inherited session id removed; lane records carry a `fingerprint` field shown by `lanes status --json` and `lanes show`; handoff commits add an `HQ-Fingerprint` trailer when a value exists. The handoff lookup is read-only and the offline builder never loads network modules (US-005).
|
|
8
|
+
- `hq fingerprint self` records the session's fingerprint with the HQ registry once per session after printing it, and writes `registered_at` to the session's fingerprint.yaml. The call has a 3 s timeout and never changes the output or the exit code. Failures go to one line in `~/.hq/fingerprint.log` with no token values. `--no-register`, `--offline` and `HQ_FINGERPRINT_OFFLINE=1` skip it. Outpost hosts, which the registry does not accept, log a skip once and are not retried (US-007).
|
|
9
|
+
- `hq fingerprint <value>` asks the HQ registry when no local session record matches, and prints source `registry` with the identity name, machine label and host kind. `--json` includes `source`, `label` and `firstSeenAt`. A value found nowhere prints `no fingerprint record` and exits 1. A local match makes no network call (US-007).
|
|
10
|
+
- `hq fingerprint self` run outside an HQ folder prints a fingerprint with machine `-`, creates no files, skips registration and reports `reason: "no HQ root"` in `--json`. A registry that answers 404 records `registration_skipped: unavailable`, logs one line and is not called again for that session (US-005).
|
|
11
|
+
|
|
5
12
|
## [5.345.134] — 2026-10-15
|
|
6
13
|
- `hq access --as <member>` now honors the most specific ACL row covering a path, so an empty folder lock reports no access instead of the write access of a broader grant. `hq access` also reports "no access" rather than "never created" when a caller cannot list a path that an ACL lock denies them.
|
|
7
14
|
|
|
@@ -1352,6 +1352,51 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1352
1352
|
readonly hidden: false;
|
|
1353
1353
|
}];
|
|
1354
1354
|
readonly subcommands: readonly [];
|
|
1355
|
+
}, {
|
|
1356
|
+
readonly name: "fingerprint";
|
|
1357
|
+
readonly description: "Print this session's HQ fingerprint, or resolve a fingerprint to its session";
|
|
1358
|
+
readonly aliases: readonly [];
|
|
1359
|
+
readonly hidden: false;
|
|
1360
|
+
readonly helpOption: true;
|
|
1361
|
+
readonly helpCommand: false;
|
|
1362
|
+
readonly usage: "[options] [command] [value]";
|
|
1363
|
+
readonly helpText: readonly [];
|
|
1364
|
+
readonly arguments: readonly [{
|
|
1365
|
+
readonly name: "value";
|
|
1366
|
+
readonly required: false;
|
|
1367
|
+
readonly variadic: false;
|
|
1368
|
+
readonly description: "fingerprint value to resolve (hqfp1:...)";
|
|
1369
|
+
}];
|
|
1370
|
+
readonly options: readonly [{
|
|
1371
|
+
readonly flags: "--json";
|
|
1372
|
+
readonly description: "output JSON";
|
|
1373
|
+
readonly hidden: false;
|
|
1374
|
+
}];
|
|
1375
|
+
readonly subcommands: readonly [{
|
|
1376
|
+
readonly name: "self";
|
|
1377
|
+
readonly description: "Print the fingerprint for the current HQ session";
|
|
1378
|
+
readonly aliases: readonly [];
|
|
1379
|
+
readonly hidden: false;
|
|
1380
|
+
readonly helpOption: true;
|
|
1381
|
+
readonly helpCommand: false;
|
|
1382
|
+
readonly usage: "[options]";
|
|
1383
|
+
readonly helpText: readonly [];
|
|
1384
|
+
readonly arguments: readonly [];
|
|
1385
|
+
readonly options: readonly [{
|
|
1386
|
+
readonly flags: "--json";
|
|
1387
|
+
readonly description: "output JSON";
|
|
1388
|
+
readonly hidden: false;
|
|
1389
|
+
}, {
|
|
1390
|
+
readonly flags: "--offline";
|
|
1391
|
+
readonly description: "never make a network call (identity lookup or registration)";
|
|
1392
|
+
readonly hidden: false;
|
|
1393
|
+
}, {
|
|
1394
|
+
readonly flags: "--no-register";
|
|
1395
|
+
readonly description: "do not record this fingerprint with the HQ registry";
|
|
1396
|
+
readonly hidden: false;
|
|
1397
|
+
}];
|
|
1398
|
+
readonly subcommands: readonly [];
|
|
1399
|
+
}];
|
|
1355
1400
|
}, {
|
|
1356
1401
|
readonly name: "capture";
|
|
1357
1402
|
readonly description: "Record your screen with HQ Capture: start, record, stop, status and wait";
|
|
@@ -1621,6 +1621,62 @@ export const COMMAND_CATALOG = [
|
|
|
1621
1621
|
],
|
|
1622
1622
|
"subcommands": []
|
|
1623
1623
|
},
|
|
1624
|
+
{
|
|
1625
|
+
"name": "fingerprint",
|
|
1626
|
+
"description": "Print this session's HQ fingerprint, or resolve a fingerprint to its session",
|
|
1627
|
+
"aliases": [],
|
|
1628
|
+
"hidden": false,
|
|
1629
|
+
"helpOption": true,
|
|
1630
|
+
"helpCommand": false,
|
|
1631
|
+
"usage": "[options] [command] [value]",
|
|
1632
|
+
"helpText": [],
|
|
1633
|
+
"arguments": [
|
|
1634
|
+
{
|
|
1635
|
+
"name": "value",
|
|
1636
|
+
"required": false,
|
|
1637
|
+
"variadic": false,
|
|
1638
|
+
"description": "fingerprint value to resolve (hqfp1:...)"
|
|
1639
|
+
}
|
|
1640
|
+
],
|
|
1641
|
+
"options": [
|
|
1642
|
+
{
|
|
1643
|
+
"flags": "--json",
|
|
1644
|
+
"description": "output JSON",
|
|
1645
|
+
"hidden": false
|
|
1646
|
+
}
|
|
1647
|
+
],
|
|
1648
|
+
"subcommands": [
|
|
1649
|
+
{
|
|
1650
|
+
"name": "self",
|
|
1651
|
+
"description": "Print the fingerprint for the current HQ session",
|
|
1652
|
+
"aliases": [],
|
|
1653
|
+
"hidden": false,
|
|
1654
|
+
"helpOption": true,
|
|
1655
|
+
"helpCommand": false,
|
|
1656
|
+
"usage": "[options]",
|
|
1657
|
+
"helpText": [],
|
|
1658
|
+
"arguments": [],
|
|
1659
|
+
"options": [
|
|
1660
|
+
{
|
|
1661
|
+
"flags": "--json",
|
|
1662
|
+
"description": "output JSON",
|
|
1663
|
+
"hidden": false
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
"flags": "--offline",
|
|
1667
|
+
"description": "never make a network call (identity lookup or registration)",
|
|
1668
|
+
"hidden": false
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
"flags": "--no-register",
|
|
1672
|
+
"description": "do not record this fingerprint with the HQ registry",
|
|
1673
|
+
"hidden": false
|
|
1674
|
+
}
|
|
1675
|
+
],
|
|
1676
|
+
"subcommands": []
|
|
1677
|
+
}
|
|
1678
|
+
]
|
|
1679
|
+
},
|
|
1624
1680
|
{
|
|
1625
1681
|
"name": "capture",
|
|
1626
1682
|
"description": "Record your screen with HQ Capture: start, record, stop, status and wait",
|
|
@@ -213,6 +213,12 @@ export declare const REGISTRATION_PLAN: readonly [{
|
|
|
213
213
|
readonly parent: "program";
|
|
214
214
|
readonly module: "./commands/version.js";
|
|
215
215
|
readonly exportName: "registerVersionCommand";
|
|
216
|
+
}, {
|
|
217
|
+
readonly type: "registrar";
|
|
218
|
+
readonly root: "fingerprint";
|
|
219
|
+
readonly parent: "program";
|
|
220
|
+
readonly module: "./commands/fingerprint.js";
|
|
221
|
+
readonly exportName: "registerFingerprintCommand";
|
|
216
222
|
}, {
|
|
217
223
|
readonly type: "registrar";
|
|
218
224
|
readonly root: "capture";
|
|
@@ -38,6 +38,7 @@ export const REGISTRATION_PLAN = [
|
|
|
38
38
|
{ type: "registrar", root: "resolve-company", parent: "program", module: "./commands/link.js", exportName: "registerResolveCompanyCommand" },
|
|
39
39
|
{ type: "registrar", root: "provider", parent: "program", module: "./commands/provider.js", exportName: "registerProviderCommand" },
|
|
40
40
|
{ type: "registrar", root: "version", parent: "program", module: "./commands/version.js", exportName: "registerVersionCommand" },
|
|
41
|
+
{ type: "registrar", root: "fingerprint", parent: "program", module: "./commands/fingerprint.js", exportName: "registerFingerprintCommand" },
|
|
41
42
|
{ type: "registrar", root: "capture", parent: "program", module: "./commands/capture.js", exportName: "registerCaptureCommand" },
|
|
42
43
|
{ type: "registrar", root: "auth", parent: "program", module: "./commands/auth.js", exportName: "registerAuthCommands" },
|
|
43
44
|
{ type: "registrar", root: "secrets", parent: "program", module: "./commands/secrets.js", exportName: "registerSecretsCommand" },
|
|
@@ -10,6 +10,8 @@ type CommandResult = {
|
|
|
10
10
|
stderr: string | Buffer;
|
|
11
11
|
};
|
|
12
12
|
type GitRunner = (args: string[], cwd: string) => CommandResult;
|
|
13
|
+
/** Builds the HQ-Fingerprint commit trailer value; null adds no trailer. */
|
|
14
|
+
export type FingerprintBuilder = (root: string) => string | null;
|
|
13
15
|
declare function samePath(left: string, right: string): boolean;
|
|
14
16
|
declare function windowsNativePath(value: string): string;
|
|
15
17
|
declare function safePath(value: unknown): value is string;
|
|
@@ -30,7 +32,7 @@ declare function parseNulPaths(status: string): {
|
|
|
30
32
|
paths: string[];
|
|
31
33
|
originals: string[];
|
|
32
34
|
};
|
|
33
|
-
export declare function runHandoffKnowledgeCommit(args: readonly string[], cwd?: string, git?: GitRunner): KnowledgeCommitResult;
|
|
35
|
+
export declare function runHandoffKnowledgeCommit(args: readonly string[], cwd?: string, git?: GitRunner, buildFingerprint?: FingerprintBuilder): KnowledgeCommitResult;
|
|
34
36
|
export declare const __handoffKnowledgeTest: {
|
|
35
37
|
safePath: typeof safePath;
|
|
36
38
|
validChangeset: typeof validChangeset;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { fingerprintTrailerArgs, offlineFingerprint } from "../lib/fingerprint/offline.js";
|
|
4
5
|
export const HANDOFF_KNOWLEDGE_COMMIT_ASSET = "core/scripts/handoff-knowledge-commit.sh";
|
|
5
6
|
const USAGE = "Usage: handoff-knowledge-commit.sh --files-touched-json-file <path> [--root <hq-root>]";
|
|
6
7
|
const fail = (message, code = 1) => ({ code, stdout: "", stderr: message + "\n" });
|
|
7
8
|
const gitDefault = (args, cwd) => spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
9
|
+
const fingerprintDefault = (root) => offlineFingerprint(root, process.env, "hq handoff", { readOnly: true });
|
|
8
10
|
const text = (value) => typeof value === "string" ? value : value.toString("utf8");
|
|
9
11
|
function canonicalPath(value) {
|
|
10
12
|
const slashPath = value.replaceAll("\\", "/");
|
|
@@ -157,7 +159,7 @@ function parseNulPaths(status) {
|
|
|
157
159
|
}
|
|
158
160
|
return { paths, originals };
|
|
159
161
|
}
|
|
160
|
-
function commitKnowledge(git, root, changes, handled, output) {
|
|
162
|
+
function commitKnowledge(git, root, changes, handled, output, trailer) {
|
|
161
163
|
for (const entry of entriesUnder(root)) {
|
|
162
164
|
let stat;
|
|
163
165
|
try {
|
|
@@ -209,7 +211,7 @@ function commitKnowledge(git, root, changes, handled, output) {
|
|
|
209
211
|
output.stderr.push(text(add.stderr));
|
|
210
212
|
return { code: add.status ?? 1, stdout: output.stdout.join(""), stderr: output.stderr.join("") };
|
|
211
213
|
}
|
|
212
|
-
const commit = git(["commit", "--only", "-m", "checkpoint: save session knowledge changes", "--", ...repoPaths], repo);
|
|
214
|
+
const commit = git(["commit", "--only", "-m", "checkpoint: save session knowledge changes", ...trailer(), "--", ...repoPaths], repo);
|
|
213
215
|
output.stdout.push(text(commit.stdout));
|
|
214
216
|
output.stderr.push(text(commit.stderr));
|
|
215
217
|
if (commit.status !== 0)
|
|
@@ -217,7 +219,7 @@ function commitKnowledge(git, root, changes, handled, output) {
|
|
|
217
219
|
}
|
|
218
220
|
return undefined;
|
|
219
221
|
}
|
|
220
|
-
export function runHandoffKnowledgeCommit(args, cwd = process.cwd(), git = gitDefault) {
|
|
222
|
+
export function runHandoffKnowledgeCommit(args, cwd = process.cwd(), git = gitDefault, buildFingerprint = fingerprintDefault) {
|
|
221
223
|
let changesFile = "";
|
|
222
224
|
let root = cwd;
|
|
223
225
|
for (let index = 0; index < args.length;) {
|
|
@@ -270,7 +272,9 @@ export function runHandoffKnowledgeCommit(args, cwd = process.cwd(), git = gitDe
|
|
|
270
272
|
});
|
|
271
273
|
const output = { stdout: [], stderr: [] };
|
|
272
274
|
const handled = new Set();
|
|
273
|
-
|
|
275
|
+
let trailerArgs;
|
|
276
|
+
const trailer = () => (trailerArgs ??= fingerprintTrailerArgs(buildFingerprint(root)));
|
|
277
|
+
const commitFailure = commitKnowledge(git, root, changes, handled, output, trailer);
|
|
274
278
|
if (commitFailure)
|
|
275
279
|
return commitFailure;
|
|
276
280
|
for (const touched of changes) {
|
|
@@ -13,6 +13,8 @@ type HandoffContext = {
|
|
|
13
13
|
now?: () => Date;
|
|
14
14
|
/** Test-only override for the finalizer's historical fixed /tmp paths. */
|
|
15
15
|
tmpDir?: string;
|
|
16
|
+
/** Builds the HQ-Fingerprint commit trailer value. Defaults to the offline builder. */
|
|
17
|
+
buildFingerprint?: (root: string) => string | null;
|
|
16
18
|
};
|
|
17
19
|
export declare function runHandoffFinalize(args: string[], context: HandoffContext): RunResult;
|
|
18
20
|
export declare function runHandoffPost(args: string[], context: HandoffContext): RunResult | Promise<RunResult>;
|
|
@@ -14,6 +14,7 @@ import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
|
14
14
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
15
15
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
16
16
|
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
17
|
+
import { fingerprintTrailerArgs, offlineFingerprint } from "../lib/fingerprint/offline.js";
|
|
17
18
|
import { compactJson, isRecord, jqTruthy, prettyJson, runHandoffOpenSteps, shellDate, } from "./core-handoff-open-steps.js";
|
|
18
19
|
export { runHandoffOpenSteps };
|
|
19
20
|
const ASSETS = {
|
|
@@ -585,7 +586,8 @@ export function runHandoffFinalize(args, context) {
|
|
|
585
586
|
if (probe.hasGit && staged > 0) {
|
|
586
587
|
const pending = git(["diff", "--cached", "--name-only"], root).stdout.split(/\r?\n/).filter(Boolean);
|
|
587
588
|
if (pending.length > 0) {
|
|
588
|
-
const
|
|
589
|
+
const fingerprint = (context.buildFingerprint ?? ((hqRoot) => offlineFingerprint(hqRoot, process.env, "hq handoff", { readOnly: true })))(root);
|
|
590
|
+
const committed = git(["commit", "-m", `checkpoint: handoff ${threadId}`, ...fingerprintTrailerArgs(fingerprint)], root);
|
|
589
591
|
if (committed.code === 0) {
|
|
590
592
|
hqCommitted = true;
|
|
591
593
|
commitStatus = "committed";
|
|
@@ -340,18 +340,20 @@ function readSessionCompanySlugs(sid) {
|
|
|
340
340
|
return [...new Set(slugs)];
|
|
341
341
|
}
|
|
342
342
|
function parentSessionId() {
|
|
343
|
-
|
|
344
|
-
|| process.env.HQ_SESSION_ID
|
|
343
|
+
const own = process.env.HQ_SESSION_ID
|
|
345
344
|
|| process.env.CLAUDE_CODE_SESSION_ID
|
|
346
345
|
|| process.env.CLAUDE_SESSION_ID
|
|
347
346
|
|| process.env.CODEX_SESSION_ID
|
|
348
|
-
|| process.env.CODEX_THREAD_ID
|
|
349
|
-
|
|
347
|
+
|| process.env.CODEX_THREAD_ID;
|
|
348
|
+
// Inside a lane worker HQ_PARENT_SESSION_ID names the session that started
|
|
349
|
+
// the worker; the worker's own session is the parent of what it spawns.
|
|
350
|
+
const laneWorker = Boolean(process.env.HQ_LANE_ID?.trim()) && Boolean(own?.trim());
|
|
351
|
+
return String((laneWorker ? own : process.env.HQ_PARENT_SESSION_ID || own) || '').replace(/\s+/g, '');
|
|
350
352
|
}
|
|
351
353
|
function engineChildEnv() {
|
|
352
354
|
const extra = {};
|
|
353
355
|
const parent = parentSessionId();
|
|
354
|
-
if (
|
|
356
|
+
if (parent && parent !== process.env.HQ_PARENT_SESSION_ID)
|
|
355
357
|
extra.HQ_PARENT_SESSION_ID = parent;
|
|
356
358
|
if (!process.env.HQ_SPAWN_COMPANY) {
|
|
357
359
|
const capability = readSessionCompanyCapability(parent);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* The HQ root for `self`: HQ_ROOT, else the nearest ancestor of `cwd` (itself
|
|
4
|
+
* included) that holds `.claude` or `workers` and core/core.yaml. Null when
|
|
5
|
+
* there is none. Unlike findHqRoot it never falls back to `cwd`, so a commit
|
|
6
|
+
* in an unrelated repo does not create `.hq/` or session files there.
|
|
7
|
+
*/
|
|
8
|
+
export declare function findSelfHqRoot(env?: NodeJS.ProcessEnv, cwd?: string, existsSync?: (candidate: string) => boolean): string | null;
|
|
9
|
+
export declare function registerFingerprintCommand(program: Command): void;
|
|
10
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq fingerprint - print or resolve HQ session fingerprints.
|
|
3
|
+
*
|
|
4
|
+
* hq fingerprint self [--json] [--offline] [--no-register]
|
|
5
|
+
* hq fingerprint <value> [--json]
|
|
6
|
+
*
|
|
7
|
+
* `self` records the fingerprint with the hq-pro registry once per session
|
|
8
|
+
* after printing it (skipped with --no-register, --offline, and
|
|
9
|
+
* HQ_FINGERPRINT_OFFLINE=1). `<value>` checks local session records first and
|
|
10
|
+
* asks the registry only when none matches.
|
|
11
|
+
*
|
|
12
|
+
* Exit codes for `hq fingerprint <value>`: 0 found, 1 no fingerprint record,
|
|
13
|
+
* 2 not an HQ fingerprint. `self` exits 0 whether or not a value is built or
|
|
14
|
+
* registered.
|
|
15
|
+
*
|
|
16
|
+
* Outside an HQ tree (no HQ_ROOT and no ancestor with `.claude` or `workers`
|
|
17
|
+
* plus core/core.yaml) `self` prints a fingerprint with machine `-`, writes
|
|
18
|
+
* nothing, skips registration, and reports `reason: "no HQ root"` in --json.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { isFingerprint } from "../lib/fingerprint/format.js";
|
|
23
|
+
import { resolveLocalFingerprint } from "../lib/fingerprint/resolve-local.js";
|
|
24
|
+
import { buildSelfFingerprint, registerSelfFingerprint } from "../lib/fingerprint/self.js";
|
|
25
|
+
import { findHqRoot } from "../utils/manifest.js";
|
|
26
|
+
function hqRootFromEnv() {
|
|
27
|
+
return process.env.HQ_ROOT || findHqRoot();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The HQ root for `self`: HQ_ROOT, else the nearest ancestor of `cwd` (itself
|
|
31
|
+
* included) that holds `.claude` or `workers` and core/core.yaml. Null when
|
|
32
|
+
* there is none. Unlike findHqRoot it never falls back to `cwd`, so a commit
|
|
33
|
+
* in an unrelated repo does not create `.hq/` or session files there.
|
|
34
|
+
*/
|
|
35
|
+
export function findSelfHqRoot(env = process.env, cwd = process.cwd(), existsSync = fs.existsSync) {
|
|
36
|
+
const fromEnv = env.HQ_ROOT?.trim();
|
|
37
|
+
if (fromEnv)
|
|
38
|
+
return fromEnv;
|
|
39
|
+
let dir = path.resolve(cwd);
|
|
40
|
+
while (true) {
|
|
41
|
+
const marked = existsSync(path.join(dir, ".claude")) || existsSync(path.join(dir, "workers"));
|
|
42
|
+
if (marked && existsSync(path.join(dir, "core", "core.yaml")))
|
|
43
|
+
return dir;
|
|
44
|
+
const parent = path.dirname(dir);
|
|
45
|
+
if (parent === dir)
|
|
46
|
+
return null;
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function loadIdToken() {
|
|
51
|
+
const { loadCachedTokens } = await import("../utils/cognito-session.js");
|
|
52
|
+
return loadCachedTokens()?.idToken ?? null;
|
|
53
|
+
}
|
|
54
|
+
/** Asks the registry about a fingerprint with no local record. Never throws. */
|
|
55
|
+
async function lookupRegistry(value) {
|
|
56
|
+
// Loaded here so a local hit never loads the registry client.
|
|
57
|
+
const registry = await import("../lib/fingerprint/registry-client.js");
|
|
58
|
+
let token;
|
|
59
|
+
try {
|
|
60
|
+
token = await loadIdToken();
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
registry.appendFingerprintLog("lookup", `failed: could not read the cached token: ${err.name}`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (!token)
|
|
67
|
+
return null;
|
|
68
|
+
const result = await registry.lookupFingerprint(value, token);
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
registry.appendFingerprintLog("lookup", `failed: ${registry.describeFailure(result)} fingerprint=${value}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return result.found ? result.record : null;
|
|
74
|
+
}
|
|
75
|
+
export function registerFingerprintCommand(program) {
|
|
76
|
+
const fingerprint = program
|
|
77
|
+
.command("fingerprint")
|
|
78
|
+
.description("Print this session's HQ fingerprint, or resolve a fingerprint to its session")
|
|
79
|
+
.argument("[value]", "fingerprint value to resolve (hqfp1:...)")
|
|
80
|
+
.option("--json", "output JSON")
|
|
81
|
+
.action(async (value, opts) => {
|
|
82
|
+
if (!value) {
|
|
83
|
+
fingerprint.help();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!isFingerprint(value)) {
|
|
87
|
+
process.stderr.write("not an HQ fingerprint\n");
|
|
88
|
+
process.exitCode = 2;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const record = resolveLocalFingerprint(hqRootFromEnv(), value);
|
|
92
|
+
if (record) {
|
|
93
|
+
if (opts.json) {
|
|
94
|
+
process.stdout.write(`${JSON.stringify({ ...record, found: true })}\n`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const lines = [
|
|
98
|
+
`fingerprint: ${record.fingerprint}`,
|
|
99
|
+
`identity: ${record.identityUid ?? "-"} (${record.identityKind ?? "unknown"})`,
|
|
100
|
+
`machine: ${record.machineId ?? "-"} (${record.hostKind ?? "unknown"})`,
|
|
101
|
+
`session: ${record.sessionId ?? "-"}`,
|
|
102
|
+
`company: ${record.company ?? "-"}`,
|
|
103
|
+
`source: ${record.source}`,
|
|
104
|
+
];
|
|
105
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let remote = null;
|
|
109
|
+
try {
|
|
110
|
+
remote = await lookupRegistry(value);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
// lookupRegistry already turns request failures into null; this is a
|
|
114
|
+
// failure to load the client itself.
|
|
115
|
+
process.stderr.write(`hq fingerprint: registry lookup failed: ${err.message}\n`);
|
|
116
|
+
}
|
|
117
|
+
if (!remote) {
|
|
118
|
+
if (opts.json) {
|
|
119
|
+
process.stdout.write(`${JSON.stringify({ fingerprint: value, found: false })}\n`);
|
|
120
|
+
}
|
|
121
|
+
process.stderr.write("no fingerprint record\n");
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (opts.json) {
|
|
126
|
+
process.stdout.write(`${JSON.stringify({
|
|
127
|
+
fingerprint: value,
|
|
128
|
+
...remote,
|
|
129
|
+
source: "registry",
|
|
130
|
+
label: remote.machineLabel,
|
|
131
|
+
firstSeenAt: remote.firstSeenAt,
|
|
132
|
+
found: true,
|
|
133
|
+
})}\n`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const identityName = remote.identityName ? `${remote.identityName} ` : "";
|
|
137
|
+
const machineLabel = remote.machineLabel ? `${remote.machineLabel} ` : "";
|
|
138
|
+
const lines = [
|
|
139
|
+
`fingerprint: ${value}`,
|
|
140
|
+
`identity: ${identityName}(${remote.identityUid}, ${remote.identityKind})`,
|
|
141
|
+
`machine: ${machineLabel}(${remote.hostKind})`,
|
|
142
|
+
`session: ${remote.sessionId}`,
|
|
143
|
+
`company: ${remote.companySlug}`,
|
|
144
|
+
`first seen: ${remote.firstSeenAt}`,
|
|
145
|
+
`source: registry`,
|
|
146
|
+
];
|
|
147
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
148
|
+
});
|
|
149
|
+
fingerprint
|
|
150
|
+
.command("self")
|
|
151
|
+
.description("Print the fingerprint for the current HQ session")
|
|
152
|
+
.option("--json", "output JSON")
|
|
153
|
+
.option("--offline", "never make a network call (identity lookup or registration)")
|
|
154
|
+
.option("--no-register", "do not record this fingerprint with the HQ registry")
|
|
155
|
+
.action(async (_opts, cmd) => {
|
|
156
|
+
// The parent also defines --json and commander hands it to the parent,
|
|
157
|
+
// so read the merged options.
|
|
158
|
+
const opts = cmd.optsWithGlobals();
|
|
159
|
+
const offline = opts.offline === true || process.env.HQ_FINGERPRINT_OFFLINE === "1";
|
|
160
|
+
const foundRoot = findSelfHqRoot();
|
|
161
|
+
const noRoot = foundRoot === null;
|
|
162
|
+
const hqRoot = foundRoot ?? process.cwd();
|
|
163
|
+
// The commit-msg hook calls this on every commit, so it never throws
|
|
164
|
+
// and always exits 0.
|
|
165
|
+
let result;
|
|
166
|
+
let built = null;
|
|
167
|
+
try {
|
|
168
|
+
built = await buildSelfFingerprint({ hqRoot, offline, noRoot });
|
|
169
|
+
result = built;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
process.stderr.write(`hq fingerprint: ${err.message}\n`);
|
|
173
|
+
result = { fingerprint: null, reason: `could not build fingerprint: ${err.message}` };
|
|
174
|
+
}
|
|
175
|
+
if (opts.json) {
|
|
176
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
177
|
+
}
|
|
178
|
+
else if (result.fingerprint) {
|
|
179
|
+
process.stdout.write(`${result.fingerprint}\n`);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
process.stdout.write(`no fingerprint: ${result.reason ?? "unknown"}\n`);
|
|
183
|
+
}
|
|
184
|
+
// Registration runs after the output is written and reports only to
|
|
185
|
+
// ~/.hq/fingerprint.log; registerSelfFingerprint does not throw.
|
|
186
|
+
if (built?.fingerprint && !noRoot) {
|
|
187
|
+
await registerSelfFingerprint(built, { hqRoot, offline, register: opts.register !== false });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=fingerprint.js.map
|
package/dist/commands/lanes.js
CHANGED
|
@@ -2432,11 +2432,18 @@ function autopilotWorkerStarter(ctx) {
|
|
|
2432
2432
|
return { lane_id: started.lane_id };
|
|
2433
2433
|
};
|
|
2434
2434
|
}
|
|
2435
|
+
/**
|
|
2436
|
+
* The senior for a lane created without `--senior`. Inside a lane worker
|
|
2437
|
+
* (HQ_LANE_ID set) the worker's own session wins, because HQ_PARENT_SESSION_ID
|
|
2438
|
+
* there names the session that started the worker, one level too high.
|
|
2439
|
+
*/
|
|
2435
2440
|
function defaultSenior(ctx) {
|
|
2441
|
+
const sessionId = sessionIdFromEnv(ctx.env);
|
|
2442
|
+
if (ctx.env.HQ_LANE_ID?.trim() && sessionId)
|
|
2443
|
+
return { kind: "session", id: sessionId };
|
|
2436
2444
|
const parent = ctx.env.HQ_PARENT_SESSION_ID?.trim();
|
|
2437
2445
|
if (parent)
|
|
2438
2446
|
return { kind: "session", id: parent };
|
|
2439
|
-
const sessionId = sessionIdFromEnv(ctx.env);
|
|
2440
2447
|
if (sessionId)
|
|
2441
2448
|
return { kind: "session", id: sessionId };
|
|
2442
2449
|
return { kind: "user" };
|
|
@@ -6305,6 +6312,7 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane, resolveLaneTarget
|
|
|
6305
6312
|
...(showRefresh.remoteRefresh === undefined ? {} : { remote_refresh: showRefresh.remoteRefresh }),
|
|
6306
6313
|
provider_session_id: lane.provider_session_id ?? null,
|
|
6307
6314
|
provider_session_id_absent: lane.provider_session_id_absent ?? null,
|
|
6315
|
+
fingerprint: lane.fingerprint ?? null,
|
|
6308
6316
|
envelope_repair_mode: lane.envelope_repair?.mode ?? null,
|
|
6309
6317
|
envelope_validation_errors: lane.envelope_validation_errors ?? [],
|
|
6310
6318
|
last_resume: lane.last_resume ?? null,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fingerprint test vectors. hq-pro (US-006) copies this list so the CLI
|
|
3
|
+
* and the server agree on what a valid fingerprint is.
|
|
4
|
+
*/
|
|
5
|
+
import type { FingerprintParts } from "./format.js";
|
|
6
|
+
export interface ValidFingerprintVector {
|
|
7
|
+
name: string;
|
|
8
|
+
parts: FingerprintParts;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
export interface InvalidFingerprintVector {
|
|
12
|
+
name: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const FINGERPRINT_TEST_VECTORS: {
|
|
16
|
+
valid: ValidFingerprintVector[];
|
|
17
|
+
invalid: InvalidFingerprintVector[];
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=fixtures.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const FINGERPRINT_TEST_VECTORS = {
|
|
2
|
+
valid: [
|
|
3
|
+
{
|
|
4
|
+
name: "person on a laptop",
|
|
5
|
+
parts: {
|
|
6
|
+
company: "indigo",
|
|
7
|
+
identityUid: "prs_01J8ZK4Q7M2N3P4R5S6T7V8W9X",
|
|
8
|
+
hostKind: "laptop",
|
|
9
|
+
machineId: "e3ef5a5a-f51d-435d-90b9-dd876caa249b",
|
|
10
|
+
sessionId: "fabb19c9-95a7-48a4-9a95-f77e28d0e3db",
|
|
11
|
+
},
|
|
12
|
+
value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "agent on a fleet box",
|
|
16
|
+
parts: {
|
|
17
|
+
company: "acme",
|
|
18
|
+
identityUid: "agt_01JA0000000000000000000000",
|
|
19
|
+
hostKind: "fleet",
|
|
20
|
+
machineId: "0123456789abcdef",
|
|
21
|
+
sessionId: "sess-1",
|
|
22
|
+
},
|
|
23
|
+
value: "hqfp1:acme:agt_01JA0000:fleet.01234567:sess-1",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "outpost with no company",
|
|
27
|
+
parts: {
|
|
28
|
+
company: null,
|
|
29
|
+
identityUid: "prs_01J8ZK4Q7M2N3P4R5S6T7V8W9X",
|
|
30
|
+
hostKind: "outpost",
|
|
31
|
+
machineId: "ABCDEF12-0000-0000-0000-000000000000",
|
|
32
|
+
sessionId: null,
|
|
33
|
+
},
|
|
34
|
+
value: "hqfp1:-:prs_01J8ZK4Q:outpost.abcdef12:-",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "session only",
|
|
38
|
+
parts: {
|
|
39
|
+
company: null,
|
|
40
|
+
identityUid: null,
|
|
41
|
+
hostKind: null,
|
|
42
|
+
machineId: null,
|
|
43
|
+
sessionId: "abcdefgh12345",
|
|
44
|
+
},
|
|
45
|
+
value: "hqfp1:-:-:-:abcdefgh",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
invalid: [
|
|
49
|
+
{ name: "empty", value: "" },
|
|
50
|
+
{ name: "wrong prefix", value: "hqfp2:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9" },
|
|
51
|
+
{ name: "too few fields", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a" },
|
|
52
|
+
{ name: "too many fields", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9:x" },
|
|
53
|
+
{ name: "bad identity prefix", value: "hqfp1:indigo:usr_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9" },
|
|
54
|
+
{ name: "short identity", value: "hqfp1:indigo:prs_01J8:laptop.e3ef5a5a:fabb19c9" },
|
|
55
|
+
{ name: "bad host kind", value: "hqfp1:indigo:prs_01J8ZK4Q:desktop.e3ef5a5a:fabb19c9" },
|
|
56
|
+
{ name: "non-hex machine", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.zzzzzzzz:fabb19c9" },
|
|
57
|
+
{ name: "long session", value: "hqfp1:indigo:prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9x" },
|
|
58
|
+
{ name: "empty field", value: "hqfp1::prs_01J8ZK4Q:laptop.e3ef5a5a:fabb19c9" },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=fixtures.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HQ session fingerprint value format.
|
|
3
|
+
*
|
|
4
|
+
* A fingerprint names one HQ session without exposing a full identity, machine
|
|
5
|
+
* or session id:
|
|
6
|
+
*
|
|
7
|
+
* hqfp1:<company>:<identity>:<machine>:<session>
|
|
8
|
+
*
|
|
9
|
+
* - company: company slug, or "-"
|
|
10
|
+
* - identity: "prs_" or "agt_" plus the first 8 chars of the ULID, or "-"
|
|
11
|
+
* - machine: "<laptop|outpost|fleet>.<first 8 hex chars of the machine id>", or "-"
|
|
12
|
+
* - session: first 8 chars of the session id, or "-"
|
|
13
|
+
*
|
|
14
|
+
* No component may contain ":". hq-pro (US-006) copies the test vectors in
|
|
15
|
+
* ./fixtures.ts so both sides agree on the format.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FINGERPRINT_PREFIX = "hqfp1";
|
|
18
|
+
export declare const UNKNOWN = "-";
|
|
19
|
+
export type HostKind = "laptop" | "outpost" | "fleet";
|
|
20
|
+
export type IdentityKind = "person" | "agent";
|
|
21
|
+
export declare const HOST_KINDS: readonly HostKind[];
|
|
22
|
+
export interface FingerprintParts {
|
|
23
|
+
company: string | null;
|
|
24
|
+
/** Full `prs_*` or `agt_*` uid. Truncated by build(). */
|
|
25
|
+
identityUid: string | null;
|
|
26
|
+
hostKind: HostKind | null;
|
|
27
|
+
/** Full machine id (UUID or hex). Truncated by build(). */
|
|
28
|
+
machineId: string | null;
|
|
29
|
+
/** Full session id. Truncated by build(). */
|
|
30
|
+
sessionId: string | null;
|
|
31
|
+
}
|
|
32
|
+
export interface ParsedFingerprint {
|
|
33
|
+
value: string;
|
|
34
|
+
company: string | null;
|
|
35
|
+
/** Short identity, for example `prs_01J8ABCD`. */
|
|
36
|
+
identity: string | null;
|
|
37
|
+
identityKind: IdentityKind | null;
|
|
38
|
+
hostKind: HostKind | null;
|
|
39
|
+
/** Short machine id, 8 lowercase hex chars. */
|
|
40
|
+
machine: string | null;
|
|
41
|
+
/** Short session id, 8 chars. */
|
|
42
|
+
session: string | null;
|
|
43
|
+
}
|
|
44
|
+
export declare const COMPANY_RE: RegExp;
|
|
45
|
+
/** Build a fingerprint value. Throws when a component is malformed or contains ":". */
|
|
46
|
+
export declare function build(parts: FingerprintParts): string;
|
|
47
|
+
/** Parse a fingerprint value. Returns null for anything that is not a valid hqfp1 value. */
|
|
48
|
+
export declare function parse(value: string): ParsedFingerprint | null;
|
|
49
|
+
export declare function isFingerprint(value: unknown): value is string;
|
|
50
|
+
//# sourceMappingURL=format.d.ts.map
|