@indigoai-us/hq-cli 5.305.0 → 5.307.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/assets/scaffold/core/scripts/audit-log.sh +384 -0
  3. package/assets/scaffold/core/scripts/git-pack-extension-check.sh +164 -0
  4. package/assets/scaffold/core/scripts/hq-pin.sh +118 -0
  5. package/assets/scaffold/core/scripts/lib/portable.sh +211 -0
  6. package/assets/scaffold/core/scripts/refresh-vault-access.sh +188 -0
  7. package/dist/command-catalog.generated.d.ts +54 -2
  8. package/dist/command-catalog.generated.js +66 -2
  9. package/dist/commands/core-audit-log-native.d.ts +9 -0
  10. package/dist/commands/core-audit-log-native.js +257 -0
  11. package/dist/commands/core-git-pack-extension-check-native.d.ts +22 -0
  12. package/dist/commands/core-git-pack-extension-check-native.js +190 -0
  13. package/dist/commands/core-native-utilities-flag.d.ts +4 -0
  14. package/dist/commands/core-native-utilities-flag.js +7 -0
  15. package/dist/commands/core-native-utilities.d.ts +28 -0
  16. package/dist/commands/core-native-utilities.js +52 -0
  17. package/dist/commands/core-pin-native.d.ts +14 -0
  18. package/dist/commands/core-pin-native.js +236 -0
  19. package/dist/commands/core-refresh-vault-access-native.d.ts +20 -0
  20. package/dist/commands/core-refresh-vault-access-native.js +229 -0
  21. package/dist/commands/core.js +15 -0
  22. package/dist/commands/daemon.js +24 -23
  23. package/dist/commands/dm.js +6 -1
  24. package/dist/commands/lanes.js +43 -1
  25. package/dist/lib/agent-kit/fallback.d.ts +0 -7
  26. package/dist/lib/agent-kit/fallback.js +4 -4
  27. package/dist/lib/agent-kit/inbox-state.d.ts +0 -2
  28. package/dist/lib/agent-kit/inbox-state.js +2 -2
  29. package/dist/lib/agent-kit/kit-config.d.ts +0 -5
  30. package/dist/lib/agent-kit/kit-config.js +5 -5
  31. package/dist/lib/agent-kit/log.d.ts +0 -1
  32. package/dist/lib/agent-kit/log.js +1 -1
  33. package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +3 -12
  34. package/dist/lib/agent-kit/mcp/jsonrpc.js +4 -4
  35. package/dist/lib/agent-kit/mcp/tools.d.ts +0 -2
  36. package/dist/lib/agent-kit/mcp/tools.js +2 -2
  37. package/dist/lib/agent-kit/services.d.ts +0 -1
  38. package/dist/lib/agent-kit/services.js +1 -1
  39. package/dist/lib/checkpoint-ontology/apply.d.ts +0 -49
  40. package/dist/lib/checkpoint-ontology/apply.js +5 -5
  41. package/dist/lib/checkpoint-ontology/identity.d.ts +2 -1
  42. package/dist/lib/checkpoint-ontology/identity.js +1 -1
  43. package/dist/lib/checkpoint-ontology/multi-writer.d.ts +4 -9
  44. package/dist/lib/checkpoint-ontology/multi-writer.js +6 -6
  45. package/dist/lib/checkpoint-ontology/plan.d.ts +4 -17
  46. package/dist/lib/checkpoint-ontology/plan.js +5 -5
  47. package/dist/lib/checkpoint-ontology/queued-touches.d.ts +0 -3
  48. package/dist/lib/checkpoint-ontology/queued-touches.js +3 -3
  49. package/dist/lib/daemon/catalog.js +6 -2
  50. package/dist/lib/daemon/remote-control.d.ts +19 -8
  51. package/dist/lib/daemon/remote-control.js +62 -12
  52. package/dist/lib/daemon/supervisor.d.ts +2 -0
  53. package/dist/lib/daemon/supervisor.js +11 -0
  54. package/dist/lib/db/control-plane.js +6 -0
  55. package/dist/lib/deploy-apps.js +5 -0
  56. package/dist/startup-registration.js +9 -3
  57. package/dist/utils/install-tree-torn.d.ts +13 -6
  58. package/dist/utils/install-tree-torn.js +42 -16
  59. package/dist/utils/sentry-fingerprint.js +1 -0
  60. package/package.json +1 -1
@@ -0,0 +1,236 @@
1
+ /** Native per-session pin implementation matching core/scripts/hq-pin.sh. */
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { resolveLiveRoot } from "../utils/hq-roots.js";
6
+ import { bundledShellUnboundValue, registerNativeUtility, utilityError } from "./core-native-utilities.js";
7
+ export const PIN_ASSET = "core/scripts/hq-pin.sh";
8
+ export const PIN_HELP = `# hq-pin.sh — anchor the current session to one goal (the "pin").
9
+ #
10
+ # A pin is the message, goal, or target the session keeps working toward
11
+ # across loops, wakeups, compactions, and hand-offs. It is stored per session
12
+ # and re-read at every checkpoint so the session cannot drift.
13
+ #
14
+ # Usage:
15
+ # core/scripts/hq-pin.sh set "<goal>" [--done "<criterion>"]... [--owner <name>] [--channel <dm-channel>]
16
+ # core/scripts/hq-pin.sh show # print the pin (goal, done criteria, owner, channel, set-at)
17
+ # core/scripts/hq-pin.sh check # print a one-line reminder for loop ticks; exit 3 if no pin
18
+ # core/scripts/hq-pin.sh note "<progress>" # append a dated progress line under the pin
19
+ # core/scripts/hq-pin.sh clear # remove the pin (the goal is met or withdrawn)
20
+ #
21
+ # Storage: workspace/sessions/<sid>/pin.md plus \`pin: <goal>\` in the session's
22
+ `;
23
+ const SESSION_ENV_KEYS = ["HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "GROK_SESSION_ID"];
24
+ const stamp = (date) => date.toISOString().replace(/\.\d{3}Z$/, "Z");
25
+ function normalizeId(value) { return value.replace(/[\t\n\v\f\r ]/g, ""); }
26
+ function validId(value) { return value !== "" && value !== "." && value !== ".." && /^[A-Za-z0-9._-]+$/.test(value); }
27
+ export function resolvePinSessionId(root, env) {
28
+ for (const key of SESSION_ENV_KEYS) {
29
+ const value = normalizeId(env[key] ?? "");
30
+ if (validId(value))
31
+ return value;
32
+ }
33
+ try {
34
+ const value = normalizeId(fs.readFileSync(path.join(root, "workspace/sessions/.current"), "utf8"));
35
+ return validId(value) ? value : "";
36
+ }
37
+ catch {
38
+ return "";
39
+ }
40
+ }
41
+ function pinPath(deps, sid) { return path.join(deps.root, "workspace/sessions", sid, "pin.md"); }
42
+ function fail(message) { return utilityError(`hq-pin: ${message}`, 2); }
43
+ function sessionMetaPath(deps, sid) { return path.join(deps.root, "workspace/sessions", sid, "meta.yaml"); }
44
+ function writeSessionPin(deps, sid, goal) {
45
+ const meta = sessionMetaPath(deps, sid);
46
+ fs.mkdirSync(path.dirname(meta), { recursive: true });
47
+ let contents;
48
+ try {
49
+ contents = fs.readFileSync(meta, "utf8");
50
+ }
51
+ catch (error) {
52
+ if (error.code !== "ENOENT")
53
+ throw error;
54
+ contents = `session_id: ${sid}\nstarted_at: "${stamp(deps.now())}"\nsenior: user\n`;
55
+ }
56
+ const lines = contents ? contents.replace(/\n$/, "").split("\n") : [];
57
+ let found = false;
58
+ const next = lines.map((line) => {
59
+ if (line.trimStart().split(/[\t ]/)[0] === "pin:") {
60
+ found = true;
61
+ return `pin: ${goal}`;
62
+ }
63
+ return line;
64
+ });
65
+ if (!found)
66
+ next.push(`pin: ${goal}`);
67
+ const text = `${next.join("\n")}\n`;
68
+ const temp = path.join(path.dirname(meta), `.meta.yaml.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`);
69
+ fs.writeFileSync(temp, text, { mode: 0o600 });
70
+ fs.renameSync(temp, meta);
71
+ }
72
+ function createPinDirectory(deps) {
73
+ const sid = resolvePinSessionId(deps.root, deps.env);
74
+ if (!sid)
75
+ return { error: fail("no current session (hq-session.sh current is empty)") };
76
+ const file = pinPath(deps, sid);
77
+ fs.mkdirSync(path.dirname(file), { recursive: true });
78
+ return { sid, file };
79
+ }
80
+ function setPin(args, deps) {
81
+ const goal = args[0] ?? "";
82
+ if (!goal)
83
+ return fail("set needs the goal text");
84
+ let owner = "";
85
+ let channel = "";
86
+ const done = [];
87
+ for (let i = 1; i < args.length;) {
88
+ const arg = args[i];
89
+ if (arg === "--done") {
90
+ if (i + 1 >= args.length)
91
+ return bundledShellUnboundValue(PIN_ASSET, 36);
92
+ done.push(args[i + 1]);
93
+ i += 2;
94
+ }
95
+ else if (arg === "--owner") {
96
+ if (i + 1 >= args.length)
97
+ return bundledShellUnboundValue(PIN_ASSET, 37);
98
+ owner = args[i + 1];
99
+ i += 2;
100
+ }
101
+ else if (arg === "--channel") {
102
+ if (i + 1 >= args.length)
103
+ return bundledShellUnboundValue(PIN_ASSET, 38);
104
+ channel = args[i + 1];
105
+ i += 2;
106
+ }
107
+ else
108
+ return fail(`set: unknown arg ${arg}`);
109
+ }
110
+ const current = createPinDirectory(deps);
111
+ if (current.error)
112
+ return current.error;
113
+ const lines = ["# Pin", "", `Goal: ${goal}`, "", `Set: ${stamp(deps.now())}`];
114
+ if (owner)
115
+ lines.push(`Owner: ${owner}`);
116
+ if (channel)
117
+ lines.push(`Channel: ${channel}`);
118
+ if (done.length > 0)
119
+ lines.push("", "## Done when", ...done.map((criterion) => `- [ ] ${criterion}`));
120
+ lines.push("", "## Progress");
121
+ fs.writeFileSync(current.file, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o666 });
122
+ writeSessionPin(deps, current.sid, goal);
123
+ return { code: 0, stdout: `pinned session ${current.sid}: ${goal}\n`, stderr: "" };
124
+ }
125
+ function showPin(deps) {
126
+ const current = createPinDirectory(deps);
127
+ if (current.error)
128
+ return current.error;
129
+ if (!fs.existsSync(current.file))
130
+ return { code: 3, stdout: `no pin for session ${current.sid}\n`, stderr: "" };
131
+ return { code: 0, stdout: fs.readFileSync(current.file, "utf8"), stderr: "" };
132
+ }
133
+ function checkPin(deps) {
134
+ const current = createPinDirectory(deps);
135
+ if (current.error)
136
+ return current.error;
137
+ if (!fs.existsSync(current.file))
138
+ return { code: 3, stdout: "no pin\n", stderr: "" };
139
+ const text = fs.readFileSync(current.file, "utf8");
140
+ const goal = text.split(/\r?\n/).find((line) => line.startsWith("Goal: "))?.slice(6) ?? "";
141
+ const open = (text.match(/^- \[ \]/gm) ?? []).length;
142
+ const closed = (text.match(/^- \[x\]/gm) ?? []).length;
143
+ return { code: 0, stdout: `PIN: ${goal} (done criteria: ${closed} met, ${open} open)\n`, stderr: "" };
144
+ }
145
+ function notePin(args, deps) {
146
+ const text = args[0] ?? "";
147
+ if (!text)
148
+ return fail("note needs text");
149
+ const current = createPinDirectory(deps);
150
+ if (current.error)
151
+ return current.error;
152
+ if (!fs.existsSync(current.file))
153
+ return fail("no pin to note against");
154
+ const time = deps.now().toISOString().slice(11, 16);
155
+ fs.appendFileSync(current.file, `- ${time}Z ${text}\n`);
156
+ return { code: 0, stdout: "noted\n", stderr: "" };
157
+ }
158
+ function donePin(args, deps) {
159
+ const needle = args[0] ?? "";
160
+ if (!needle)
161
+ return fail("done needs a criterion substring");
162
+ const current = createPinDirectory(deps);
163
+ if (current.error)
164
+ return current.error;
165
+ if (!fs.existsSync(current.file))
166
+ return fail("no pin");
167
+ const text = fs.readFileSync(current.file, "utf8");
168
+ const lines = text.split("\n");
169
+ // Bash read ignores a final line without a newline. The shell script then
170
+ // echoes every line it did read, adding a newline to each one.
171
+ lines.pop();
172
+ let hit = false;
173
+ const next = lines.map((line) => {
174
+ if (line.startsWith("- [ ] ") && line.includes(needle)) {
175
+ hit = true;
176
+ // Preserve the bundled shell's parameter-expansion result: it keeps the
177
+ // matched source line after adding the checked marker.
178
+ return `- [x] ${line}`;
179
+ }
180
+ return line;
181
+ });
182
+ const output = next.length > 0 ? `${next.join("\n")}\n` : "";
183
+ const tempDir = fs.mkdtempSync(path.join(deps.tmpDir ?? os.tmpdir(), "hq-pin-"));
184
+ const tempFile = path.join(tempDir, "pin");
185
+ try {
186
+ fs.writeFileSync(tempFile, output, { encoding: "utf8", mode: 0o600, flag: "wx" });
187
+ try {
188
+ fs.renameSync(tempFile, current.file);
189
+ }
190
+ catch (error) {
191
+ if (error.code !== "EXDEV")
192
+ throw error;
193
+ fs.copyFileSync(tempFile, current.file);
194
+ fs.chmodSync(current.file, 0o600);
195
+ fs.unlinkSync(tempFile);
196
+ }
197
+ }
198
+ finally {
199
+ fs.rmSync(tempDir, { recursive: true, force: true });
200
+ }
201
+ return hit
202
+ ? { code: 0, stdout: `marked done: ${needle}\n`, stderr: "" }
203
+ : fail(`no open criterion matches: ${needle}`);
204
+ }
205
+ function clearPin(deps) {
206
+ const current = createPinDirectory(deps);
207
+ if (current.error)
208
+ return current.error;
209
+ fs.rmSync(current.file, { force: true });
210
+ writeSessionPin(deps, current.sid, "");
211
+ return { code: 0, stdout: "pin cleared\n", stderr: "" };
212
+ }
213
+ export function runPin(args, deps) {
214
+ const [command = "", ...rest] = args;
215
+ if (command === "set")
216
+ return setPin(rest, deps);
217
+ if (command === "show")
218
+ return showPin(deps);
219
+ if (command === "check")
220
+ return checkPin(deps);
221
+ if (command === "note")
222
+ return notePin(rest, deps);
223
+ if (command === "done")
224
+ return donePin(rest, deps);
225
+ if (command === "clear")
226
+ return clearPin(deps);
227
+ return { code: 2, stdout: PIN_HELP, stderr: "" };
228
+ }
229
+ export function registerCorePinNative(core) {
230
+ registerNativeUtility(core, {
231
+ name: "pin", asset: PIN_ASSET, root: "live", summary: "Pin the current session to one goal",
232
+ resolveRoot: (_cwd, hqRoot) => resolveLiveRoot({ hqRoot }),
233
+ run: (args, context) => runPin(args, context),
234
+ });
235
+ }
236
+ //# sourceMappingURL=core-pin-native.js.map
@@ -0,0 +1,20 @@
1
+ import type { Command } from "commander";
2
+ import { type UtilityOutput } from "./core-native-utilities.js";
3
+ export declare const REFRESH_VAULT_ACCESS_ASSET = "core/scripts/refresh-vault-access.sh";
4
+ export declare const REFRESH_VAULT_ACCESS_HELP: string;
5
+ export type HqResult = {
6
+ code: number;
7
+ stdout: string;
8
+ stderr: string;
9
+ missing?: boolean;
10
+ };
11
+ export type RefreshVaultAccessDependencies = {
12
+ cwd: string;
13
+ env: NodeJS.ProcessEnv;
14
+ now: () => Date;
15
+ hqAvailable: boolean;
16
+ runHq?: (args: string[], cwd: string) => HqResult;
17
+ };
18
+ export declare function runRefreshVaultAccess(args: string[], deps: RefreshVaultAccessDependencies): UtilityOutput;
19
+ export declare function registerCoreRefreshVaultAccessNative(core: Command): void;
20
+ //# sourceMappingURL=core-refresh-vault-access-native.d.ts.map
@@ -0,0 +1,229 @@
1
+ /** Native local manifest refresh for core/scripts/refresh-vault-access.sh. */
2
+ import { spawnSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
6
+ import { registerNativeUtility } from "./core-native-utilities.js";
7
+ export const REFRESH_VAULT_ACCESS_ASSET = "core/scripts/refresh-vault-access.sh";
8
+ export const REFRESH_VAULT_ACCESS_HELP = `# hq-core: public
9
+ # refresh-vault-access.sh — build/refresh the local vault-access manifest that
10
+ # enforce-vault-write-access.sh (PreToolUse hook) reads.
11
+ #
12
+ # Output: <hqRoot>/.hq/vault-access.json
13
+ #
14
+ # {
15
+ # "version": 1,
16
+ # "generatedAt": "2026-01-01T00:00:00Z",
17
+ # "companies": {
18
+ # "<slug>": {
19
+ # "role": "owner|admin|member|guest|unknown",
20
+ # "grants": [ { "path": "reports/*", "permission": "read|write" } ]
21
+ # }
22
+ # }
23
+ # }
24
+ #
25
+ # Sources (best-effort, all via the hq CLI):
26
+ # - grants: \`hq files shared-with-me\` (explicit per-prefix grants)
27
+ # - role: \`hq members --company <slug> list\` matched against the caller's
28
+ # email (from \`hq whoami\`, override with HQ_VAULT_ACCESS_EMAIL)
29
+ #
30
+ # Fail-open by design: anything this script cannot determine is recorded as
31
+ # role "unknown" (the hook does not enforce unknown roles), and a company it
32
+ # cannot query at all is left out of the manifest entirely (the hook allows
33
+ # companies absent from the manifest). The server-side STS/ACL layer remains
34
+ # the authoritative security boundary — this manifest only powers the local
35
+ # early-warning block for read-only shares.
36
+ #
37
+ # Usage:
38
+ # bash core/scripts/refresh-vault-access.sh [--company <slug>]... [--root <hqRoot>]
39
+ #
40
+ `.replace(/^# ?/gm, "");
41
+ function defaultHq(args, cwd, env) {
42
+ const result = spawnSync("hq", args, { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
43
+ if (result.error)
44
+ return { code: 127, stdout: "", stderr: result.error.message, missing: result.error.code === "ENOENT" };
45
+ return { code: result.status ?? 128, stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? "") };
46
+ }
47
+ function executableOnPath(name, value) {
48
+ if (!value)
49
+ return false;
50
+ for (const directory of value.split(path.delimiter)) {
51
+ if (!directory)
52
+ continue;
53
+ const candidate = path.join(directory, name);
54
+ try {
55
+ fs.accessSync(candidate, fs.constants.X_OK);
56
+ return true;
57
+ }
58
+ catch { /* keep checking the PATH */ }
59
+ }
60
+ return false;
61
+ }
62
+ function isHqRoot(root) {
63
+ const candidate = root.endsWith(path.sep) ? root.slice(0, -1) : root;
64
+ if (!candidate)
65
+ return false;
66
+ try {
67
+ return fs.statSync(path.join(candidate, "core")).isDirectory() && fs.statSync(path.join(candidate, ".claude")).isDirectory();
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ function parseOptions(args) {
74
+ let root = "";
75
+ const slugs = [];
76
+ for (let i = 0; i < args.length;) {
77
+ const arg = args[i];
78
+ if (arg === "--root") {
79
+ if (i + 1 >= args.length)
80
+ return { root, slugs, help: false, error: "--root requires a value" };
81
+ root = args[i + 1];
82
+ i += 2;
83
+ }
84
+ else if (arg === "--company") {
85
+ if (i + 1 >= args.length)
86
+ return { root, slugs, help: false, error: "--company requires a value" };
87
+ slugs.push(args[i + 1]);
88
+ i += 2;
89
+ }
90
+ else if (arg === "-h" || arg === "--help")
91
+ return { root, slugs, help: true };
92
+ else
93
+ return { root, slugs, help: false, error: `unknown argument: ${arg}` };
94
+ }
95
+ return { root, slugs, help: false };
96
+ }
97
+ function resolveRoot(parsedRoot, deps) {
98
+ if (parsedRoot)
99
+ return parsedRoot;
100
+ if (isHqRoot(deps.env.CLAUDE_PROJECT_DIR ?? ""))
101
+ return deps.env.CLAUDE_PROJECT_DIR;
102
+ let current = path.resolve(deps.cwd);
103
+ while (current !== path.parse(current).root) {
104
+ if (isHqRoot(current))
105
+ return current;
106
+ current = path.dirname(current);
107
+ }
108
+ return "";
109
+ }
110
+ // eslint-disable-next-line no-control-regex -- strip ANSI SGR sequences printed by hq.
111
+ function cleanAnsi(value) { return value.replace(/\u001b\[[0-9;]*m/g, ""); }
112
+ function log(message) { return `refresh-vault-access: ${message}\n`; }
113
+ function getEmail(output) {
114
+ const matches = cleanAnsi(output).match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g) ?? [];
115
+ return matches.find((email) => !email.includes("@outposts.")) ?? "";
116
+ }
117
+ function parseGrantTable(output) {
118
+ const grants = new Map();
119
+ for (let line of cleanAnsi(output).split(/\r?\n/)) {
120
+ line = line.replace(" (private folder)", "");
121
+ if (/^COMPANY\s/.test(line) || /^─/.test(line) || /^\s*$/.test(line) || /^Nothing is explicitly shared/.test(line))
122
+ continue;
123
+ const fields = line.trim().split(/\s+/);
124
+ if (fields.length < 3)
125
+ continue;
126
+ const permission = ["read", "write", "admin"].includes(fields[2]) ? fields[2]
127
+ : fields.length >= 4 && ["read", "write", "admin"].includes(fields[3]) ? fields[3] : "";
128
+ if (!permission)
129
+ continue;
130
+ const rows = grants.get(fields[0]) ?? [];
131
+ rows.push({ path: fields[1], permission });
132
+ grants.set(fields[0], rows);
133
+ }
134
+ return grants;
135
+ }
136
+ function roleFromTable(output, email) {
137
+ if (!email)
138
+ return "unknown";
139
+ for (const line of cleanAnsi(output).split(/\r?\n/)) {
140
+ const [candidate, role] = line.trim().split(/\s+/);
141
+ if (candidate === email && ["owner", "admin", "member", "guest"].includes(role ?? ""))
142
+ return role;
143
+ if (candidate === email)
144
+ return "unknown";
145
+ }
146
+ return "unknown";
147
+ }
148
+ function resolveEmail(deps, runHq) {
149
+ const override = deps.env.HQ_VAULT_ACCESS_EMAIL ?? "";
150
+ if (override)
151
+ return { email: override, stderr: "" };
152
+ const result = runHq(["whoami"], deps.cwd);
153
+ const email = getEmail(result.stdout);
154
+ return { email, stderr: email ? "" : log("could not determine caller email — roles will be 'unknown' (hook will not enforce)") };
155
+ }
156
+ export function runRefreshVaultAccess(args, deps) {
157
+ const parsed = parseOptions(args);
158
+ if (parsed.help)
159
+ return { code: 0, stdout: REFRESH_VAULT_ACCESS_HELP, stderr: "" };
160
+ if (parsed.error)
161
+ return { code: 1, stdout: "", stderr: log(parsed.error) };
162
+ const root = resolveRoot(parsed.root, deps);
163
+ if (!isHqRoot(root))
164
+ return { code: 1, stdout: "", stderr: log("could not resolve the HQ root (pass --root <hqRoot>)") };
165
+ if (!deps.hqAvailable)
166
+ return { code: 0, stdout: "", stderr: log("hq CLI not found — nothing refreshed (the write-access hook stays fail-open without a manifest)") };
167
+ const runHq = deps.runHq ?? ((hqArgs, cwd) => defaultHq(hqArgs, cwd, deps.env));
168
+ const identity = resolveEmail(deps, runHq);
169
+ let stderr = identity.stderr;
170
+ const explicit = parsed.slugs;
171
+ const allGrants = new Map();
172
+ if (explicit.length > 0) {
173
+ for (const slug of explicit) {
174
+ const result = runHq(["files", "shared-with-me", "--company", slug], deps.cwd);
175
+ if (result.code !== 0) {
176
+ stderr += log(`skipping ${slug} — shared-with-me query failed`);
177
+ continue;
178
+ }
179
+ const rows = parseGrantTable(result.stdout);
180
+ for (const [company, grants] of rows)
181
+ allGrants.set(company, [...(allGrants.get(company) ?? []), ...grants]);
182
+ }
183
+ }
184
+ else {
185
+ const result = runHq(["files", "shared-with-me"], deps.cwd);
186
+ if (result.code !== 0)
187
+ return { code: 0, stdout: "", stderr: `${stderr}${log("shared-with-me roll-up failed — nothing refreshed")}` };
188
+ for (const [company, grants] of parseGrantTable(result.stdout))
189
+ allGrants.set(company, grants);
190
+ }
191
+ const companies = new Set(allGrants.keys());
192
+ for (const slug of explicit)
193
+ companies.add(slug);
194
+ const sortedSlugs = [...companies].sort();
195
+ if (sortedSlugs.length === 0)
196
+ stderr += log("no explicit grants found — writing an empty manifest (hook stays fail-open everywhere)");
197
+ const companyData = {};
198
+ for (const slug of sortedSlugs) {
199
+ let role = "unknown";
200
+ if (identity.email) {
201
+ const result = runHq(["members", "--company", slug, "list"], deps.cwd);
202
+ role = result.code === 0 ? roleFromTable(result.stdout, identity.email) : "unknown";
203
+ }
204
+ const grants = (allGrants.get(slug) ?? []).map((grant) => ({ path: grant.path, permission: grant.permission }));
205
+ companyData[slug] = { role, grants };
206
+ }
207
+ const manifest = { version: 1, generatedAt: deps.now().toISOString().replace(/\.\d{3}Z$/, "Z"), companies: companyData };
208
+ const directory = path.join(root, ".hq");
209
+ fs.mkdirSync(directory, { recursive: true });
210
+ const target = path.join(directory, "vault-access.json");
211
+ const temp = `${target}.${process.pid}.${Math.random().toString(16).slice(2)}`;
212
+ fs.writeFileSync(temp, serializeJqJson(manifest, "pretty"), { encoding: "utf8", mode: 0o600, flag: "wx" });
213
+ fs.chmodSync(temp, 0o600);
214
+ fs.renameSync(temp, target);
215
+ stderr += log(`wrote .hq/vault-access.json (${sortedSlugs.length} companies, identity: ${identity.email || "unknown"})`);
216
+ return { code: 0, stdout: "", stderr };
217
+ }
218
+ export function registerCoreRefreshVaultAccessNative(core) {
219
+ registerNativeUtility(core, {
220
+ name: "refresh-vault-access", asset: REFRESH_VAULT_ACCESS_ASSET, root: "cwd", summary: "Refresh the local vault access manifest",
221
+ resolveRoot: (cwd) => cwd,
222
+ run: (args, context) => runRefreshVaultAccess(args, {
223
+ cwd: context.cwd, env: context.env, now: context.now,
224
+ hqAvailable: executableOnPath("hq", context.env.PATH),
225
+ runHq: (args, cwd) => defaultHq(args, cwd, context.env),
226
+ }),
227
+ });
228
+ }
229
+ //# sourceMappingURL=core-refresh-vault-access-native.js.map
@@ -38,6 +38,10 @@ import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
38
38
  import { registerCorePolicyCommand } from "./core-policy.js";
39
39
  import { registerCoreJobsNativeCommand } from "./core-job-native.js";
40
40
  import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
41
+ import { registerCoreAuditLogNative } from "./core-audit-log-native.js";
42
+ import { registerCoreGitPackExtensionCheckNative } from "./core-git-pack-extension-check-native.js";
43
+ import { registerCorePinNative } from "./core-pin-native.js";
44
+ import { registerCoreRefreshVaultAccessNative } from "./core-refresh-vault-access-native.js";
41
45
  import { resolveLiveRoot } from "../utils/hq-roots.js";
42
46
  import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
43
47
  import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
@@ -271,6 +275,13 @@ export const SCAFFOLD_ONLY_ASSETS = [
271
275
  { asset: "core/scripts/handoff-post.sh", root: "live", summary: "Rollback shell for native handoff post" },
272
276
  { asset: "core/scripts/handoff-open-steps.sh", root: "live", summary: "Rollback shell for native handoff open-steps" },
273
277
  { asset: "core/scripts/qmd-reindex-bg.sh", root: "live", summary: "Rollback handoff reindex forwarder" },
278
+ // Wave 5 native utility rollback companions, sourced verbatim from staging
279
+ // because these assets were not present in the hq-cli bundle at launch.
280
+ { asset: "core/scripts/audit-log.sh", root: "live", summary: "Rollback shell for native audit-log" },
281
+ { asset: "core/scripts/git-pack-extension-check.sh", root: "cwd", summary: "Rollback shell for native git-pack-extension-check" },
282
+ { asset: "core/scripts/hq-pin.sh", root: "live", summary: "Rollback shell for native pin" },
283
+ { asset: "core/scripts/refresh-vault-access.sh", root: "cwd", summary: "Rollback shell for native refresh-vault-access" },
284
+ { asset: "core/scripts/lib/portable.sh", root: "live", summary: "Shared portable helpers sourced by the audit-log rollback shell" },
274
285
  ...NATIVE_UTILITY_COMMANDS.map(({ asset, root, summary }) => ({
275
286
  asset,
276
287
  root,
@@ -354,6 +365,10 @@ export function registerCoreCommands(program) {
354
365
  registerCoreKnowledgePrefsCommand(core);
355
366
  registerCorePolicyCommand(core);
356
367
  registerCoreJobsNativeCommand(core);
368
+ registerCoreAuditLogNative(core);
369
+ registerCoreGitPackExtensionCheckNative(core);
370
+ registerCorePinNative(core);
371
+ registerCoreRefreshVaultAccessNative(core);
357
372
  // Wave 2 handoff/worktree commands retain their byte-identical shell assets
358
373
  // as a default-off rollback path for one release. Keep the gate helper small
359
374
  // so sibling Wave 2 work can reuse it without editing this command table.
@@ -627,25 +627,9 @@ async function runServiceSelfUpdate(d) {
627
627
  if (!result.ok)
628
628
  process.exitCode = 1;
629
629
  }
630
- /** Size, mtime and real path of a binary: changes whenever a new version is installed over it. */
631
- function binaryFingerprint(bin) {
632
- try {
633
- const real = fs.realpathSync(bin);
634
- const st = fs.statSync(real);
635
- return `${real}:${st.size}:${st.mtimeMs}`;
636
- }
637
- catch {
638
- return null;
639
- }
640
- }
641
630
  async function runTmuxService(d, opts) {
642
- const { installFingerprint, runRemoteControlHost } = await import("../lib/daemon/remote-control.js");
631
+ const { runRemoteControlHost, toolFingerprint } = await import("../lib/daemon/remote-control.js");
643
632
  const { findOnPath } = await import("../lib/daemon/run.js");
644
- const { resolveRunningInstall } = await import("../utils/version-gate.js");
645
- // The daemon keeps this host running across a CLI update, so a new hq-cli
646
- // install also counts as an update and restarts the session once it is quiet.
647
- const cliRoot = resolveRunningInstall().packageRoot;
648
- const watched = cliRoot ? [opts.watch, path.join(cliRoot, "package.json")] : [opts.watch];
649
633
  const hqRoot = d.env.HQ_ROOT?.trim();
650
634
  if (!hqRoot)
651
635
  throw new DaemonCommandError(`${opts.label} needs HQ_ROOT (the daemon sets it from the saved HQ folder)`);
@@ -663,7 +647,9 @@ async function runTmuxService(d, opts) {
663
647
  cwd: hqRoot,
664
648
  argv: opts.argv,
665
649
  label: opts.label,
666
- binaryFingerprint: () => installFingerprint(watched, binaryFingerprint),
650
+ // Only a new version of the tool restarts the session: not a same-version
651
+ // reinstall, and not an hq-cli release (the host's own code rarely matters).
652
+ binaryFingerprint: () => toolFingerprint(opts.watch),
667
653
  now: () => Date.now(),
668
654
  run: d.run,
669
655
  pollMs: 5_000,
@@ -683,22 +669,37 @@ async function runTmuxService(d, opts) {
683
669
  process.off("SIGINT", stop);
684
670
  }
685
671
  }
672
+ /** The claude or codex a remote-control host runs: --claude/--codex, else the one installed now. */
673
+ async function hostTool(d, name, given) {
674
+ const [{ serviceTool }, { findUserBinary }] = await Promise.all([
675
+ import("../lib/daemon/remote-control.js"),
676
+ import("../lib/daemon/run.js"),
677
+ ]);
678
+ try {
679
+ return serviceTool(name, given, (tool) => findUserBinary(tool, d.env, d.home));
680
+ }
681
+ catch (err) {
682
+ throw new DaemonCommandError(err.message);
683
+ }
684
+ }
686
685
  async function runServiceRemoteControl(d, opts) {
687
686
  const { TMUX_SESSION, remoteControlArgs } = await import("../lib/daemon/remote-control.js");
687
+ const claude = await hostTool(d, "claude", opts.claude);
688
688
  await runTmuxService(d, {
689
689
  session: TMUX_SESSION,
690
- argv: [opts.claude, ...remoteControlArgs({ name: opts.name, permissionMode: opts.permissionMode })],
690
+ argv: [claude, ...remoteControlArgs({ name: opts.name, permissionMode: opts.permissionMode })],
691
691
  label: "claude remote-control",
692
- watch: opts.claude,
692
+ watch: claude,
693
693
  });
694
694
  }
695
695
  async function runServiceCodexRemoteControl(d, opts) {
696
696
  const { CODEX_TMUX_SESSION } = await import("../lib/daemon/remote-control.js");
697
+ const codex = await hostTool(d, "codex", opts.codex);
697
698
  await runTmuxService(d, {
698
699
  session: CODEX_TMUX_SESSION,
699
700
  argv: ["bash", opts.script],
700
701
  label: "codex remote-control",
701
- watch: opts.codex,
702
+ watch: codex,
702
703
  });
703
704
  }
704
705
  export function registerDaemonCommand(program, over = {}) {
@@ -839,7 +840,7 @@ export function registerDaemonCommand(program, over = {}) {
839
840
  service
840
841
  .command("remote-control")
841
842
  .description("Keep `claude remote-control` running in a tmux session in the HQ folder")
842
- .requiredOption("--claude <path>", "claude binary")
843
+ .option("--claude <path>", "claude binary (default: the one installed for this user)")
843
844
  .requiredOption("--name <name>", "remote-control session name")
844
845
  .option("--permission-mode <mode>", "claude --permission-mode")
845
846
  .action(wrap((d, opts) => runServiceRemoteControl(d, opts)));
@@ -847,7 +848,7 @@ export function registerDaemonCommand(program, over = {}) {
847
848
  .command("codex-remote-control")
848
849
  .description("Keep the box's Codex remote-control runner going in a tmux session, restarting it after a Codex update")
849
850
  .requiredOption("--script <path>", "the box's Codex runner script")
850
- .requiredOption("--codex <path>", "codex binary, watched for updates")
851
+ .option("--codex <path>", "codex binary, watched for updates (default: the one installed for this user)")
851
852
  .action(wrap((d, opts) => runServiceCodexRemoteControl(d, opts)));
852
853
  return daemon;
853
854
  }
@@ -407,7 +407,12 @@ async function runConnectionAction(action, identifier) {
407
407
  const token = await ensureCognitoToken();
408
408
  // Resolve the pairKey from the caller's pending requests when possible so
409
409
  // accept/decline/block work against a friendly email or personUid.
410
- const requests = await fetchRequests(token).catch(() => []);
410
+ const requests = await fetchRequests(token).catch((err) => {
411
+ console.error("hq dm: pending connection request lookup failed", {
412
+ errorName: err instanceof Error ? err.name : typeof err,
413
+ });
414
+ return [];
415
+ });
411
416
  const matched = matchRequest(requests, identifier);
412
417
  const body = buildConnectionActionBody(identifier, matched);
413
418
  const res = await vaultApiFetch({