@indigoai-us/hq-cli 5.345.133 → 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 +17 -3
- 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/access.d.ts +2 -0
- package/dist/commands/access.js +58 -9
- 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,13 +2,27 @@
|
|
|
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
|
+
|
|
12
|
+
## [5.345.134] — 2026-10-15
|
|
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.
|
|
14
|
+
|
|
5
15
|
## [5.345.133] — 2026-10-15
|
|
16
|
+
- Pipeline loop lanes no longer hang after their first phase. 5.345.132 kept the lane running while its next phase resumed, which left the previous phase's watcher holding the lane, so the next phase's result was never recorded. That change is reverted; a loop lane may again show as done for a few seconds while its next phase starts.
|
|
17
|
+
|
|
18
|
+
## [5.345.132] — 2026-10-14
|
|
19
|
+
- A loop lane starting its next phase now stays running in `hq lanes status` and `hq lanes list` while the phase resumes, instead of showing done for several seconds.
|
|
20
|
+
- Recognize Git Bash on Git for Windows 2.56+, which reports `MSYSTEM=UCRT64` instead of `MINGW64`, so delegate jq calls keep their byte-exact output and journal titles with newlines get a valid file name on Windows.
|
|
21
|
+
|
|
22
|
+
## [5.345.131] — 2026-10-14
|
|
6
23
|
- `hq lanes --account auto` and `hq capacity route` no longer refuse every signed-in account when the capacity collector is off or its readings are stale; those accounts are picked with unknown usage, below accounts with fresh headroom.
|
|
7
24
|
- `hq lanes create` now resumes an earlier worker session only when the project, model, and effort match, and otherwise starts a fresh thread.
|
|
8
|
-
- A loop lane starting its next phase now stays running in `hq lanes status` and `hq lanes list` while the phase resumes, instead of showing done for several seconds.
|
|
9
25
|
- A loop lane waiting for its next phase now shows as running in `hq lanes status` and `hq lanes list`, and becomes done only after it is stopped.
|
|
10
|
-
- Recognize Git Bash on Git for Windows 2.56+, which reports `MSYSTEM=UCRT64` instead of `MINGW64`, so delegate jq calls keep their byte-exact output and journal titles with newlines get a valid file name on Windows.
|
|
11
|
-
- Pipeline loop lanes no longer hang after their first phase. 5.345.132 kept the lane running while its next phase resumed, which left the previous phase's watcher holding the lane, so the next phase's result was never recorded. That change is reverted; a loop lane may again show as done for a few seconds while its next phase starts.
|
|
12
26
|
|
|
13
27
|
## [5.345.130] — 2026-10-14
|
|
14
28
|
- Add native TypeScript `hq core workflow run` orchestration with the bundled runner as its flag-off fallback.
|
|
@@ -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" },
|
|
@@ -48,6 +48,8 @@ export interface AccessAclTree {
|
|
|
48
48
|
updatedAt: string;
|
|
49
49
|
} | null;
|
|
50
50
|
effectivePermission: string | null;
|
|
51
|
+
/** Most-specific ACL row covering this prefix, including empty lock rows. */
|
|
52
|
+
coveringAclPath?: string | null;
|
|
51
53
|
}
|
|
52
54
|
export interface AccessMember {
|
|
53
55
|
personUid: string;
|
package/dist/commands/access.js
CHANGED
|
@@ -91,6 +91,27 @@ function fileGrantCoversPath(grantPath, targetPath) {
|
|
|
91
91
|
return classifyAclSource(targetPath, [grantPath]) !== "role-bypass";
|
|
92
92
|
}
|
|
93
93
|
const FILE_PERMISSION_RANK = { read: 1, write: 2, admin: 3 };
|
|
94
|
+
function aclPatternSpecificity(pattern) {
|
|
95
|
+
if (pattern === "*" || pattern === "")
|
|
96
|
+
return [0, 0];
|
|
97
|
+
const glob = pattern.endsWith("/*");
|
|
98
|
+
const body = glob ? pattern.slice(0, -2) : pattern;
|
|
99
|
+
const segs = body.split("/").filter(Boolean).length;
|
|
100
|
+
return [segs, glob ? 0 : 1];
|
|
101
|
+
}
|
|
102
|
+
function pickMostSpecificCoveringRow(rows, targetPath, company) {
|
|
103
|
+
const covering = rows.filter((row) => fileGrantCoversPath(toBucketRelative(row.path, company), targetPath));
|
|
104
|
+
if (covering.length === 0)
|
|
105
|
+
return null;
|
|
106
|
+
covering.sort((a, b) => {
|
|
107
|
+
const [aSegs, aExact] = aclPatternSpecificity(toBucketRelative(a.path, company));
|
|
108
|
+
const [bSegs, bExact] = aclPatternSpecificity(toBucketRelative(b.path, company));
|
|
109
|
+
if (aSegs !== bSegs)
|
|
110
|
+
return bSegs - aSegs;
|
|
111
|
+
return bExact - aExact;
|
|
112
|
+
});
|
|
113
|
+
return covering[0] ?? null;
|
|
114
|
+
}
|
|
94
115
|
function httpStatusOf(err) {
|
|
95
116
|
if (!err || typeof err !== "object")
|
|
96
117
|
return undefined;
|
|
@@ -146,10 +167,13 @@ function treeIsEmpty(tree) {
|
|
|
146
167
|
function aclDenied(tree) {
|
|
147
168
|
if (!tree)
|
|
148
169
|
return false;
|
|
170
|
+
const perm = tree.effectivePermission;
|
|
171
|
+
const denied = perm === null || perm === "none" || perm === "deny";
|
|
172
|
+
if (tree.coveringAclPath && denied)
|
|
173
|
+
return true;
|
|
149
174
|
if (treeIsEmpty(tree))
|
|
150
175
|
return false;
|
|
151
|
-
|
|
152
|
-
return perm === null || perm === "none" || perm === "deny";
|
|
176
|
+
return denied;
|
|
153
177
|
}
|
|
154
178
|
/**
|
|
155
179
|
* Build the result and record one cause-mix telemetry line. Every exit path
|
|
@@ -289,6 +313,18 @@ export async function runAccess(input) {
|
|
|
289
313
|
}
|
|
290
314
|
else if (fuzzy.length === 0) {
|
|
291
315
|
steps.push("list: 0 hits");
|
|
316
|
+
const tree = await deps.fetchAclTree(relative);
|
|
317
|
+
if (aclDenied(tree)) {
|
|
318
|
+
steps.push("acl: deny");
|
|
319
|
+
const result = finish("no-access", {
|
|
320
|
+
path: anchored(relative) || input.target,
|
|
321
|
+
company: slug,
|
|
322
|
+
exists: true,
|
|
323
|
+
steps,
|
|
324
|
+
}, finishCtx);
|
|
325
|
+
emit(input, result, log);
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
292
328
|
const result = finish("never-existed", {
|
|
293
329
|
path: anchored(relative) || input.target,
|
|
294
330
|
company: slug,
|
|
@@ -632,7 +668,7 @@ async function promptPick(candidates) {
|
|
|
632
668
|
function treeFromBody(body) {
|
|
633
669
|
if (!body)
|
|
634
670
|
return null;
|
|
635
|
-
if (treeIsEmpty(body))
|
|
671
|
+
if (treeIsEmpty(body) && !body.coveringAclPath)
|
|
636
672
|
return null;
|
|
637
673
|
return body;
|
|
638
674
|
}
|
|
@@ -730,12 +766,25 @@ export async function runAccessForPerson(rawPath, personUid, opts) {
|
|
|
730
766
|
}
|
|
731
767
|
const roleBypass = projection.files.roleBypass === true;
|
|
732
768
|
const grants = Array.isArray(projection.files.grants) ? projection.files.grants : [];
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
769
|
+
const locks = Array.isArray(projection.files.locks) ? projection.files.locks : [];
|
|
770
|
+
const coveringRows = [];
|
|
771
|
+
for (const grant of grants) {
|
|
772
|
+
if (typeof grant.path === "string" &&
|
|
773
|
+
typeof grant.permission === "string" &&
|
|
774
|
+
grant.permission in FILE_PERMISSION_RANK) {
|
|
775
|
+
coveringRows.push({
|
|
776
|
+
path: grant.path,
|
|
777
|
+
permission: grant.permission,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
for (const lock of locks) {
|
|
782
|
+
if (typeof lock.path === "string") {
|
|
783
|
+
coveringRows.push({ path: lock.path, permission: null });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const winner = pickMostSpecificCoveringRow(coveringRows, relativePath, company);
|
|
787
|
+
const permission = roleBypass ? "admin" : (winner?.permission ?? null);
|
|
739
788
|
return {
|
|
740
789
|
personUid: uid,
|
|
741
790
|
company,
|
|
@@ -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,
|