@indigoai-us/hq-cli 5.345.68 → 5.345.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/assets/scaffold/core/scripts/hq-dm-bind.sh +5 -0
- package/dist/commands/core-checkpoint-stop-gate-native.js +13 -1
- package/dist/commands/core-delegate-w1b.js +2 -2
- package/dist/commands/core-delegate.js +2 -2
- package/dist/commands/core-dm-bind-native.d.ts +1 -1
- package/dist/commands/core-dm-bind-native.js +55 -52
- package/dist/commands/core-handoff-fast.js +13 -8
- package/dist/commands/core-handoff.js +10 -2
- package/dist/commands/core-native-family-flag.d.ts +7 -0
- package/dist/commands/core-native-family-flag.js +21 -0
- package/dist/commands/core-native-handoff-flag.js +2 -2
- package/dist/commands/core-native-hook-helpers-flag.js +2 -2
- package/dist/commands/core-native-jobs-flag.js +2 -2
- package/dist/commands/core-native-policy-flag.js +2 -2
- package/dist/commands/core-native-session-flag.js +2 -2
- package/dist/commands/core-native-utilities-flag.js +2 -2
- package/dist/commands/core-ontology-native.js +2 -2
- package/dist/commands/core-refresh-vault-access-native.js +14 -1
- package/dist/fast-core.d.ts +10 -1
- package/dist/fast-core.js +86 -15
- package/dist/fast-scaffold.d.ts +3 -0
- package/dist/fast-scaffold.js +14 -0
- package/dist/index.js +77 -11
- package/dist/lib/flag-registry.d.ts +4 -1
- package/dist/lib/flag-registry.js +18 -6
- package/dist/lib/flag-snapshot-file.d.ts +2 -0
- package/dist/lib/flag-snapshot-file.js +30 -0
- package/dist/lib/process-flag-company-context.d.ts +2 -0
- package/dist/lib/process-flag-company-context.js +15 -0
- package/dist/native-fast-route.d.ts +2 -2
- package/dist/native-fast-route.js +6 -8
- package/dist/startup-registration.d.ts +2 -0
- package/dist/startup-registration.js +20 -0
- package/dist/utils/hq-spawn.d.ts +4 -0
- package/dist/utils/hq-spawn.js +11 -0
- package/dist/utils/install-tree-torn.d.ts +1 -1
- package/dist/utils/resident-package-preload.js +4 -1
- package/dist/utils/sentry-fingerprint.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.70] — 2026-10-10
|
|
6
|
+
- Native `hq core` fast-route imports now use the bounded torn-install recovery path. If an update temporarily removes a route module, the CLI waits for the package tree to settle and re-executes once; a module that remains missing prints the reinstall remedy.
|
|
7
|
+
|
|
8
|
+
## [5.345.69] — 2026-10-10
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
5
11
|
## [5.345.68] — 2026-10-09
|
|
6
12
|
|
|
7
13
|
- 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.
|
|
@@ -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
|
|
|
@@ -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
|
|
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 {
|
|
1219
|
-
if (!
|
|
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 {
|
|
1026
|
-
if (!
|
|
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 =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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() && (
|
|
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(
|
|
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(
|
|
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
|
|
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 {
|
|
5
|
-
return
|
|
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 {
|
|
5
|
-
return
|
|
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 {
|
|
5
|
-
return
|
|
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 {
|
|
10
|
-
return
|
|
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 {
|
|
5
|
-
return
|
|
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 {
|
|
5
|
-
return
|
|
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 {
|
|
1174
|
-
if (!
|
|
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
|
|
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;
|
package/dist/fast-core.d.ts
CHANGED
|
@@ -7,17 +7,26 @@
|
|
|
7
7
|
* module. The registration in `core.ts` remains the complete fallback path.
|
|
8
8
|
*/
|
|
9
9
|
import { parseFastCore } from "./fast-scaffold.js";
|
|
10
|
+
type RecoveredModule<T> = {
|
|
11
|
+
module?: T;
|
|
12
|
+
reexecStatus?: number;
|
|
13
|
+
};
|
|
14
|
+
type RecoverFastImportOptions = {
|
|
15
|
+
reexecOnSettled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
type RecoverFastImport = <T>(load: () => Promise<T>, options?: RecoverFastImportOptions) => Promise<RecoveredModule<T>>;
|
|
10
18
|
export declare function readStdinIfPiped(stream: NodeJS.ReadStream, maxBytes?: number): Promise<string>;
|
|
11
19
|
/**
|
|
12
20
|
* Run the fast-path core command named in `argv` and exit with its code.
|
|
13
21
|
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
|
14
22
|
* doing anything.
|
|
15
23
|
*/
|
|
16
|
-
export declare function runFastCore(argv: readonly string[], parsed?: ReturnType<typeof parseFastCore
|
|
24
|
+
export declare function runFastCore(argv: readonly string[], parsed?: ReturnType<typeof parseFastCore>, recoverImport?: RecoverFastImport): Promise<void>;
|
|
17
25
|
/**
|
|
18
26
|
* Resolve valid explicit roots for the two measured hook routes without loading
|
|
19
27
|
* the general root resolver. Anything uncertain falls through to that resolver,
|
|
20
28
|
* which retains its diagnostics and cwd-discovery behavior.
|
|
21
29
|
*/
|
|
22
30
|
export declare function resolveExplicitFastLiveRoot(hqRootArg: string | undefined, env: NodeJS.ProcessEnv): string | undefined;
|
|
31
|
+
export {};
|
|
23
32
|
//# sourceMappingURL=fast-core.d.ts.map
|
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;
|
|
@@ -33,7 +33,7 @@ export async function readStdinIfPiped(stream, maxBytes) {
|
|
|
33
33
|
* Only call when {@link isFastCoreRequest} is true; a non-match returns without
|
|
34
34
|
* doing anything.
|
|
35
35
|
*/
|
|
36
|
-
export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
36
|
+
export async function runFastCore(argv, parsed = parseFastCore(argv), recoverImport = async (load) => ({ module: await load() })) {
|
|
37
37
|
if (!parsed)
|
|
38
38
|
return;
|
|
39
39
|
const profile = wantsFastRouteProfile(process.env);
|
|
@@ -50,18 +50,47 @@ 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
|
-
|
|
53
|
+
if (envOverride !== null) {
|
|
54
|
+
familyEnabled = envOverride;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const loaded = await recoverImport(() => import("./native-fast-route.js"));
|
|
58
|
+
if (loaded.reexecStatus !== undefined) {
|
|
59
|
+
process.exitCode = loaded.reexecStatus;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!loaded.module)
|
|
63
|
+
throw new Error("Native fast-route module did not load");
|
|
64
|
+
const { fastFlagEnabled, fastFlagEnabledDefaultOn } = loaded.module;
|
|
65
|
+
const readFamilyFlag = isDefaultOnFastFamily(entry.familyFlagKey)
|
|
66
|
+
? fastFlagEnabledDefaultOn
|
|
67
|
+
: fastFlagEnabled;
|
|
68
|
+
familyEnabled = await readFamilyFlag(entry.familyFlagKey, process.env, { numericEnvironmentOverride: false });
|
|
69
|
+
}
|
|
54
70
|
}
|
|
55
71
|
if (entry.kind === "native" && entry.fallbackToCommandGraph && !familyEnabled) {
|
|
56
|
-
const
|
|
57
|
-
|
|
72
|
+
const loaded = await recoverImport(() => import("./main.js"));
|
|
73
|
+
if (loaded.reexecStatus !== undefined) {
|
|
74
|
+
process.exitCode = loaded.reexecStatus;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!loaded.module)
|
|
78
|
+
throw new Error("Main command graph did not load");
|
|
79
|
+
await loaded.module.runCli();
|
|
58
80
|
return;
|
|
59
81
|
}
|
|
60
82
|
familyFlagMs = profileElapsed(profileStart, profileMark());
|
|
61
83
|
const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
|
|
62
84
|
let worktreeScopeFixEnabled;
|
|
63
85
|
if (entry.name === "repo-run-registry") {
|
|
64
|
-
const
|
|
86
|
+
const loaded = await recoverImport(() => import("./commands/core-rrr-worktree-scope-fix-fast-flag.js"));
|
|
87
|
+
if (loaded.reexecStatus !== undefined) {
|
|
88
|
+
process.exitCode = loaded.reexecStatus;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!loaded.module)
|
|
92
|
+
throw new Error("Repo-run-registry flag module did not load");
|
|
93
|
+
const { coreRrrWorktreeScopeFixFastFlagEnabled } = loaded.module;
|
|
65
94
|
worktreeScopeFixEnabled = await coreRrrWorktreeScopeFixFastFlagEnabled(process.env);
|
|
66
95
|
}
|
|
67
96
|
const shouldInjectRoot = useShell
|
|
@@ -87,14 +116,27 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
87
116
|
hqRoot = shortcutRoot;
|
|
88
117
|
}
|
|
89
118
|
else {
|
|
90
|
-
const
|
|
119
|
+
const loaded = await recoverImport(() => import("./utils/hq-roots-core.js"));
|
|
120
|
+
if (loaded.reexecStatus !== undefined) {
|
|
121
|
+
process.exitCode = loaded.reexecStatus;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!loaded.module)
|
|
125
|
+
throw new Error("HQ root resolver did not load");
|
|
126
|
+
const { resolveLiveRoot } = loaded.module;
|
|
91
127
|
try {
|
|
92
128
|
hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
|
|
93
129
|
}
|
|
94
130
|
catch {
|
|
95
131
|
if (entry.kind === "native") {
|
|
96
|
-
const
|
|
97
|
-
|
|
132
|
+
const loaded = await recoverImport(() => import("./main.js"));
|
|
133
|
+
if (loaded.reexecStatus !== undefined) {
|
|
134
|
+
process.exitCode = loaded.reexecStatus;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!loaded.module)
|
|
138
|
+
throw new Error("Main command graph did not load");
|
|
139
|
+
await loaded.module.runCli();
|
|
98
140
|
return;
|
|
99
141
|
}
|
|
100
142
|
hqRoot = undefined;
|
|
@@ -104,9 +146,22 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
104
146
|
rootResolutionMs = profileElapsed(rootResolutionStartedAt, profileMark());
|
|
105
147
|
if (entry.kind === "native" && !useShell) {
|
|
106
148
|
const handlerImportStartedAt = profileMark();
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
149
|
+
const errorHandlers = await recoverImport(() => import("./utils/fast-stream-errors.js"));
|
|
150
|
+
if (errorHandlers.reexecStatus !== undefined) {
|
|
151
|
+
process.exitCode = errorHandlers.reexecStatus;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!errorHandlers.module)
|
|
155
|
+
throw new Error("Fast-path stream error handlers did not load");
|
|
156
|
+
errorHandlers.module.installFastPathStreamErrorHandlers();
|
|
157
|
+
const loadedHandler = await recoverImport(() => import(entry.modulePath));
|
|
158
|
+
if (loadedHandler.reexecStatus !== undefined) {
|
|
159
|
+
process.exitCode = loadedHandler.reexecStatus;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!loadedHandler.module)
|
|
163
|
+
throw new Error("Native fast handler module did not load");
|
|
164
|
+
const handlerModule = loadedHandler.module;
|
|
110
165
|
const handler = handlerModule[entry.exportName];
|
|
111
166
|
if (typeof handler !== "function") {
|
|
112
167
|
throw new Error(`Native fast handler ${entry.exportName} was not exported`);
|
|
@@ -131,8 +186,16 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
131
186
|
if (!entry.fallbackToCommandGraph) {
|
|
132
187
|
throw new Error(`Native fast handler ${entry.exportName} requested an unavailable command-graph fallback`);
|
|
133
188
|
}
|
|
134
|
-
|
|
135
|
-
|
|
189
|
+
// The fast handler has already run (and may have consumed stdin). If a
|
|
190
|
+
// torn main import is detected now, report it without replaying the command.
|
|
191
|
+
const loaded = await recoverImport(() => import("./main.js"), { reexecOnSettled: false });
|
|
192
|
+
if (loaded.reexecStatus !== undefined) {
|
|
193
|
+
process.exitCode = loaded.reexecStatus;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!loaded.module)
|
|
197
|
+
throw new Error("Main command graph did not load");
|
|
198
|
+
await loaded.module.runCli();
|
|
136
199
|
return;
|
|
137
200
|
}
|
|
138
201
|
// Let Node drain handler output before terminating. The timeout guard's
|
|
@@ -158,12 +221,20 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
158
221
|
...(entry.name === "repo-run-registry" ? { HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX: String(worktreeScopeFixEnabled) } : {}),
|
|
159
222
|
};
|
|
160
223
|
// Keep the rollback runner off native commands' import path.
|
|
161
|
-
const
|
|
224
|
+
const runner = await recoverImport(() => import("./utils/run-bundled-script.js"));
|
|
225
|
+
if (runner.reexecStatus !== undefined) {
|
|
226
|
+
process.exitCode = runner.reexecStatus;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!runner.module)
|
|
230
|
+
throw new Error("Bundled-script runner did not load");
|
|
231
|
+
const { runBundledScript } = runner.module;
|
|
162
232
|
const { code } = runBundledScript({
|
|
163
233
|
asset,
|
|
164
234
|
args: parsed.args,
|
|
165
235
|
cwd,
|
|
166
236
|
...(injectRoot ? { hqRoot } : {}),
|
|
237
|
+
msysScriptPath: process.platform === "win32",
|
|
167
238
|
...(Object.keys(childEnv).length ? { env: childEnv } : {}),
|
|
168
239
|
});
|
|
169
240
|
// Propagate verbatim: these scripts use their exit code as their interface.
|
package/dist/fast-scaffold.d.ts
CHANGED
|
@@ -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 = {
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -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
|
{
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,80 @@ async function runMainWithInstallTreeRecovery() {
|
|
|
59
59
|
throw error;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Fast-route imports happen before main.ts command registration, so the
|
|
64
|
+
* registration recovery seam cannot see a torn fast module unless each import
|
|
65
|
+
* is run through the same bounded recovery boundary. Imports before handler
|
|
66
|
+
* execution may re-exec after a settled repair; the post-handler fallback passes
|
|
67
|
+
* reexecOnSettled=false so a torn main import is reported without replaying the
|
|
68
|
+
* handler or losing the stdin it already consumed.
|
|
69
|
+
*/
|
|
70
|
+
const recoverFastImport = async (load, options = {}) => {
|
|
71
|
+
try {
|
|
72
|
+
return { module: await load() };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Keep the clean hook path free of startup-registration's update-lock and
|
|
76
|
+
// package-root imports. Only a failed import pays for the recovery seam.
|
|
77
|
+
const { registerCommandsWithRecovery } = await import("./startup-registration.js");
|
|
78
|
+
let module;
|
|
79
|
+
try {
|
|
80
|
+
const result = await registerCommandsWithRecovery({
|
|
81
|
+
register: async () => {
|
|
82
|
+
module = await load();
|
|
83
|
+
},
|
|
84
|
+
argv: process.argv,
|
|
85
|
+
env: process.env,
|
|
86
|
+
entryPath: fileURLToPath(import.meta.url),
|
|
87
|
+
stderr: process.stderr,
|
|
88
|
+
reexecOnSettled: options.reexecOnSettled,
|
|
89
|
+
});
|
|
90
|
+
return result.reexecStatus === undefined
|
|
91
|
+
? { module }
|
|
92
|
+
: { reexecStatus: result.reexecStatus };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Fast-route imports precede main.ts/Sentry initialization. When recovery
|
|
96
|
+
// cannot repair a still-absent module, report it here as the registration
|
|
97
|
+
// path does instead of relying on the later unhandled-rejection boundary.
|
|
98
|
+
const installTree = await import("./utils/install-tree-torn.js");
|
|
99
|
+
if (!installTree.isInstallTreeTornError(error))
|
|
100
|
+
throw error;
|
|
101
|
+
process.stderr.write(`${installTree.installTreeTornStderrLine(error)}\n`);
|
|
102
|
+
const sentry = await import("./sentry.js");
|
|
103
|
+
sentry.initSentry();
|
|
104
|
+
sentry.Sentry.captureException(error, installTree.installTreeTornCaptureContext(error));
|
|
105
|
+
sentry.finishSentrySession();
|
|
106
|
+
await sentry.Sentry.flush(2_000);
|
|
107
|
+
return { reexecStatus: 1 };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
async function runFastCoreWithRecovery(parsed) {
|
|
112
|
+
const loaded = await recoverFastImport(() => import("./fast-core.js"));
|
|
113
|
+
if (loaded.reexecStatus !== undefined) {
|
|
114
|
+
process.exitCode = loaded.reexecStatus;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!loaded.module)
|
|
118
|
+
throw new Error("Fast core module did not load");
|
|
119
|
+
await loaded.module.runFastCore(process.argv, parsed, recoverFastImport);
|
|
120
|
+
}
|
|
121
|
+
async function runNativeFastRouteWithRecovery(parsed) {
|
|
122
|
+
const loaded = await recoverFastImport(() => import("./native-fast-route.js"));
|
|
123
|
+
if (loaded.reexecStatus !== undefined) {
|
|
124
|
+
process.exitCode = loaded.reexecStatus;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!loaded.module)
|
|
128
|
+
throw new Error("Native fast-route module did not load");
|
|
129
|
+
if (await loaded.module.nativeFastRouteEnabled(process.argv)) {
|
|
130
|
+
await runFastCoreWithRecovery(parsed);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
await runMainWithInstallTreeRecovery();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
62
136
|
function dispatchCliEntry() {
|
|
63
137
|
const versionRequest = isVersionRequest(process.argv);
|
|
64
138
|
const helpRequest = !versionRequest && isCommandHelpRequest(process.argv);
|
|
@@ -99,21 +173,13 @@ function dispatchCliEntry() {
|
|
|
99
173
|
// An explicit enabled override has already decided the global gate. Skip the
|
|
100
174
|
// snapshot reader import on hot native plumbing paths; runFastCore still
|
|
101
175
|
// checks the command-family flag before choosing native or shell rollback.
|
|
102
|
-
void
|
|
176
|
+
void runFastCoreWithRecovery(parsedFastCore);
|
|
103
177
|
}
|
|
104
178
|
else if (parsedFastCore?.entry.kind === "native") {
|
|
105
179
|
// Native routes are default-off. Resolve the company-scoped persisted flag
|
|
106
180
|
// without loading the command graph; a miss or stale snapshot takes the
|
|
107
181
|
// existing full-graph route.
|
|
108
|
-
void
|
|
109
|
-
if (await nativeFastRouteEnabled(process.argv)) {
|
|
110
|
-
const { runFastCore } = await import("./fast-core.js");
|
|
111
|
-
await runFastCore(process.argv, parsedFastCore);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
await runMainWithInstallTreeRecovery();
|
|
115
|
-
}
|
|
116
|
-
});
|
|
182
|
+
void runNativeFastRouteWithRecovery(parsedFastCore);
|
|
117
183
|
}
|
|
118
184
|
else if (parseSecretsEnvFastRequest(process.argv)) {
|
|
119
185
|
const request = parseSecretsEnvFastRequest(process.argv);
|
|
@@ -138,7 +204,7 @@ function dispatchCliEntry() {
|
|
|
138
204
|
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
139
205
|
// dispatches straight to the bundled-script runner, so it never pays for the
|
|
140
206
|
// ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
|
|
141
|
-
void
|
|
207
|
+
void runFastCoreWithRecovery(parsedFastCore);
|
|
142
208
|
}
|
|
143
209
|
else {
|
|
144
210
|
// Keep the command lifecycle detached from module evaluation, as it was
|
|
@@ -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
|
|
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
|
-
:
|
|
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
|
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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 [{
|
|
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
|
|
42
|
-
|
|
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.
|
|
@@ -88,6 +88,8 @@ export interface RegisterCommandsWithRecoveryArgs {
|
|
|
88
88
|
/** The RUNNING install's own `dist/index.js` — the safe re-exec target. */
|
|
89
89
|
entryPath: string;
|
|
90
90
|
stderr: Pick<typeof process.stderr, "write">;
|
|
91
|
+
/** False when command execution already happened and must not be replayed. */
|
|
92
|
+
reexecOnSettled?: boolean;
|
|
91
93
|
deps?: RegisterRecoveryDeps;
|
|
92
94
|
}
|
|
93
95
|
export interface RegisterCommandsWithRecoveryResult {
|
|
@@ -179,6 +179,26 @@ export async function registerCommandsWithRecovery(args) {
|
|
|
179
179
|
const treeState = result.sawLock || result.sawRetired || result.sawTreeChange
|
|
180
180
|
? "in_flight"
|
|
181
181
|
: "stale";
|
|
182
|
+
if (args.reexecOnSettled === false) {
|
|
183
|
+
throw new InstallTreeTornError({
|
|
184
|
+
cause: err,
|
|
185
|
+
classified,
|
|
186
|
+
packageRoot,
|
|
187
|
+
versionGatePhase: readVersionGatePhase(deps),
|
|
188
|
+
outcome: "replay-unsafe",
|
|
189
|
+
treeState,
|
|
190
|
+
attempt: 1,
|
|
191
|
+
waitedMs: result.waitedMs,
|
|
192
|
+
sawLock: result.sawLock,
|
|
193
|
+
sawRetired: result.sawRetired,
|
|
194
|
+
retiredSiblingPresent: result.retiredSiblingPresent,
|
|
195
|
+
retiredSiblingKind: result.retiredSiblingKind,
|
|
196
|
+
installTargetState: result.installTargetState,
|
|
197
|
+
updaterPidAlive: result.updaterPidAlive,
|
|
198
|
+
sawTreeChange: result.sawTreeChange,
|
|
199
|
+
lockTool: result.lockTool,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
182
202
|
const child = spawn(process.execPath, [...execArgv, entryPath, ...argv.slice(2)], {
|
|
183
203
|
stdio: "inherit",
|
|
184
204
|
env: {
|
|
@@ -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
|
|
@@ -336,7 +336,7 @@ export interface InstallRewriteSignalArgs {
|
|
|
336
336
|
*/
|
|
337
337
|
export declare function installRewriteInProgress(args: InstallRewriteSignalArgs): boolean;
|
|
338
338
|
/** The recovery OUTCOME, which — not the dialect — decides capture. */
|
|
339
|
-
export type InstallTreeTornOutcome = "guarded" | "unsettled" | "reexec-failed" | "lock-held" | "stale_tree" | "retired_unverified";
|
|
339
|
+
export type InstallTreeTornOutcome = "guarded" | "replay-unsafe" | "unsettled" | "reexec-failed" | "lock-held" | "stale_tree" | "retired_unverified";
|
|
340
340
|
type InstallTreeState = "in_flight" | "stale";
|
|
341
341
|
export type InstallTreeTornDiagnostics = {
|
|
342
342
|
dialect: ModuleErrorDialect;
|
|
@@ -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"),
|