@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/command-catalog.generated.d.ts +45 -0
  3. package/dist/command-catalog.generated.js +56 -0
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +3 -1
  7. package/dist/commands/core-bucket-c-handoff-knowledge-native.js +8 -4
  8. package/dist/commands/core-handoff.d.ts +2 -0
  9. package/dist/commands/core-handoff.js +3 -1
  10. package/dist/commands/core-pipeline-workflow-runner-native.js +7 -5
  11. package/dist/commands/fingerprint.d.ts +10 -0
  12. package/dist/commands/fingerprint.js +191 -0
  13. package/dist/commands/lanes.js +9 -1
  14. package/dist/lib/fingerprint/fixtures.d.ts +19 -0
  15. package/dist/lib/fingerprint/fixtures.js +61 -0
  16. package/dist/lib/fingerprint/format.d.ts +50 -0
  17. package/dist/lib/fingerprint/format.js +105 -0
  18. package/dist/lib/fingerprint/offline.d.ts +16 -0
  19. package/dist/lib/fingerprint/offline.js +44 -0
  20. package/dist/lib/fingerprint/registry-client.d.ts +74 -0
  21. package/dist/lib/fingerprint/registry-client.js +141 -0
  22. package/dist/lib/fingerprint/resolve-local.d.ts +14 -0
  23. package/dist/lib/fingerprint/resolve-local.js +67 -0
  24. package/dist/lib/fingerprint/self-core.d.ts +139 -0
  25. package/dist/lib/fingerprint/self-core.js +371 -0
  26. package/dist/lib/fingerprint/self.d.ts +44 -0
  27. package/dist/lib/fingerprint/self.js +191 -0
  28. package/dist/lib/lanes/spawn.d.ts +19 -1
  29. package/dist/lib/lanes/spawn.js +50 -6
  30. package/dist/lib/lanes/story-ledger.d.ts +1 -0
  31. package/dist/lib/lanes/story-ledger.js +3 -1
  32. package/dist/lib/lanes/types.d.ts +12 -0
  33. package/dist/lib/lanes/types.js +12 -1
  34. package/dist/utils/cli-network-lifecycle.d.ts +2 -0
  35. package/dist/utils/cli-network-lifecycle.js +5 -1
  36. package/dist/utils/cognito-session.d.ts +2 -21
  37. package/dist/utils/cognito-session.js +3 -84
  38. package/dist/utils/person-token-cache.d.ts +32 -0
  39. package/dist/utils/person-token-cache.js +95 -0
  40. package/dist/utils/vault-api.d.ts +21 -1
  41. package/dist/utils/vault-api.js +54 -5
  42. 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
- const commitFailure = commitKnowledge(git, root, changes, handled, output);
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 committed = git(["commit", "-m", `checkpoint: handoff ${threadId}`], root);
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
- return String(process.env.HQ_PARENT_SESSION_ID
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
- || '').replace(/\s+/g, '');
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 (!process.env.HQ_PARENT_SESSION_ID && parent)
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
@@ -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