@indigoai-us/hq-cli 5.345.67 → 5.345.69

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/README.md +13 -0
  3. package/assets/scaffold/core/scripts/hq-dm-bind.sh +5 -0
  4. package/dist/command-catalog.generated.d.ts +8 -0
  5. package/dist/command-catalog.generated.js +10 -0
  6. package/dist/commands/core-checkpoint-stop-gate-native.js +13 -1
  7. package/dist/commands/core-delegate-w1b.js +2 -2
  8. package/dist/commands/core-delegate.js +2 -2
  9. package/dist/commands/core-dm-bind-native.d.ts +1 -1
  10. package/dist/commands/core-dm-bind-native.js +55 -52
  11. package/dist/commands/core-handoff-fast.js +13 -8
  12. package/dist/commands/core-handoff.js +10 -2
  13. package/dist/commands/core-native-family-flag.d.ts +7 -0
  14. package/dist/commands/core-native-family-flag.js +21 -0
  15. package/dist/commands/core-native-handoff-flag.js +2 -2
  16. package/dist/commands/core-native-hook-helpers-flag.js +2 -2
  17. package/dist/commands/core-native-jobs-flag.js +2 -2
  18. package/dist/commands/core-native-policy-flag.js +2 -2
  19. package/dist/commands/core-native-session-flag.js +2 -2
  20. package/dist/commands/core-native-utilities-flag.js +2 -2
  21. package/dist/commands/core-ontology-native.js +2 -2
  22. package/dist/commands/core-refresh-vault-access-native.js +14 -1
  23. package/dist/commands/pack-install.d.ts +5 -7
  24. package/dist/commands/pack-install.js +9 -5
  25. package/dist/commands/pkg-install.js +20 -1
  26. package/dist/fast-core.js +12 -2
  27. package/dist/fast-scaffold.d.ts +3 -0
  28. package/dist/fast-scaffold.js +14 -0
  29. package/dist/lib/flag-registry.d.ts +4 -1
  30. package/dist/lib/flag-registry.js +18 -6
  31. package/dist/lib/flag-snapshot-file.d.ts +2 -0
  32. package/dist/lib/flag-snapshot-file.js +30 -0
  33. package/dist/lib/pack-signing-keys.d.ts +22 -0
  34. package/dist/lib/pack-signing-keys.js +53 -0
  35. package/dist/lib/process-flag-company-context.d.ts +2 -0
  36. package/dist/lib/process-flag-company-context.js +15 -0
  37. package/dist/native-fast-route.d.ts +2 -2
  38. package/dist/native-fast-route.js +6 -8
  39. package/dist/utils/hq-spawn.d.ts +4 -0
  40. package/dist/utils/hq-spawn.js +11 -0
  41. package/dist/utils/resident-package-preload.js +4 -1
  42. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.69] — 2026-10-10
6
+ - Supported native core handlers now run by default without a flag snapshot. Explicit false values still turn them off; delegate and worker-skills remain default-off pending parity.
7
+
8
+ ## [5.345.68] — 2026-10-09
9
+
10
+ - Marketplace pack installs now require a signature from a pinned platform key. `--allow-unsigned` is limited to local directory sources; `HQ_PACK_PUBLIC_KEY` is development-only.
11
+
5
12
  ## [5.345.67] — 2026-10-09
6
13
 
7
14
  - Persist the Codex SessionStart model and effort for conduct lanes, with safe fallback defaults.
package/README.md CHANGED
@@ -7,6 +7,19 @@ npm install -g @indigoai-us/hq-cli
7
7
  hq --help
8
8
  ```
9
9
 
10
+ ## Marketplace package verification
11
+
12
+ Marketplace packages must include an Ed25519 signature made with a platform
13
+ key embedded in the CLI. The installer rejects unsigned listings, unknown key
14
+ ids, and a listing public key that does not match the pinned platform key before
15
+ extracting any archive. `HQ_PACK_PUBLIC_KEY` is a development-only override for
16
+ the current platform key id; it is honored only with `NODE_ENV=development` or
17
+ `NODE_ENV=test` and is ignored in production.
18
+
19
+ `hq install --allow-unsigned ./local-pack` is accepted only for local directory
20
+ sources. Marketplace installs always require a platform signature, and the flag
21
+ is rejected for them.
22
+
10
23
  The global HQ runtime installs hooks for supported agent runtimes. When the
11
24
  `hq-anywhere-runtime` flag is enabled, the hooks use hqd and a per-user folder
12
25
  registry. The flag defaults on when its registry row cannot be read, and an
@@ -53,6 +53,11 @@
53
53
  # Need from you: line
54
54
  set -euo pipefail
55
55
 
56
+ if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
57
+ sed -n '2,/^set -euo pipefail$/ { /^set -euo pipefail$/!p; }' "$0"
58
+ exit 0
59
+ fi
60
+
56
61
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
57
62
  SESSION_SH="$HQ_ROOT/core/scripts/hq-session.sh"
58
63
 
@@ -157,6 +157,10 @@ export declare const COMMAND_CATALOG: readonly [{
157
157
  readonly flags: "--allow-mcp";
158
158
  readonly description: "Skip the MCP server confirmation prompt (content-pack flow)";
159
159
  readonly hidden: false;
160
+ }, {
161
+ readonly flags: "--allow-unsigned";
162
+ readonly description: "Acknowledge an unsigned local directory source; marketplace installs always require a signature";
163
+ readonly hidden: false;
160
164
  }, {
161
165
  readonly flags: "--branch";
162
166
  readonly description: "Follow a ref instead of SHA-pinning (git content-pack flow)";
@@ -543,6 +547,10 @@ export declare const COMMAND_CATALOG: readonly [{
543
547
  readonly flags: "--allow-mcp";
544
548
  readonly description: "Skip the MCP server confirmation prompt (content-pack flow)";
545
549
  readonly hidden: false;
550
+ }, {
551
+ readonly flags: "--allow-unsigned";
552
+ readonly description: "Acknowledge an unsigned local directory source; marketplace installs always require a signature";
553
+ readonly hidden: false;
546
554
  }, {
547
555
  readonly flags: "--branch";
548
556
  readonly description: "Follow a ref instead of SHA-pinning (git content-pack flow)";
@@ -157,6 +157,11 @@ export const COMMAND_CATALOG = [
157
157
  "description": "Skip the MCP server confirmation prompt (content-pack flow)",
158
158
  "hidden": false
159
159
  },
160
+ {
161
+ "flags": "--allow-unsigned",
162
+ "description": "Acknowledge an unsigned local directory source; marketplace installs always require a signature",
163
+ "hidden": false
164
+ },
160
165
  {
161
166
  "flags": "--branch",
162
167
  "description": "Follow a ref instead of SHA-pinning (git content-pack flow)",
@@ -641,6 +646,11 @@ export const COMMAND_CATALOG = [
641
646
  "description": "Skip the MCP server confirmation prompt (content-pack flow)",
642
647
  "hidden": false
643
648
  },
649
+ {
650
+ "flags": "--allow-unsigned",
651
+ "description": "Acknowledge an unsigned local directory source; marketplace installs always require a signature",
652
+ "hidden": false
653
+ },
644
654
  {
645
655
  "flags": "--branch",
646
656
  "description": "Follow a ref instead of SHA-pinning (git content-pack flow)",
@@ -1,5 +1,6 @@
1
1
  /** Native fast-route implementation of checkpoint-stop-gate.sh. */
2
2
  import * as childProcess from "node:child_process";
3
+ import { planHqSpawn } from "../utils/hq-spawn.js";
3
4
  import * as fs from "node:fs";
4
5
  import * as os from "node:os";
5
6
  import * as path from "node:path";
@@ -85,7 +86,18 @@ const defaults = () => ({
85
86
  return false;
86
87
  },
87
88
  spawnHqDetached: (args, env) => {
88
- const child = childProcess.spawn("hq", args, { detached: true, stdio: "ignore", env: env ?? process.env });
89
+ const childEnv = env ?? process.env;
90
+ const spawnPlan = planHqSpawn(args, childEnv, process.cwd());
91
+ const child = childProcess.spawn(spawnPlan.cmd, spawnPlan.args, {
92
+ detached: true,
93
+ stdio: "ignore",
94
+ env: childEnv,
95
+ shell: spawnPlan.shell,
96
+ ...(spawnPlan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
97
+ });
98
+ child.once("error", (error) => {
99
+ process.stderr.write(`${error.message}\n`);
100
+ });
89
101
  child.unref();
90
102
  },
91
103
  });
@@ -1215,8 +1215,8 @@ export function registerCoreDelegateW1b(delegate, core) {
1215
1215
  const options = core.opts();
1216
1216
  const root = resolveLiveRoot({ hqRoot: options.hqRoot });
1217
1217
  const context = { cwd: root, root };
1218
- const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
1219
- if (!resolveProcessFlagGate(FLAG, { fallback: false }, () => false)) {
1218
+ const { coreNativeDelegateEnabled } = await import("./core-native-family-flag.js");
1219
+ if (!await coreNativeDelegateEnabled()) {
1220
1220
  const result = runBundledScript({ asset: ASSETS[name], args, cwd: root, hqRoot: root });
1221
1221
  if (result.code !== 0)
1222
1222
  process.exit(result.code);
@@ -1022,8 +1022,8 @@ export function registerCoreDelegateCommand(core, registerWave1b) {
1022
1022
  const context = rootMode === "cwd"
1023
1023
  ? { cwd: process.cwd() }
1024
1024
  : (() => { const root = resolveLiveRoot({ hqRoot: scope.hqRoot }); return { cwd: root, root }; })();
1025
- const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
1026
- if (!resolveProcessFlagGate(CORE_NATIVE_DELEGATE_FLAG, { fallback: false }, () => false)) {
1025
+ const { coreNativeDelegateEnabled } = await import("./core-native-family-flag.js");
1026
+ if (!await coreNativeDelegateEnabled()) {
1027
1027
  const result = runBundledScript({ asset: SCRIPT_ASSETS[name], args, cwd: context.cwd, hqRoot: context.root });
1028
1028
  if (result.code !== 0)
1029
1029
  process.exit(result.code);
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import type { Command } from "commander";
4
4
  import { type UtilityOutput } from "./core-native-utilities.js";
5
5
  export declare const DM_BIND_ASSET = "core/scripts/hq-dm-bind.sh";
6
- export declare const DM_BIND_HELP = "# hq-dm-bind.sh \u2014 bind the current session to one HQ DM channel, post\n# structured updates to it, and listen for replies (feedback / requests).\n#\n# Usage:\n# core/scripts/hq-dm-bind.sh bind <channel> # bind + set the read cursor to \"now\"\n# core/scripts/hq-dm-bind.sh status # show the binding and cursor\n# core/scripts/hq-dm-bind.sh post --title <t> [--state <s>] [--line <l>]... [--next <l>]... [--ask <l>]... [--to <name>]... [--topic <t>] [--under <id>] [--new-thread]\n# core/scripts/hq-dm-bind.sh post --title <t> - < body.md # body from stdin (already structured)\n# core/scripts/hq-dm-bind.sh threads # list the channel's recent top-level threads (id, replies, author, text)\n# core/scripts/hq-dm-bind.sh roster # print the channel members a post will @-mention\n# core/scripts/hq-dm-bind.sh poll # print new messages since the cursor, advance it\n# core/scripts/hq-dm-bind.sh listen [--interval 60] [--timeout 1800]\n# # block until a new message arrives (or timeout)\n# core/scripts/hq-dm-bind.sh unbind\n#\n# The binding lives in the session's meta.yaml (`dm_channel`) via hq-session.sh;\n# the cursor is `workspace/sessions/<sid>/dm-bind.cursor` (the last seen sort\n# key from `hq dm channel --json`). `listen` is meant to run detached\n# (Bash run_in_background): it exits 0 and prints the new messages the moment\n# one lands, exits 3 on timeout with nothing new.\n#\n# Every post opens with an @-mention of every other member of the channel, so\n# the update reaches people (HQ mentions are structured: the `hq` CLI resolves\n# the `@\"Display Name\"` tokens against the roster and sends the real mention\n";
6
+ export declare const DM_BIND_HELP = "# hq-dm-bind.sh \u2014 bind the current session to one HQ DM channel, post\n# structured updates to it, and listen for replies (feedback / requests).\n#\n# Usage:\n# core/scripts/hq-dm-bind.sh bind <channel> # bind + set the read cursor to \"now\"\n# core/scripts/hq-dm-bind.sh status # show the binding and cursor\n# core/scripts/hq-dm-bind.sh post --title <t> [--state <s>] [--line <l>]... [--next <l>]... [--ask <l>]... [--to <name>]... [--topic <t>] [--under <id>] [--new-thread]\n# core/scripts/hq-dm-bind.sh post --title <t> - < body.md # body from stdin (already structured)\n# core/scripts/hq-dm-bind.sh threads # list the channel's recent top-level threads (id, replies, author, text)\n# core/scripts/hq-dm-bind.sh roster # print the channel members a post will @-mention\n# core/scripts/hq-dm-bind.sh poll # print new messages since the cursor, advance it\n# core/scripts/hq-dm-bind.sh listen [--interval 60] [--timeout 1800]\n# # block until a new message arrives (or timeout)\n# core/scripts/hq-dm-bind.sh unbind\n#\n# The binding lives in the session's meta.yaml (`dm_channel`) via hq-session.sh;\n# the cursor is `workspace/sessions/<sid>/dm-bind.cursor` (the last seen sort\n# key from `hq dm channel --json`). `listen` is meant to run detached\n# (Bash run_in_background): it exits 0 and prints the new messages the moment\n# one lands, exits 3 on timeout with nothing new.\n#\n# Every post opens with an @-mention of every other member of the channel, so\n# the update reaches people (HQ mentions are structured: the `hq` CLI resolves\n# the `@\"Display Name\"` tokens against the roster and sends the real mention\n# list; an unresolvable name fails the post rather than posting text that looks\n# addressed but is not). The CLI rewrites a resolved `@\"Display Name\"` token to\n# `@Display Name`, so the room never shows the quotes.\n#\n# Posts thread by topic. The first post on a topic (`--topic`, else the title)\n# is a top-level root; every later post on the same topic in this session goes\n# out as a reply under it. `--under <id>` replies under an existing thread that\n# someone else (or an earlier session) started \u2014 a top-level post is only for a\n# subject the channel has no thread for yet. `--new-thread` starts a fresh root. The topic \u2192 root\n# map is `workspace/sessions/<sid>/dm-bind.threads`.\n#\n# A post ALWAYS mentions someone. `--to <name>` (repeatable) narrows the line to\n# the named members, for a reply to one person; without it the line names every\n# other human member (bots are left out unless named with --to, because a tagged\n# bot wakes and replies). There is no way to post unmentioned: an update nobody is\n# notified about does not get read. `post` refuses when the roster cannot be\n# read rather than sending a silent update.\n#\n# Post format (kept deliberately plain \u2014 see .claude/skills/dm-bind/SKILL.md):\n# @\"Ada Lovelace\" @\"Grace Hopper\"\n#\n# <title> \u2014 <state>\n#\n# \u2022 line\n# \u2022 line\n#\n# Next: line\n# Need from you: line\n";
7
7
  export type DmBindDeps = {
8
8
  root: string;
9
9
  cwd: string;
@@ -3,54 +3,50 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
+ import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
6
7
  import { resolveLiveRoot } from "../utils/hq-roots.js";
8
+ import { planHqSpawn } from "../utils/hq-spawn.js";
7
9
  import { bundledShellUnboundValue, registerNativeUtility, utilityError } from "./core-native-utilities.js";
8
10
  export const DM_BIND_ASSET = "core/scripts/hq-dm-bind.sh";
9
- export const DM_BIND_HELP = `# hq-dm-bind.sh — bind the current session to one HQ DM channel, post
10
- # structured updates to it, and listen for replies (feedback / requests).
11
- #
12
- # Usage:
13
- # core/scripts/hq-dm-bind.sh bind <channel> # bind + set the read cursor to "now"
14
- # core/scripts/hq-dm-bind.sh status # show the binding and cursor
15
- # core/scripts/hq-dm-bind.sh post --title <t> [--state <s>] [--line <l>]... [--next <l>]... [--ask <l>]... [--to <name>]... [--topic <t>] [--under <id>] [--new-thread]
16
- # core/scripts/hq-dm-bind.sh post --title <t> - < body.md # body from stdin (already structured)
17
- # core/scripts/hq-dm-bind.sh threads # list the channel's recent top-level threads (id, replies, author, text)
18
- # core/scripts/hq-dm-bind.sh roster # print the channel members a post will @-mention
19
- # core/scripts/hq-dm-bind.sh poll # print new messages since the cursor, advance it
20
- # core/scripts/hq-dm-bind.sh listen [--interval 60] [--timeout 1800]
21
- # # block until a new message arrives (or timeout)
22
- # core/scripts/hq-dm-bind.sh unbind
23
- #
24
- # The binding lives in the session's meta.yaml (\`dm_channel\`) via hq-session.sh;
25
- # the cursor is \`workspace/sessions/<sid>/dm-bind.cursor\` (the last seen sort
26
- # key from \`hq dm channel --json\`). \`listen\` is meant to run detached
27
- # (Bash run_in_background): it exits 0 and prints the new messages the moment
28
- # one lands, exits 3 on timeout with nothing new.
29
- #
30
- # Every post opens with an @-mention of every other member of the channel, so
31
- # the update reaches people (HQ mentions are structured: the \`hq\` CLI resolves
32
- # the \`@"Display Name"\` tokens against the roster and sends the real mention
33
- `;
34
- export const createDmBindDeps = (root, cwd, env, now, home = env.HOME || os.homedir()) => ({
35
- root, cwd, env, home, now, fs,
36
- runHq: (args, childEnv) => {
37
- const result = spawnSync("hq", args, { cwd, env: { ...childEnv, HQ_ROOT: root }, encoding: "utf8" });
38
- return { code: result.status ?? 127, stdout: result.stdout ?? "", stderr: result.stderr ?? (result.error ? `${result.error.message}\n` : "") };
39
- },
40
- runHqSession: (args, childEnv) => {
41
- const result = spawnSync("bash", [path.join(root, "core/scripts/hq-session.sh"), ...args], { cwd, env: childEnv, encoding: "utf8" });
42
- return { code: result.status ?? 127, stdout: result.stdout ?? "", stderr: result.stderr ?? (result.error ? `${result.error.message}\n` : "") };
43
- },
44
- fetch: globalThis.fetch,
45
- readStdin: () => new Promise((resolve, reject) => {
46
- let data = "";
47
- process.stdin.setEncoding("utf8");
48
- process.stdin.on("data", (chunk) => { data += chunk; });
49
- process.stdin.on("end", () => resolve(data));
50
- process.stdin.on("error", reject);
51
- }),
52
- sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
53
- });
11
+ export const DM_BIND_HELP = "# hq-dm-bind.sh \u2014 bind the current session to one HQ DM channel, post\n# structured updates to it, and listen for replies (feedback / requests).\n#\n# Usage:\n# core/scripts/hq-dm-bind.sh bind <channel> # bind + set the read cursor to \"now\"\n# core/scripts/hq-dm-bind.sh status # show the binding and cursor\n# core/scripts/hq-dm-bind.sh post --title <t> [--state <s>] [--line <l>]... [--next <l>]... [--ask <l>]... [--to <name>]... [--topic <t>] [--under <id>] [--new-thread]\n# core/scripts/hq-dm-bind.sh post --title <t> - < body.md # body from stdin (already structured)\n# core/scripts/hq-dm-bind.sh threads # list the channel's recent top-level threads (id, replies, author, text)\n# core/scripts/hq-dm-bind.sh roster # print the channel members a post will @-mention\n# core/scripts/hq-dm-bind.sh poll # print new messages since the cursor, advance it\n# core/scripts/hq-dm-bind.sh listen [--interval 60] [--timeout 1800]\n# # block until a new message arrives (or timeout)\n# core/scripts/hq-dm-bind.sh unbind\n#\n# The binding lives in the session's meta.yaml (`dm_channel`) via hq-session.sh;\n# the cursor is `workspace/sessions/<sid>/dm-bind.cursor` (the last seen sort\n# key from `hq dm channel --json`). `listen` is meant to run detached\n# (Bash run_in_background): it exits 0 and prints the new messages the moment\n# one lands, exits 3 on timeout with nothing new.\n#\n# Every post opens with an @-mention of every other member of the channel, so\n# the update reaches people (HQ mentions are structured: the `hq` CLI resolves\n# the `@\"Display Name\"` tokens against the roster and sends the real mention\n# list; an unresolvable name fails the post rather than posting text that looks\n# addressed but is not). The CLI rewrites a resolved `@\"Display Name\"` token to\n# `@Display Name`, so the room never shows the quotes.\n#\n# Posts thread by topic. The first post on a topic (`--topic`, else the title)\n# is a top-level root; every later post on the same topic in this session goes\n# out as a reply under it. `--under <id>` replies under an existing thread that\n# someone else (or an earlier session) started \u2014 a top-level post is only for a\n# subject the channel has no thread for yet. `--new-thread` starts a fresh root. The topic \u2192 root\n# map is `workspace/sessions/<sid>/dm-bind.threads`.\n#\n# A post ALWAYS mentions someone. `--to <name>` (repeatable) narrows the line to\n# the named members, for a reply to one person; without it the line names every\n# other human member (bots are left out unless named with --to, because a tagged\n# bot wakes and replies). There is no way to post unmentioned: an update nobody is\n# notified about does not get read. `post` refuses when the roster cannot be\n# read rather than sending a silent update.\n#\n# Post format (kept deliberately plain \u2014 see .claude/skills/dm-bind/SKILL.md):\n# @\"Ada Lovelace\" @\"Grace Hopper\"\n#\n# <title> \u2014 <state>\n#\n# \u2022 line\n# \u2022 line\n#\n# Next: line\n# Need from you: line\n";
12
+ export const createDmBindDeps = (root, cwd, env, now, home = env.HOME || os.homedir()) => {
13
+ const d = {
14
+ root, cwd, env, home, now, fs,
15
+ runHq: (args, childEnv) => {
16
+ const envWithRoot = { ...childEnv, HQ_ROOT: root };
17
+ const spawnPlan = planHqSpawn(args, envWithRoot, cwd, d.platform ?? process.platform, (candidate) => {
18
+ try {
19
+ return d.fs.statSync(candidate).isFile();
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ });
25
+ const result = spawnSync(spawnPlan.cmd, spawnPlan.args, {
26
+ cwd,
27
+ env: envWithRoot,
28
+ encoding: "utf8",
29
+ shell: spawnPlan.shell,
30
+ ...(spawnPlan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
31
+ });
32
+ return { code: result.status ?? 127, stdout: result.stdout ?? "", stderr: result.stderr ?? (result.error ? `${result.error.message}\n` : "") };
33
+ },
34
+ runHqSession: (args, childEnv) => {
35
+ const result = spawnSync("bash", [path.join(root, "core/scripts/hq-session.sh"), ...args], { cwd, env: childEnv, encoding: "utf8" });
36
+ return { code: result.status ?? 127, stdout: result.stdout ?? "", stderr: result.stderr ?? (result.error ? `${result.error.message}\n` : "") };
37
+ },
38
+ fetch: globalThis.fetch,
39
+ readStdin: () => new Promise((resolve, reject) => {
40
+ let data = "";
41
+ process.stdin.setEncoding("utf8");
42
+ process.stdin.on("data", (chunk) => { data += chunk; });
43
+ process.stdin.on("end", () => resolve(data));
44
+ process.stdin.on("error", reject);
45
+ }),
46
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
47
+ };
48
+ return d;
49
+ };
54
50
  const DM_BIND_API_TIMEOUT_MS = 15_000;
55
51
  const fail = (message, code = 2) => utilityError(`hq-dm-bind: ${message}`, code);
56
52
  const sessionDirectory = (d, sid) => path.join(d.root, "workspace/sessions", sid);
@@ -89,19 +85,24 @@ function ensureDir(d, sid) {
89
85
  return directory;
90
86
  }
91
87
  function hasHqOnPath(d) {
88
+ if ((d.platform ?? process.platform) === "win32") {
89
+ return resolveWindowsExecutable("hq", d.env, (candidate) => {
90
+ try {
91
+ return d.fs.statSync(candidate).isFile();
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }, d.cwd) !== null;
97
+ }
92
98
  for (const directory of (d.env.PATH ?? "").split(path.delimiter)) {
93
99
  if (!directory)
94
100
  continue;
95
- // runHq spawns "hq" without a shell. On Windows that resolves only .exe and
96
- // .com files, and Windows file modes carry no execute bits, so accept exactly
97
- // what the spawn can launch. npm's hq.cmd shim needs a shell and is not
98
- // accepted here.
99
- const windows = (d.platform ?? process.platform) === "win32";
100
- const names = windows ? ["hq.exe", "hq.com"] : ["hq"];
101
+ const names = ["hq"];
101
102
  for (const name of names) {
102
103
  try {
103
104
  const stat = d.fs.statSync(path.join(directory, name));
104
- if (stat.isFile() && (windows || (stat.mode & 0o111) !== 0))
105
+ if (stat.isFile() && (stat.mode & 0o111) !== 0)
105
106
  return true;
106
107
  }
107
108
  catch { /* PATH entry is absent */ }
@@ -646,6 +647,8 @@ async function listen(d, args) {
646
647
  return { code: 3, stdout: output, stderr: "" };
647
648
  }
648
649
  export async function runDmBind(args, deps) {
650
+ if (args[0] === "-h" || args[0] === "--help")
651
+ return { code: 0, stdout: DM_BIND_HELP, stderr: "" };
649
652
  if (!hasHqOnPath(deps))
650
653
  return fail("missing dependency: hq");
651
654
  const [command = "", ...rest] = args;
@@ -2,6 +2,7 @@
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
+ import { planHqSpawn } from "../utils/hq-spawn.js";
5
6
  /**
6
7
  * `handoff finalize --help` is historically an unknown-argument error. Keep
7
8
  * that fast error path out of the large handoff implementation module.
@@ -48,20 +49,24 @@ export function handoffFinalizeHelpResult(args, root, tmpRoot = "/tmp") {
48
49
  return { code: 2, stdout: "", stderr: "Unknown arg: --help\n" };
49
50
  }
50
51
  async function launchHandoffPost(args, root) {
52
+ const childEnv = {
53
+ ...process.env,
54
+ HQ_ROOT: root,
55
+ CLAUDE_PROJECT_DIR: root,
56
+ PWD: root,
57
+ HQ_NATIVE_HANDOFF_POST_CHILD: "1",
58
+ };
59
+ const spawnPlan = planHqSpawn(["core", "--hq-root", root, "handoff", "post", ...args], childEnv, root);
51
60
  return await new Promise((resolve) => {
52
61
  try {
53
- const child = spawn("hq", ["core", "--hq-root", root, "handoff", "post", ...args], {
62
+ const child = spawn(spawnPlan.cmd, spawnPlan.args, {
54
63
  cwd: root,
55
- env: {
56
- ...process.env,
57
- HQ_ROOT: root,
58
- CLAUDE_PROJECT_DIR: root,
59
- PWD: root,
60
- HQ_NATIVE_HANDOFF_POST_CHILD: "1",
61
- },
64
+ env: childEnv,
62
65
  detached: true,
63
66
  windowsHide: true,
64
67
  stdio: "ignore",
68
+ shell: spawnPlan.shell,
69
+ ...(spawnPlan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
65
70
  });
66
71
  child.once("error", (error) => {
67
72
  process.stderr.write(`${error.message}\n`);
@@ -1,5 +1,6 @@
1
1
  /** Native implementations of the handoff scaffold command family. */
2
2
  import { spawn, spawnSync } from "node:child_process";
3
+ import { planHqSpawn } from "../utils/hq-spawn.js";
3
4
  import * as fs from "node:fs";
4
5
  import * as os from "node:os";
5
6
  import * as path from "node:path";
@@ -876,14 +877,17 @@ async function launchHandoffPost(args, root) {
876
877
  delete childEnv[key];
877
878
  childEnv.PWD = shellDisplayPath(root);
878
879
  }
880
+ const spawnPlan = planHqSpawn(["core", "--hq-root", root, "handoff", "post", ...args], childEnv, root);
879
881
  return await new Promise((resolve) => {
880
882
  try {
881
- const child = spawn("hq", ["core", "--hq-root", root, "handoff", "post", ...args], {
883
+ const child = spawn(spawnPlan.cmd, spawnPlan.args, {
882
884
  cwd: root,
883
885
  env: childEnv,
884
886
  detached: true,
885
887
  windowsHide: true,
886
888
  stdio: "ignore",
889
+ shell: spawnPlan.shell,
890
+ ...(spawnPlan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
887
891
  });
888
892
  child.once("error", (error) => resolve({ code: 127, stdout: "", stderr: `${error.message}\n` }));
889
893
  child.once("spawn", () => {
@@ -1036,11 +1040,15 @@ export function runHandoffPostWorker(args, context) {
1036
1040
  log(`workspace-sync: skipped (thread mirror missing for ${company})`);
1037
1041
  continue;
1038
1042
  }
1039
- const pushed = spawnSync("hq", ["sync", "push", "--company", company, `companies/${company}/workspace`], {
1043
+ const syncArgs = ["sync", "push", "--company", company, `companies/${company}/workspace`];
1044
+ const spawnPlan = planHqSpawn(syncArgs, process.env, root);
1045
+ const pushed = spawnSync(spawnPlan.cmd, spawnPlan.args, {
1040
1046
  cwd: root,
1041
1047
  env: process.env,
1042
1048
  encoding: "utf8",
1043
1049
  stdio: ["ignore", "pipe", "pipe"],
1050
+ shell: spawnPlan.shell,
1051
+ ...(spawnPlan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
1044
1052
  });
1045
1053
  recordCommand(logMain, {
1046
1054
  code: pushed.status ?? 1,
@@ -0,0 +1,7 @@
1
+ /** Shared fail-safe readers for shipped native core families. */
2
+ export declare function coreNativeFamilyEnabled(flagKey: string): Promise<boolean>;
3
+ export declare const CORE_NATIVE_DELEGATE_FLAG = "core-native-delegate";
4
+ export declare const CORE_NATIVE_ONTOLOGY_FLAG = "core-native-ontology";
5
+ export declare function coreNativeDelegateEnabled(): Promise<boolean>;
6
+ export declare function coreNativeOntologyEnabled(): Promise<boolean>;
7
+ //# sourceMappingURL=core-native-family-flag.d.ts.map
@@ -0,0 +1,21 @@
1
+ /** Shared fail-safe readers for shipped native core families. */
2
+ import { readLocalOverride } from "@indigoai-us/hq-flags-client";
3
+ export async function coreNativeFamilyEnabled(flagKey) {
4
+ const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
5
+ const override = readLocalOverride(flagKey, process.env);
6
+ if (override !== null)
7
+ return override;
8
+ return resolveProcessFlagGate(flagKey, { fallback: true }, () => true, undefined, {
9
+ acceptStaleSnapshot: true,
10
+ killSwitchWithoutCompany: true,
11
+ });
12
+ }
13
+ export const CORE_NATIVE_DELEGATE_FLAG = "core-native-delegate";
14
+ export const CORE_NATIVE_ONTOLOGY_FLAG = "core-native-ontology";
15
+ export function coreNativeDelegateEnabled() {
16
+ return import("../lib/flag-registry.js").then(({ resolveProcessFlagGate }) => resolveProcessFlagGate(CORE_NATIVE_DELEGATE_FLAG, { fallback: false }, () => false));
17
+ }
18
+ export function coreNativeOntologyEnabled() {
19
+ return coreNativeFamilyEnabled(CORE_NATIVE_ONTOLOGY_FLAG);
20
+ }
21
+ //# sourceMappingURL=core-native-family-flag.js.map
@@ -1,7 +1,7 @@
1
1
  /** Default-off rollout gate for the native Wave 2 worktree/resume helpers. */
2
2
  export const CORE_NATIVE_HANDOFF_FLAG = "core-native-handoff";
3
3
  export async function coreNativeHandoffEnabled() {
4
- const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
5
- return resolveProcessFlagGate(CORE_NATIVE_HANDOFF_FLAG, { fallback: false }, () => false);
4
+ const { coreNativeFamilyEnabled } = await import("./core-native-family-flag.js");
5
+ return coreNativeFamilyEnabled(CORE_NATIVE_HANDOFF_FLAG);
6
6
  }
7
7
  //# sourceMappingURL=core-native-handoff-flag.js.map
@@ -1,7 +1,7 @@
1
1
  /** Default-off rollout gate for Wave 8 hook-time helper commands. */
2
2
  export const CORE_NATIVE_HOOK_HELPERS_FLAG = "core-native-hook-helpers";
3
3
  export async function coreNativeHookHelpersEnabled() {
4
- const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
5
- return resolveProcessFlagGate(CORE_NATIVE_HOOK_HELPERS_FLAG, { fallback: false }, () => false);
4
+ const { coreNativeFamilyEnabled } = await import("./core-native-family-flag.js");
5
+ return coreNativeFamilyEnabled(CORE_NATIVE_HOOK_HELPERS_FLAG);
6
6
  }
7
7
  //# sourceMappingURL=core-native-hook-helpers-flag.js.map
@@ -1,7 +1,7 @@
1
1
  /** Default-off rollout gate for the native scheduled-jobs helpers. */
2
2
  export const CORE_NATIVE_JOBS_FLAG = "core-native-jobs";
3
3
  export async function coreNativeJobsEnabled() {
4
- const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
5
- return resolveProcessFlagGate(CORE_NATIVE_JOBS_FLAG, { fallback: false }, () => false);
4
+ const { coreNativeFamilyEnabled } = await import("./core-native-family-flag.js");
5
+ return coreNativeFamilyEnabled(CORE_NATIVE_JOBS_FLAG);
6
6
  }
7
7
  //# sourceMappingURL=core-native-jobs-flag.js.map
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export const CORE_NATIVE_POLICY_FLAG = 'core-native-policy';
8
8
  export async function coreNativePolicyEnabled() {
9
- const { resolveProcessFlagGate } = await import('../lib/flag-registry.js');
10
- return resolveProcessFlagGate(CORE_NATIVE_POLICY_FLAG, { fallback: false }, () => false);
9
+ const { coreNativeFamilyEnabled } = await import('./core-native-family-flag.js');
10
+ return coreNativeFamilyEnabled(CORE_NATIVE_POLICY_FLAG);
11
11
  }
12
12
  //# sourceMappingURL=core-native-policy-flag.js.map
@@ -1,7 +1,7 @@
1
1
  /** Default-off rollout gate for native session and Stop-hook plumbing. */
2
2
  export const CORE_NATIVE_SESSION_FLAG = "core-native-session";
3
3
  export async function coreNativeSessionEnabled() {
4
- const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
5
- return resolveProcessFlagGate(CORE_NATIVE_SESSION_FLAG, { fallback: false }, () => false);
4
+ const { coreNativeFamilyEnabled } = await import("./core-native-family-flag.js");
5
+ return coreNativeFamilyEnabled(CORE_NATIVE_SESSION_FLAG);
6
6
  }
7
7
  //# sourceMappingURL=core-native-session-flag.js.map
@@ -1,7 +1,7 @@
1
1
  /** Default-off rollout gate for native operator utility commands. */
2
2
  export const CORE_NATIVE_UTILITIES_FLAG = "core-native-utilities";
3
3
  export async function coreNativeUtilitiesEnabled() {
4
- const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
5
- return resolveProcessFlagGate(CORE_NATIVE_UTILITIES_FLAG, { fallback: false }, () => false);
4
+ const { coreNativeFamilyEnabled } = await import("./core-native-family-flag.js");
5
+ return coreNativeFamilyEnabled(CORE_NATIVE_UTILITIES_FLAG);
6
6
  }
7
7
  //# sourceMappingURL=core-native-utilities-flag.js.map
@@ -1170,8 +1170,8 @@ export function registerCoreOntologyNativeCommands(ontology, rootFor) {
1170
1170
  const forwarded = commandIndex >= 0 ? argv.slice(commandIndex + 1)
1171
1171
  : commandContext.args.length > 0 ? commandContext.args : args;
1172
1172
  const liveRoot = entry.root === "live" ? rootFor() : process.cwd();
1173
- const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
1174
- if (!resolveProcessFlagGate(FLAG, { fallback: false }, () => false)) {
1173
+ const { coreNativeOntologyEnabled } = await import("./core-native-family-flag.js");
1174
+ if (!await coreNativeOntologyEnabled()) {
1175
1175
  const result = runBundledScript({
1176
1176
  asset: `${ASSET}${entry.asset}`,
1177
1177
  args: forwarded,
@@ -2,8 +2,10 @@
2
2
  import { spawnSync } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
+ import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
5
6
  import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
6
7
  import { registerNativeUtility } from "./core-native-utilities.js";
8
+ import { planHqSpawn } from "../utils/hq-spawn.js";
7
9
  export const REFRESH_VAULT_ACCESS_ASSET = "core/scripts/refresh-vault-access.sh";
8
10
  export const REFRESH_VAULT_ACCESS_HELP = `# hq-core: public
9
11
  # refresh-vault-access.sh — build/refresh the local vault-access manifest that
@@ -39,7 +41,15 @@ export const REFRESH_VAULT_ACCESS_HELP = `# hq-core: public
39
41
  #
40
42
  `.replace(/^# ?/gm, "");
41
43
  function defaultHq(args, cwd, env) {
42
- const result = spawnSync("hq", args, { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
44
+ const spawnPlan = planHqSpawn(args, env, cwd);
45
+ const result = spawnSync(spawnPlan.cmd, spawnPlan.args, {
46
+ cwd,
47
+ env,
48
+ encoding: "utf8",
49
+ stdio: ["ignore", "pipe", "pipe"],
50
+ shell: spawnPlan.shell,
51
+ ...(spawnPlan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
52
+ });
43
53
  if (result.error)
44
54
  return { code: 127, stdout: "", stderr: result.error.message, missing: result.error.code === "ENOENT" };
45
55
  return { code: result.status ?? 128, stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? "") };
@@ -47,6 +57,9 @@ function defaultHq(args, cwd, env) {
47
57
  function executableOnPath(name, value) {
48
58
  if (!value)
49
59
  return false;
60
+ if (process.platform === "win32" && name === "hq") {
61
+ return resolveWindowsExecutable(name, { PATH: value }) !== null;
62
+ }
50
63
  for (const directory of value.split(path.delimiter)) {
51
64
  if (!directory)
52
65
  continue;
@@ -178,11 +178,10 @@ export declare function defaultMarketplaceDeps(): MarketplaceDeps;
178
178
  * resolves the tarball from the marketplace and ALWAYS verifies it against the
179
179
  * listing's approved hash/signature BEFORE extraction (US-021 → US-020).
180
180
  *
181
- * @param requireSignature production posture once signing keys exist; during
182
- * the key-deferral window callers pass false (hash still always checked).
181
+ * Marketplace listings are always signature-verified. Legacy unsigned
182
+ * listings deliberately fail before any bytes are extracted.
183
183
  */
184
184
  export declare function fetchMarketplace(source: string, tmpDir: string, deps: MarketplaceDeps, opts?: {
185
- requireSignature?: boolean;
186
185
  company?: string;
187
186
  }): Promise<FetchResult>;
188
187
  export interface LatestResult {
@@ -446,11 +445,10 @@ export interface InstallPackOptions {
446
445
  */
447
446
  marketplaceDeps?: MarketplaceDeps;
448
447
  /**
449
- * US-006: require a valid signature for marketplace artifacts. Default false
450
- * during the signing-key-deferral window (the content hash is still ALWAYS
451
- * verified); set true once platform signing keys are provisioned.
448
+ * Acknowledge that a local directory source is unsigned. This has no effect
449
+ * for marketplace sources, which always require a platform signature.
452
450
  */
453
- requireSignature?: boolean;
451
+ allowUnsigned?: boolean;
454
452
  }
455
453
  export declare function runPackInstallWithLease<T>(run: () => Promise<T>, leaseOptions?: ResidentCommandLeaseOptions): Promise<T>;
456
454
  export declare function installPack(source: string, opts?: InstallPackOptions): Promise<void>;
@@ -39,6 +39,7 @@ import * as path from 'path';
39
39
  import * as readline from 'readline';
40
40
  import { parseUserYaml } from '../utils/user-yaml-error.js';
41
41
  import { createHash, createPublicKey, verify as cryptoVerify, } from 'node:crypto';
42
+ import { trustedMarketplaceSigningKey } from '../lib/pack-signing-keys.js';
42
43
  import { execFileSync, spawnSync } from 'child_process';
43
44
  import chalk from 'chalk';
44
45
  import semverSatisfies from 'semver/functions/satisfies.js';
@@ -414,8 +415,8 @@ export function defaultMarketplaceDeps() {
414
415
  * resolves the tarball from the marketplace and ALWAYS verifies it against the
415
416
  * listing's approved hash/signature BEFORE extraction (US-021 → US-020).
416
417
  *
417
- * @param requireSignature production posture once signing keys exist; during
418
- * the key-deferral window callers pass false (hash still always checked).
418
+ * Marketplace listings are always signature-verified. Legacy unsigned
419
+ * listings deliberately fail before any bytes are extracted.
419
420
  */
420
421
  export async function fetchMarketplace(source, tmpDir, deps, opts = {}) {
421
422
  const { slug, version } = parseMarketplaceSource(source);
@@ -448,12 +449,13 @@ export async function fetchMarketplace(source, tmpDir, deps, opts = {}) {
448
449
  // downloaded bytes BEFORE extraction. A hash/signature mismatch throws
449
450
  // `ArtifactVerificationError` and nothing is unpacked or wired. The hash is
450
451
  // ALWAYS verified when present; signature is enforced when requireSignature.
452
+ const publicKey = trustedMarketplaceSigningKey(listing.signingKeyId, listing.publicKey);
451
453
  verifyArtifact({
452
454
  tarballBytes,
453
455
  expectedHash: listing.contentHash,
454
456
  signature: listing.signature,
455
- publicKey: listing.publicKey,
456
- requireSignature: opts.requireSignature ?? false,
457
+ publicKey,
458
+ requireSignature: true,
457
459
  });
458
460
  // Write to a tmp tarball and run it through the SAME hardened extractor
459
461
  // (US-020) as every other tarball transport.
@@ -1697,6 +1699,9 @@ async function installPackWithoutLease(source, opts) {
1697
1699
  // entrypoint keeps `hq packs update` fail-closed at its own call site
1698
1700
  // instead (checked before it un-wires anything).
1699
1701
  const transport = classify(source);
1702
+ if (opts.allowUnsigned && transport !== 'local') {
1703
+ throw new Error('--allow-unsigned is only valid for local directory sources; marketplace installs always require a platform signature.');
1704
+ }
1700
1705
  const hqRoot = findHqRoot();
1701
1706
  // `readHqVersion` (shared with `hq packs`) reads the CANONICAL
1702
1707
  // `core/core.yaml` (hq-core ≥v14), falling back to the legacy
@@ -1724,7 +1729,6 @@ async function installPackWithoutLease(source, opts) {
1724
1729
  break;
1725
1730
  case 'marketplace':
1726
1731
  fetched = await fetchMarketplace(source, tmpDir, opts.marketplaceDeps ?? defaultMarketplaceDeps(), {
1727
- requireSignature: opts.requireSignature,
1728
1732
  ...(opts.company !== undefined ? { company: opts.company } : {}),
1729
1733
  });
1730
1734
  break;
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { peekHqApiKey, requireApiKeyCapability, } from '../utils/resolve-vault-credential.js';
3
- import { MARKETPLACE_PREFIX, PINNED_HQ_ANYWHERE_SOURCE, installPack, sourceMatchesPackPattern, } from './pack-install.js';
3
+ import { MARKETPLACE_PREFIX, PINNED_HQ_ANYWHERE_SOURCE, classify, installPack, sourceMatchesPackPattern, } from './pack-install.js';
4
4
  import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
5
5
  /**
6
6
  * True when `source` installs through the marketplace — an explicit
@@ -43,6 +43,7 @@ export function registerPackageInstallCommand(parent) {
43
43
  .option('--company <co>', 'Scope package secret authorization to a specific company')
44
44
  .option('--allow-hooks', 'Skip the hooks confirmation prompt (content-pack flow)')
45
45
  .option('--allow-mcp', 'Skip the MCP server confirmation prompt (content-pack flow)')
46
+ .option('--allow-unsigned', 'Acknowledge an unsigned local directory source; marketplace installs always require a signature')
46
47
  .option('--branch', 'Follow a ref instead of SHA-pinning (git content-pack flow)')
47
48
  .option('--global', 'Install HQ into an agent runtime at user scope (requires the hq-anywhere-runtime flag)')
48
49
  .option('--runtime <runtime>', 'Runtime for --global (claude | codex | chatgpt | grok)', 'claude')
@@ -87,6 +88,21 @@ export function registerPackageInstallCommand(parent) {
87
88
  process.exitCode = 1;
88
89
  return;
89
90
  }
91
+ if (opts.allowUnsigned) {
92
+ let transport;
93
+ try {
94
+ transport = classify(source);
95
+ }
96
+ catch {
97
+ // Bare slugs resolve through the marketplace below, where this
98
+ // option is never permitted.
99
+ }
100
+ if (transport !== 'local') {
101
+ console.error('error: --allow-unsigned is only valid for local directory sources; marketplace installs always require a platform signature.');
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ }
90
106
  try {
91
107
  // Gate the top-level `hq install` entrypoint only — `installPack`
92
108
  // itself is also called by `hq packs update` (packs.ts), which must
@@ -106,6 +122,7 @@ export function registerPackageInstallCommand(parent) {
106
122
  company: opts.company,
107
123
  allowHooks: opts.allowHooks,
108
124
  allowMcp: opts.allowMcp,
125
+ allowUnsigned: opts.allowUnsigned,
109
126
  followBranch: opts.branch,
110
127
  allowPinnedHqAnywhereName: true,
111
128
  });
@@ -116,6 +133,7 @@ export function registerPackageInstallCommand(parent) {
116
133
  company: opts.company,
117
134
  allowHooks: opts.allowHooks,
118
135
  allowMcp: opts.allowMcp,
136
+ allowUnsigned: opts.allowUnsigned,
119
137
  followBranch: opts.branch,
120
138
  });
121
139
  }
@@ -128,6 +146,7 @@ export function registerPackageInstallCommand(parent) {
128
146
  company: opts.company,
129
147
  allowHooks: opts.allowHooks,
130
148
  allowMcp: opts.allowMcp,
149
+ allowUnsigned: opts.allowUnsigned,
131
150
  followBranch: opts.branch,
132
151
  });
133
152
  }
package/dist/fast-core.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
- import { parseFastCore } from "./fast-scaffold.js";
12
+ import { isDefaultOnFastFamily, parseFastCore } from "./fast-scaffold.js";
13
13
  const FAST_ROUTE_PROFILE_TOKEN = "hq-fast-route";
14
14
  function wantsFastRouteProfile(env) {
15
15
  return env.NODE_DEBUG === FAST_ROUTE_PROFILE_TOKEN;
@@ -50,7 +50,16 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
50
50
  let familyEnabled = true;
51
51
  if (entry.kind === "native" && entry.familyFlagKey) {
52
52
  const envOverride = localBooleanFlagOverride(entry.familyFlagKey, process.env);
53
- familyEnabled = envOverride ?? await (await import("./native-fast-route.js")).fastFlagEnabled(entry.familyFlagKey, process.env, { numericEnvironmentOverride: false });
53
+ if (envOverride !== null) {
54
+ familyEnabled = envOverride;
55
+ }
56
+ else {
57
+ const { fastFlagEnabled, fastFlagEnabledDefaultOn } = await import("./native-fast-route.js");
58
+ const readFamilyFlag = isDefaultOnFastFamily(entry.familyFlagKey)
59
+ ? fastFlagEnabledDefaultOn
60
+ : fastFlagEnabled;
61
+ familyEnabled = await readFamilyFlag(entry.familyFlagKey, process.env, { numericEnvironmentOverride: false });
62
+ }
54
63
  }
55
64
  if (entry.kind === "native" && entry.fallbackToCommandGraph && !familyEnabled) {
56
65
  const { runCli } = await import("./main.js");
@@ -164,6 +173,7 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
164
173
  args: parsed.args,
165
174
  cwd,
166
175
  ...(injectRoot ? { hqRoot } : {}),
176
+ msysScriptPath: process.platform === "win32",
167
177
  ...(Object.keys(childEnv).length ? { env: childEnv } : {}),
168
178
  });
169
179
  // Propagate verbatim: these scripts use their exit code as their interface.
@@ -49,6 +49,9 @@ export type FastScaffoldScript = {
49
49
  /** Whether the bundled fast-route module closure needs Node's network timeout patch. */
50
50
  requiresNodeNetworkCompatibility?: boolean;
51
51
  };
52
+ /** Family kill switches whose owner-approved default is enabled. */
53
+ export declare const CORE_NATIVE_DEFAULT_ON_FAMILY_FLAGS: readonly ["core-native-session", "core-native-hook-helpers", "core-native-policy", "core-native-utilities", "core-native-jobs", "core-native-ontology", "core-native-handoff"];
54
+ export declare function isDefaultOnFastFamily(flagKey: string): boolean;
52
55
  /** The relocated scripts that qualify for the entrypoint fast path. */
53
56
  export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];
54
57
  export type ParsedFastCore = {
@@ -9,6 +9,20 @@
9
9
  * on every turn's Stop. The entrypoint routes eligible commands before loading
10
10
  * the graph, and this module holds the parser + table that routing needs.
11
11
  */
12
+ /** Family kill switches whose owner-approved default is enabled. */
13
+ export const CORE_NATIVE_DEFAULT_ON_FAMILY_FLAGS = [
14
+ "core-native-session",
15
+ "core-native-hook-helpers",
16
+ "core-native-policy",
17
+ "core-native-utilities",
18
+ "core-native-jobs",
19
+ "core-native-ontology",
20
+ "core-native-handoff",
21
+ ];
22
+ const defaultOnFamilyFlags = new Set(CORE_NATIVE_DEFAULT_ON_FAMILY_FLAGS);
23
+ export function isDefaultOnFastFamily(flagKey) {
24
+ return defaultOnFamilyFlags.has(flagKey);
25
+ }
12
26
  /** The relocated scripts that qualify for the entrypoint fast path. */
13
27
  export const FAST_SCAFFOLD_SCRIPTS = [
14
28
  {
@@ -67,7 +67,10 @@ export declare function resolveFlagGate(reader: FlagReader | null | undefined, f
67
67
  /** Resolve one explicit company's flag without relying on process-wide binding. */
68
68
  export declare function resolveCompanyFlagGate(companyUid: string, idToken: string, flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean, dependencies?: CompanyFlagGateDependencies): Promise<boolean>;
69
69
  /** Resolve a process gate against the shared snapshot reader, when configured. */
70
- export declare function resolveProcessFlagGate(flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean, companyUid?: string): boolean;
70
+ export declare function resolveProcessFlagGate(flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean, companyUid?: string, options?: {
71
+ acceptStaleSnapshot?: boolean;
72
+ killSwitchWithoutCompany?: boolean;
73
+ }): boolean;
71
74
  /**
72
75
  * The registry snapshot held by this process, or null when none has arrived
73
76
  * (no endpoint configured, offline, or still fetching). Callers that must
@@ -9,9 +9,9 @@ import { Worker } from "node:worker_threads";
9
9
  import { createFlagClient, isFlagSnapshot, readLocalOverride, resolveFlag, } from "@indigoai-us/hq-flags-client";
10
10
  import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
11
11
  import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
12
- import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
12
+ import { resolveFlagSnapshotCompanyUid, resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
13
13
  import { registerFlagRegistryRefreshWaiter } from "./flag-registry-refresh-wait.js";
14
- import { flagSnapshotWriteFailureTags, readFlagSnapshotFile, writePersonFlagSnapshotFile, writeFlagSnapshotFile, } from "./flag-snapshot-file.js";
14
+ import { flagSnapshotWriteFailureTags, readDefaultOnFlagSnapshot, readFlagSnapshotFile, writePersonFlagSnapshotFile, writeFlagSnapshotFile, } from "./flag-snapshot-file.js";
15
15
  function captureFlagRegistryException(error, tags, sentry) {
16
16
  if (!sentry)
17
17
  return;
@@ -348,10 +348,20 @@ export async function resolveCompanyFlagGate(companyUid, idToken, flagKey, looku
348
348
  }
349
349
  }
350
350
  /** Resolve a process gate against the shared snapshot reader, when configured. */
351
- export function resolveProcessFlagGate(flagKey, lookup, legacyFallback, companyUid) {
351
+ export function resolveProcessFlagGate(flagKey, lookup, legacyFallback, companyUid, options = {}) {
352
+ const defaultOnKillSwitch = options.killSwitchWithoutCompany === true;
353
+ const resolvedCompanyUid = defaultOnKillSwitch
354
+ ? resolveFlagSnapshotCompanyUid()
355
+ : resolveProcessFlagCompanyContext().companyUid;
352
356
  const targetCompanyUid = companyUid ?? (processReader
353
- ? refreshCompanyUid
354
- : resolveProcessFlagCompanyContext().companyUid);
357
+ ? refreshCompanyUid ?? (defaultOnKillSwitch ? resolvedCompanyUid : undefined)
358
+ : resolvedCompanyUid);
359
+ if (!targetCompanyUid && defaultOnKillSwitch) {
360
+ const override = readLocalOverride(flagKey, process.env);
361
+ if (override !== null)
362
+ return override;
363
+ return readDefaultOnFlagSnapshot(flagKey);
364
+ }
355
365
  const processSnapshotMatchesTarget = refreshCompanyUid === targetCompanyUid;
356
366
  if (processReader && heldSnapshot && processSnapshotMatchesTarget) {
357
367
  return resolveFlagGate(processReader, flagKey, lookup, legacyFallback);
@@ -360,7 +370,9 @@ export function resolveProcessFlagGate(flagKey, lookup, legacyFallback, companyU
360
370
  // Use the last validated, fresh company snapshot in that window, and when
361
371
  // the process reader was initialized for a different company scope.
362
372
  if (targetCompanyUid) {
363
- const cached = readFlagSnapshotFile(targetCompanyUid);
373
+ const cached = readFlagSnapshotFile(targetCompanyUid, options.acceptStaleSnapshot
374
+ ? { maxAgeMs: Number.POSITIVE_INFINITY }
375
+ : {});
364
376
  if (cached) {
365
377
  const snapshot = { version: 0, flags: cached.flags };
366
378
  const reader = createProcessSnapshotReader([targetCompanyUid], () => snapshot);
@@ -37,4 +37,6 @@ export declare function readFlagSnapshotFile(companyUid: string, options?: {
37
37
  now?: number;
38
38
  maxAgeMs?: number;
39
39
  }): PersistedFlagSnapshot | null;
40
+ /** Resolve a default-on kill switch from offline snapshots, allowing stale decisions. */
41
+ export declare function readDefaultOnFlagSnapshot(flagKey: string, companyUid?: string, env?: NodeJS.ProcessEnv): boolean;
40
42
  //# sourceMappingURL=flag-snapshot-file.d.ts.map
@@ -372,4 +372,34 @@ export function readFlagSnapshotFile(companyUid, options = {}) {
372
372
  }
373
373
  return primary;
374
374
  }
375
+ /** Resolve a default-on kill switch from offline snapshots, allowing stale decisions. */
376
+ export function readDefaultOnFlagSnapshot(flagKey, companyUid, env = process.env) {
377
+ const options = { env, maxAgeMs: Number.POSITIVE_INFINITY };
378
+ if (companyUid)
379
+ return readFlagSnapshotFile(companyUid, options)?.flags[flagKey] !== false;
380
+ const directories = new Set([path.dirname(personFlagSnapshotPath(env))]);
381
+ const fallbackProbe = fallbackSnapshotPath("cmp_000", false, env);
382
+ if (fallbackProbe)
383
+ directories.add(path.dirname(fallbackProbe));
384
+ for (const directory of directories) {
385
+ let names;
386
+ try {
387
+ names = fs.readdirSync(directory);
388
+ }
389
+ catch {
390
+ continue;
391
+ }
392
+ for (const name of names) {
393
+ if (name === "snapshot-person.json") {
394
+ if (readPersonFlagSnapshotFile(options)?.flags[flagKey] === false)
395
+ return false;
396
+ continue;
397
+ }
398
+ const match = /^snapshot-(cmp_[A-Za-z0-9]{3,128})\.json$/.exec(name);
399
+ if (match?.[1] && readFlagSnapshotFile(match[1], options)?.flags[flagKey] === false)
400
+ return false;
401
+ }
402
+ }
403
+ return true;
404
+ }
375
405
  //# sourceMappingURL=flag-snapshot-file.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Marketplace artifact signing keys trusted by the CLI.
3
+ *
4
+ * Public keys are intentionally embedded: authenticity must not depend on the
5
+ * marketplace response that carries the artifact. Add old keys here during a
6
+ * rotation so installs of previously signed listings continue to verify.
7
+ *
8
+ * `HQ_PACK_PUBLIC_KEY` is a development-only override for the current key id.
9
+ * It exists for local integration tests and non-production marketplace stacks;
10
+ * production installs must use the embedded platform key below.
11
+ */
12
+ export declare const MARKETPLACE_SIGNING_KEYS: {
13
+ readonly "mkt-sign-v1": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEALmDnMFl0vMytdx/TdP8+SmZjW2oES6B1nOESioUNc6k=\n-----END PUBLIC KEY-----";
14
+ };
15
+ export type MarketplaceSigningKeyId = keyof typeof MARKETPLACE_SIGNING_KEYS;
16
+ /**
17
+ * Return the trusted public key only when the listing names a pinned key and
18
+ * echoes the same public key. The echoed key is checked for API-contract drift,
19
+ * never trusted as the source of authenticity.
20
+ */
21
+ export declare function trustedMarketplaceSigningKey(signingKeyId: unknown, listingPublicKey: unknown): string;
22
+ //# sourceMappingURL=pack-signing-keys.d.ts.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Marketplace artifact signing keys trusted by the CLI.
3
+ *
4
+ * Public keys are intentionally embedded: authenticity must not depend on the
5
+ * marketplace response that carries the artifact. Add old keys here during a
6
+ * rotation so installs of previously signed listings continue to verify.
7
+ *
8
+ * `HQ_PACK_PUBLIC_KEY` is a development-only override for the current key id.
9
+ * It exists for local integration tests and non-production marketplace stacks;
10
+ * production installs must use the embedded platform key below.
11
+ */
12
+ export const MARKETPLACE_SIGNING_KEYS = {
13
+ "mkt-sign-v1": `-----BEGIN PUBLIC KEY-----
14
+ MCowBQYDK2VwAyEALmDnMFl0vMytdx/TdP8+SmZjW2oES6B1nOESioUNc6k=
15
+ -----END PUBLIC KEY-----`,
16
+ };
17
+ function normalizedPem(pem) {
18
+ return pem.replace(/\r\n/g, "\n").trim();
19
+ }
20
+ function developmentPublicKeyOverride() {
21
+ // Packaged production CLIs always use the embedded key. The override exists
22
+ // only for local development and test stacks.
23
+ if (process.env.NODE_ENV !== "development" && process.env.NODE_ENV !== "test") {
24
+ return undefined;
25
+ }
26
+ const override = process.env.HQ_PACK_PUBLIC_KEY;
27
+ return typeof override === "string" && override.trim().length > 0
28
+ ? override
29
+ : undefined;
30
+ }
31
+ /**
32
+ * Return the trusted public key only when the listing names a pinned key and
33
+ * echoes the same public key. The echoed key is checked for API-contract drift,
34
+ * never trusted as the source of authenticity.
35
+ */
36
+ export function trustedMarketplaceSigningKey(signingKeyId, listingPublicKey) {
37
+ if (typeof signingKeyId !== "string" || signingKeyId.length === 0) {
38
+ throw new Error("Marketplace listing did not provide a signing key id — refusing to install unsigned or legacy listings.");
39
+ }
40
+ if (!Object.hasOwn(MARKETPLACE_SIGNING_KEYS, signingKeyId)) {
41
+ throw new Error(`Marketplace listing uses unpinned signing key "${signingKeyId}" — refusing to install.`);
42
+ }
43
+ if (typeof listingPublicKey !== "string" || listingPublicKey.trim().length === 0) {
44
+ throw new Error("Marketplace listing did not provide the public key for its signing key id — refusing to install.");
45
+ }
46
+ const keyId = signingKeyId;
47
+ const pinnedKey = developmentPublicKeyOverride() ?? MARKETPLACE_SIGNING_KEYS[keyId];
48
+ if (normalizedPem(listingPublicKey) !== normalizedPem(pinnedKey)) {
49
+ throw new Error(`Marketplace listing public key does not match pinned signing key "${keyId}" — refusing to install.`);
50
+ }
51
+ return pinnedKey;
52
+ }
53
+ //# sourceMappingURL=pack-signing-keys.js.map
@@ -3,4 +3,6 @@ import { type ProcessFlagCompanyContext } from "./process-flag-company-context-l
3
3
  export type { ProcessFlagCompanyContext } from "./process-flag-company-context-light.js";
4
4
  /** Resolve the same company identity the process-wide flag reader uses. */
5
5
  export declare function resolveProcessFlagCompanyContext(env?: NodeJS.ProcessEnv): ProcessFlagCompanyContext;
6
+ /** Resolve the process scope, then the explicitly selected device default, without network access. */
7
+ export declare function resolveFlagSnapshotCompanyUid(env?: NodeJS.ProcessEnv): string | undefined;
6
8
  //# sourceMappingURL=process-flag-company-context.d.ts.map
@@ -1,5 +1,7 @@
1
1
  /** Company scope shared by the full flag registry and its fast-path reader. */
2
2
  import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
3
+ import { readDeviceConfig } from "./work-context/config.js";
4
+ import { workContextRoot } from "./work-context/paths.js";
3
5
  import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
4
6
  import { normalizedCompanyUid, resolveLightProcessFlagCompanyContext, } from "./process-flag-company-context-light.js";
5
7
  function companyUidForContext(context, env = process.env) {
@@ -20,4 +22,17 @@ export function resolveProcessFlagCompanyContext(env = process.env) {
20
22
  ...(context.companySlug ? { companySlug: context.companySlug } : {}),
21
23
  };
22
24
  }
25
+ /** Resolve the process scope, then the explicitly selected device default, without network access. */
26
+ export function resolveFlagSnapshotCompanyUid(env = process.env) {
27
+ const contextUid = resolveProcessFlagCompanyContext(env).companyUid;
28
+ if (contextUid)
29
+ return contextUid;
30
+ try {
31
+ const defaultCompany = readDeviceConfig({ root: workContextRoot(undefined, env) }).defaultCompany;
32
+ return defaultCompany?.enabled ? normalizedCompanyUid(defaultCompany.uid) : undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
23
38
  //# sourceMappingURL=process-flag-company-context.js.map
@@ -1,10 +1,10 @@
1
- /** Default-off flag check for native entrypoint routes. */
1
+ /** Fast-route flag check for native entrypoint routes. */
2
2
  export declare function nativeFastRouteEnabled(argv: readonly string[], env?: NodeJS.ProcessEnv): Promise<boolean>;
3
3
  /** Reads a default-off fast-route gate from its local override or fresh snapshot. */
4
4
  export declare function fastFlagEnabled(flagKey: string, env?: NodeJS.ProcessEnv, options?: {
5
5
  numericEnvironmentOverride?: boolean;
6
6
  }): Promise<boolean>;
7
- /** Reads a fail-safe flag that stays enabled unless a fresh value disables it. */
7
+ /** Reads an enabled-by-default flag; any validated explicit false stays a kill switch, even when stale. */
8
8
  export declare function fastFlagEnabledDefaultOn(flagKey: string, env?: NodeJS.ProcessEnv, options?: {
9
9
  numericEnvironmentOverride?: boolean;
10
10
  }): Promise<boolean>;
@@ -1,11 +1,11 @@
1
- /** Default-off flag check for native entrypoint routes. */
1
+ /** Fast-route flag check for native entrypoint routes. */
2
2
  import { localOverrideEnvVar, readLocalOverride } from "@indigoai-us/hq-flags-client";
3
3
  import { parseFastCore } from "./fast-scaffold.js";
4
4
  export async function nativeFastRouteEnabled(argv, env = process.env) {
5
5
  const parsed = parseFastCore(argv);
6
6
  if (!parsed || parsed.entry.kind !== "native")
7
7
  return false;
8
- return fastFlagEnabled(parsed.entry.flagKey, env);
8
+ return fastFlagEnabledDefaultOn(parsed.entry.flagKey, env);
9
9
  }
10
10
  /** Reads a default-off fast-route gate from its local override or fresh snapshot. */
11
11
  export async function fastFlagEnabled(flagKey, env = process.env, options = {}) {
@@ -28,20 +28,18 @@ export async function fastFlagEnabled(flagKey, env = process.env, options = {})
28
28
  return false;
29
29
  }
30
30
  }
31
- /** Reads a fail-safe flag that stays enabled unless a fresh value disables it. */
31
+ /** Reads an enabled-by-default flag; any validated explicit false stays a kill switch, even when stale. */
32
32
  export async function fastFlagEnabledDefaultOn(flagKey, env = process.env, options = {}) {
33
33
  try {
34
34
  const override = nativeRouteOverride(flagKey, env, options.numericEnvironmentOverride !== false);
35
35
  if (override !== null)
36
36
  return override;
37
- const [{ resolveProcessFlagCompanyContext }, { readFlagSnapshotFile }] = await Promise.all([
37
+ const [{ resolveFlagSnapshotCompanyUid }, { readDefaultOnFlagSnapshot }] = await Promise.all([
38
38
  import("./lib/process-flag-company-context.js"),
39
39
  import("./lib/flag-snapshot-file.js"),
40
40
  ]);
41
- const { companyUid } = resolveProcessFlagCompanyContext(env);
42
- if (!companyUid)
43
- return true;
44
- return readFlagSnapshotFile(companyUid, { env })?.flags[flagKey] !== false;
41
+ const companyUid = resolveFlagSnapshotCompanyUid(env);
42
+ return readDefaultOnFlagSnapshot(flagKey, companyUid, env);
45
43
  }
46
44
  catch {
47
45
  // This flag is a kill switch: missing scope or cache data must leave the feature enabled.
@@ -0,0 +1,4 @@
1
+ import { type SpawnPlan } from "./windows-spawn.js";
2
+ /** Resolve the installed hq executable before applying the platform spawn plan. */
3
+ export declare function planHqSpawn(args: readonly string[], env: NodeJS.ProcessEnv, cwd: string, platform?: NodeJS.Platform, existsSync?: (candidate: string) => boolean): SpawnPlan;
4
+ //# sourceMappingURL=hq-spawn.d.ts.map
@@ -0,0 +1,11 @@
1
+ import * as fs from "node:fs";
2
+ import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
3
+ import { planResolvedCommandSpawn } from "./windows-spawn.js";
4
+ /** Resolve the installed hq executable before applying the platform spawn plan. */
5
+ export function planHqSpawn(args, env, cwd, platform = process.platform, existsSync = fs.existsSync) {
6
+ const executable = platform === "win32"
7
+ ? resolveWindowsExecutable("hq", env, existsSync, cwd) ?? "hq"
8
+ : "hq";
9
+ return planResolvedCommandSpawn(executable, args, env, platform);
10
+ }
11
+ //# sourceMappingURL=hq-spawn.js.map
@@ -42,7 +42,7 @@ export const RESIDENT_PACKAGE_MODULES = {
42
42
  "bot-logs-follow": [],
43
43
  mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../lib/anywhere-person-setting.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
44
44
  childWrapper: ["../main.js", "../lib/service-manager/startup-migration.js"],
45
- coreJobRun: ["../lib/flag-registry.js"],
45
+ coreJobRun: ["../commands/core-native-jobs-flag.js", "../commands/core-native-family-flag.js", "../lib/flag-registry.js", "../lib/flag-snapshot-file.js"],
46
46
  "daemon-call": ["../lib/daemon/protocol.js"],
47
47
  daemon: [
48
48
  "../sentry.js",
@@ -203,7 +203,10 @@ const LOADERS = {
203
203
  () => import("../lib/service-manager/startup-migration.js"),
204
204
  ],
205
205
  coreJobRun: [
206
+ () => import("../commands/core-native-jobs-flag.js"),
207
+ () => import("../commands/core-native-family-flag.js"),
206
208
  () => import("../lib/flag-registry.js"),
209
+ () => import("../lib/flag-snapshot-file.js"),
207
210
  ],
208
211
  "daemon-call": [
209
212
  () => import("../lib/daemon/protocol.js"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.67",
3
+ "version": "5.345.69",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {