@indigoai-us/hq-cli 5.274.1 → 5.276.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -4
- package/README.md +12 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
- package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +74 -18
- package/dist/command-catalog.generated.d.ts +34 -1
- package/dist/command-catalog.generated.js +43 -1
- package/dist/commands/cloud.js +22 -9
- package/dist/commands/company.js +66 -3
- package/dist/commands/core-checkpoint-gate-setting.d.ts +69 -0
- package/dist/commands/core-checkpoint-gate-setting.js +163 -0
- package/dist/commands/core-checkpoint.d.ts +4 -2
- package/dist/commands/core-checkpoint.js +34 -3
- package/dist/commands/core-command-catalog.d.ts +4 -2
- package/dist/commands/core-command-catalog.js +5 -2
- package/dist/commands/core-ontology.js +3 -3
- package/dist/commands/core.js +3 -1
- package/dist/commands/daemon.d.ts +1 -0
- package/dist/commands/daemon.js +9 -3
- package/dist/commands/feedback.d.ts +6 -0
- package/dist/commands/feedback.js +86 -37
- package/dist/commands/lanes.js +33 -11
- package/dist/commands/run-feature-telemetry.d.ts +3 -0
- package/dist/commands/run-feature-telemetry.js +6 -0
- package/dist/commands/run.js +3 -2
- package/dist/commands/secrets.js +7 -1
- package/dist/commands/whoami.d.ts +4 -6
- package/dist/commands/whoami.js +39 -8
- package/dist/fast-core.d.ts +6 -7
- package/dist/fast-core.js +22 -8
- package/dist/fast-scaffold.d.ts +16 -7
- package/dist/fast-scaffold.js +13 -8
- package/dist/index.js +16 -1
- package/dist/lib/billing/plan-lock.d.ts +3 -1
- package/dist/lib/billing/plan-lock.js +7 -1
- package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
- package/dist/lib/checkpoint-ontology/company.js +15 -7
- package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
- package/dist/lib/checkpoint-ontology/plan.js +53 -4
- package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
- package/dist/lib/checkpoint-ontology/queued-touches.js +193 -0
- package/dist/lib/core-utils/timeout-guard-native.d.ts +7 -0
- package/dist/lib/core-utils/timeout-guard-native.js +40 -0
- package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
- package/dist/lib/core-utils/timeout-guard.js +28 -13
- package/dist/lib/daemon/catalog.d.ts +7 -0
- package/dist/lib/daemon/catalog.js +6 -0
- package/dist/lib/daemon/flag.d.ts +23 -0
- package/dist/lib/daemon/flag.js +57 -0
- package/dist/lib/daemon/install.js +6 -1
- package/dist/lib/daemon/remote-control.d.ts +6 -0
- package/dist/lib/daemon/remote-control.js +12 -0
- package/dist/lib/daemon/run.js +115 -2
- package/dist/lib/daemon/succession.d.ts +116 -0
- package/dist/lib/daemon/succession.js +262 -0
- package/dist/lib/daemon/supervisor.d.ts +34 -1
- package/dist/lib/daemon/supervisor.js +190 -23
- package/dist/lib/flag-registry.d.ts +12 -7
- package/dist/lib/flag-registry.js +66 -75
- package/dist/lib/flag-snapshot-file.d.ts +20 -0
- package/dist/lib/flag-snapshot-file.js +91 -0
- package/dist/lib/lanes/host-capacity.js +7 -1
- package/dist/lib/lanes/models.d.ts +2 -0
- package/dist/lib/lanes/models.js +6 -1
- package/dist/lib/lanes/reconcile.js +20 -4
- package/dist/lib/process-flag-company-context.d.ts +8 -0
- package/dist/lib/process-flag-company-context.js +67 -0
- package/dist/main.js +3 -1
- package/dist/native-fast-route.d.ts +3 -0
- package/dist/native-fast-route.js +39 -0
- package/dist/utils/cli-telemetry.d.ts +26 -2
- package/dist/utils/cli-telemetry.js +178 -3
- package/dist/utils/hq-root-resolver.d.ts +13 -0
- package/dist/utils/hq-root-resolver.js +103 -0
- package/dist/utils/scaffold-fast-import-guard.js +6 -1
- package/dist/utils/vault-api.d.ts +8 -0
- package/dist/utils/vault-api.js +13 -1
- package/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq core checkpoint-gate-setting` — keeps the local cache that lets a
|
|
3
|
+
* company owner turn the checkpoint Stop gate off for their company (CO-58).
|
|
4
|
+
*
|
|
5
|
+
* The owner sets `companySettings.checkpointGateEnabled` on hq-pro
|
|
6
|
+
* (`hq company settings set --checkpoint-gate-enabled false`, or the console
|
|
7
|
+
* Settings page). The Stop gate script never calls the network: it reads
|
|
8
|
+
* `~/.hq/cache/checkpoint-gate/<slug>.json` and, when that is missing or older
|
|
9
|
+
* than ten minutes, starts this command detached with `--refresh`.
|
|
10
|
+
*
|
|
11
|
+
* `--refresh` reads the setting through the member-readable
|
|
12
|
+
* `GET /company-settings/checkpoint-gate` route and writes the cache only while
|
|
13
|
+
* the hq-flag `checkpoint.gate.company-setting` is on for the company. With the
|
|
14
|
+
* flag off it removes the cache, so the gate keeps today's behaviour. Any
|
|
15
|
+
* failure (no login, API unreachable, timeout) leaves the cache as it was: the
|
|
16
|
+
* gate then uses the last known value, or today's behaviour when there is none.
|
|
17
|
+
* Only the named company's setting is ever read.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import * as os from "node:os";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
23
|
+
import { getEntityUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
24
|
+
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
25
|
+
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
26
|
+
export const CHECKPOINT_GATE_SETTING_FLAG = "checkpoint.gate.company-setting";
|
|
27
|
+
/** The refresh gives up after this long; the gate keeps the cached value. */
|
|
28
|
+
export const CHECKPOINT_GATE_REFRESH_TIMEOUT_MS = 3_000;
|
|
29
|
+
/** Same character set the gate script accepts; anything else is refused. */
|
|
30
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
31
|
+
export function assertCompanySlug(slug) {
|
|
32
|
+
if (!SLUG_RE.test(slug) || slug === "personal") {
|
|
33
|
+
throw new Error(`checkpoint-gate-setting: '${slug}' is not a company slug`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function checkpointGateCacheDir(home = os.homedir()) {
|
|
37
|
+
return path.join(home, ".hq", "cache", "checkpoint-gate");
|
|
38
|
+
}
|
|
39
|
+
export function checkpointGateCachePath(slug, home = os.homedir()) {
|
|
40
|
+
return path.join(checkpointGateCacheDir(home), `${slug}.json`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `readLanesRegistryFlag` resolves a failed registry read to `false`. The
|
|
44
|
+
* refresh must not treat that as "flag off" and delete a valid cache, so a
|
|
45
|
+
* reported failure becomes null (unknown).
|
|
46
|
+
*/
|
|
47
|
+
export async function readCheckpointGateSettingFlag(slug, companyUid) {
|
|
48
|
+
let failed = false;
|
|
49
|
+
const enabled = await readLanesRegistryFlag(CHECKPOINT_GATE_SETTING_FLAG, {
|
|
50
|
+
company: { slug, uid: companyUid },
|
|
51
|
+
reportFailure: () => {
|
|
52
|
+
failed = true;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
return failed ? null : enabled;
|
|
56
|
+
}
|
|
57
|
+
async function defaultFetchSetting(token, companyUid, signal) {
|
|
58
|
+
const res = await vaultApiFetch({
|
|
59
|
+
token,
|
|
60
|
+
path: "/company-settings/checkpoint-gate",
|
|
61
|
+
query: { companyUid },
|
|
62
|
+
signal,
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok)
|
|
65
|
+
throw new Error(`GET /company-settings/checkpoint-gate returned ${res.status}`);
|
|
66
|
+
const body = (await res.json());
|
|
67
|
+
if (typeof body.checkpointGateEnabled !== "boolean") {
|
|
68
|
+
throw new Error("GET /company-settings/checkpoint-gate returned no boolean checkpointGateEnabled");
|
|
69
|
+
}
|
|
70
|
+
return body.checkpointGateEnabled;
|
|
71
|
+
}
|
|
72
|
+
function writeCacheAtomic(file, entry) {
|
|
73
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
74
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
75
|
+
fs.writeFileSync(tmp, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
76
|
+
fs.renameSync(tmp, file);
|
|
77
|
+
}
|
|
78
|
+
function withTimeout(work, ms) {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
let timer;
|
|
81
|
+
const timeout = new Promise((_, reject) => {
|
|
82
|
+
timer = setTimeout(() => {
|
|
83
|
+
controller.abort();
|
|
84
|
+
reject(new Error(`timed out after ${ms} ms`));
|
|
85
|
+
}, ms);
|
|
86
|
+
});
|
|
87
|
+
return Promise.race([work(controller.signal), timeout]).finally(() => clearTimeout(timer));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Refresh one company's cache entry. Never throws for a network or auth
|
|
91
|
+
* failure: those return `kept` and leave the cache untouched.
|
|
92
|
+
*/
|
|
93
|
+
export async function refreshCheckpointGateSetting(slug, deps = {}) {
|
|
94
|
+
assertCompanySlug(slug);
|
|
95
|
+
const file = checkpointGateCachePath(slug, deps.home);
|
|
96
|
+
// Never interactive: a detached refresh must not open a browser login.
|
|
97
|
+
const getToken = deps.getToken ?? (() => ensureCognitoToken({ interactive: false }));
|
|
98
|
+
const resolveCompanyUid = deps.resolveCompanyUid ?? ((token, companySlug) => getEntityUid(token, { companySlug }));
|
|
99
|
+
const fetchSetting = deps.fetchSetting ?? defaultFetchSetting;
|
|
100
|
+
const readFlag = deps.readFlag ?? readCheckpointGateSettingFlag;
|
|
101
|
+
// One timeout covers every network step, the flag read included.
|
|
102
|
+
let fetched;
|
|
103
|
+
try {
|
|
104
|
+
fetched = await withTimeout(async (signal) => {
|
|
105
|
+
const token = await getToken();
|
|
106
|
+
const uid = await resolveCompanyUid(token, slug);
|
|
107
|
+
const enabled = await fetchSetting(token, uid, signal);
|
|
108
|
+
return { enabled, flag: await readFlag(slug, uid) };
|
|
109
|
+
}, deps.timeoutMs ?? CHECKPOINT_GATE_REFRESH_TIMEOUT_MS);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return { status: "kept", company: slug, reason: error instanceof Error ? error.message : String(error) };
|
|
113
|
+
}
|
|
114
|
+
if (fetched.flag === null) {
|
|
115
|
+
return { status: "kept", company: slug, reason: `could not read ${CHECKPOINT_GATE_SETTING_FLAG}` };
|
|
116
|
+
}
|
|
117
|
+
if (!fetched.flag) {
|
|
118
|
+
fs.rmSync(file, { force: true });
|
|
119
|
+
return { status: "removed", company: slug, reason: "flag_off" };
|
|
120
|
+
}
|
|
121
|
+
const now = (deps.now?.() ?? new Date()).getTime();
|
|
122
|
+
writeCacheAtomic(file, { company: slug, checkpointGateEnabled: fetched.enabled, fetched_at: Math.floor(now / 1000) });
|
|
123
|
+
return { status: "written", company: slug, checkpointGateEnabled: fetched.enabled };
|
|
124
|
+
}
|
|
125
|
+
export function describeRefreshOutcome(outcome) {
|
|
126
|
+
switch (outcome.status) {
|
|
127
|
+
case "written":
|
|
128
|
+
return `checkpoint gate for ${outcome.company}: ${outcome.checkpointGateEnabled ? "on" : "off"} (cached)`;
|
|
129
|
+
case "removed":
|
|
130
|
+
return `checkpoint gate for ${outcome.company}: company setting not in use (${CHECKPOINT_GATE_SETTING_FLAG} is off); cache cleared`;
|
|
131
|
+
case "kept":
|
|
132
|
+
return `checkpoint gate for ${outcome.company}: could not refresh (${outcome.reason}); cache unchanged`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export function registerCoreCheckpointGateSettingCommand(core) {
|
|
136
|
+
const command = core
|
|
137
|
+
.command("checkpoint-gate-setting")
|
|
138
|
+
.description("Refresh the local cache of a company's checkpoint gate setting (used by the Stop gate)")
|
|
139
|
+
.requiredOption("--company <slug>", "company whose setting to read")
|
|
140
|
+
.option("--refresh", "read the setting from HQ and update the local cache")
|
|
141
|
+
.option("--json", "print the result as JSON")
|
|
142
|
+
.action(async (options) => {
|
|
143
|
+
try {
|
|
144
|
+
assertCompanySlug(options.company);
|
|
145
|
+
if (!options.refresh) {
|
|
146
|
+
const file = checkpointGateCachePath(options.company);
|
|
147
|
+
const cached = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : null;
|
|
148
|
+
process.stdout.write(`${options.json ? JSON.stringify({ company: options.company, cached }) : cached ? `checkpoint gate for ${options.company}: ${cached.checkpointGateEnabled ? "on" : "off"} (cached at ${new Date(cached.fetched_at * 1000).toISOString()})` : `checkpoint gate for ${options.company}: no cached setting`}\n`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const outcome = await refreshCheckpointGateSetting(options.company);
|
|
152
|
+
process.stdout.write(`${options.json ? JSON.stringify(outcome) : describeRefreshOutcome(outcome)}\n`);
|
|
153
|
+
// A timed-out request can keep sockets open; the answer is already out.
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
describeCoreCommand(command, { execution: "native", root: "cwd" });
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=core-checkpoint-gate-setting.js.map
|
|
@@ -45,8 +45,10 @@ export declare const SIBLING_PROMPT_TEMPLATE = "You are the HQ checkpoint siblin
|
|
|
45
45
|
* Rendered into the sibling prompt only when the payload names a company. The
|
|
46
46
|
* flag check happens inside `hq core ontology plan`, in the sibling, because
|
|
47
47
|
* the checkpoint parent performs no network I/O. Only this run's own payload
|
|
48
|
-
* gets the step
|
|
49
|
-
*
|
|
48
|
+
* gets the step. A queued payload's company files are recorded as queued
|
|
49
|
+
* touches when it is queued (CO-59), and a later plan for that company takes
|
|
50
|
+
* them while checkpoint.ontology.queued-touches is on; a run with no company
|
|
51
|
+
* of its own gets the step for the company whose touches waited longest.
|
|
50
52
|
*/
|
|
51
53
|
export declare const SIBLING_ONTOLOGY_STEP = "5b. ONTOLOGY (this run's own payload only; skip it for claimed queue payloads).\n Run: hq core ontology plan --run-dir \"<runDir>\" --hq-root \"<hqRoot>\"\n If it prints \"ontology: skipped\", do nothing more for this step. Otherwise\n read <runDir>/ontology-plan.json, follow its \"instructions\" exactly, and\n write <runDir>/ontology-candidates.json. Read only the files and line\n ranges the plan lists. Then run:\n hq core ontology apply --run-dir \"<runDir>\" --hq-root \"<hqRoot>\"\n Never write under the company's ontology/ folder yourself, and never edit\n payload.json or ontology-plan.json. Copy the command's one-line output into\n the report. If either command fails, record the error and continue.\n";
|
|
52
54
|
export declare function renderSiblingPrompt(runDir: string, payloadPath: string, options?: {
|
|
@@ -17,6 +17,7 @@ import { toHqStateWriteError } from "../utils/hq-state-write-error.js";
|
|
|
17
17
|
import { checkpointStampWriteErrorFor, isCheckpointStampWriteError, } from "../utils/checkpoint-stamp-write-error.js";
|
|
18
18
|
import { readBackendPreference } from "../lib/checkpoint-ontology/plan.js";
|
|
19
19
|
import { resolveOntologyCompany } from "../lib/checkpoint-ontology/company.js";
|
|
20
|
+
import { markBorrowed, oldestQueuedCompany, recordQueuedTouches } from "../lib/checkpoint-ontology/queued-touches.js";
|
|
20
21
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
21
22
|
const DEFAULT_TRIGGER = "stop-gate";
|
|
22
23
|
const BACKENDS = new Set(["auto", "claude", "codex", "grok", "none"]);
|
|
@@ -189,8 +190,10 @@ report instead of guessing.
|
|
|
189
190
|
* Rendered into the sibling prompt only when the payload names a company. The
|
|
190
191
|
* flag check happens inside `hq core ontology plan`, in the sibling, because
|
|
191
192
|
* the checkpoint parent performs no network I/O. Only this run's own payload
|
|
192
|
-
* gets the step
|
|
193
|
-
*
|
|
193
|
+
* gets the step. A queued payload's company files are recorded as queued
|
|
194
|
+
* touches when it is queued (CO-59), and a later plan for that company takes
|
|
195
|
+
* them while checkpoint.ontology.queued-touches is on; a run with no company
|
|
196
|
+
* of its own gets the step for the company whose touches waited longest.
|
|
194
197
|
*/
|
|
195
198
|
export const SIBLING_ONTOLOGY_STEP = `5b. ONTOLOGY (this run's own payload only; skip it for claimed queue payloads).
|
|
196
199
|
Run: hq core ontology plan --run-dir "<runDir>" --hq-root "<hqRoot>"
|
|
@@ -1001,15 +1004,43 @@ function startSibling(liveRoot, input, threadPath, backend) {
|
|
|
1001
1004
|
fs.mkdirSync(siblingRoot, { recursive: true });
|
|
1002
1005
|
if (activePid !== null) {
|
|
1003
1006
|
const depth = appendPending(pendingPath, payload);
|
|
1007
|
+
try {
|
|
1008
|
+
recordQueuedTouches(liveRoot, payload.company, payload.files, new Date());
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
// Queued touches only help a later ontology plan; they must never fail
|
|
1012
|
+
// the checkpoint itself.
|
|
1013
|
+
}
|
|
1004
1014
|
printResult(`checkpoint: sibling busy — payload queued (${depth} pending)`);
|
|
1005
1015
|
return;
|
|
1006
1016
|
}
|
|
1007
1017
|
const runDir = path.join(siblingRoot, `${formatTimestamp(new Date())}-${summarySlug(input.summary ?? "checkpoint")}`);
|
|
1008
1018
|
fs.mkdirSync(runDir, { recursive: true });
|
|
1009
1019
|
payload.pending_payloads = claimPending(pendingPath, path.join(runDir, "pending-claimed.jsonl"));
|
|
1020
|
+
if (!payload.company) {
|
|
1021
|
+
let queued;
|
|
1022
|
+
try {
|
|
1023
|
+
queued = oldestQueuedCompany(liveRoot);
|
|
1024
|
+
}
|
|
1025
|
+
catch {
|
|
1026
|
+
queued = null;
|
|
1027
|
+
}
|
|
1028
|
+
if (queued) {
|
|
1029
|
+
payload.ontology_company = queued;
|
|
1030
|
+
try {
|
|
1031
|
+
markBorrowed(liveRoot, queued, new Date());
|
|
1032
|
+
}
|
|
1033
|
+
catch {
|
|
1034
|
+
// A borrowed-company marker must never fail the checkpoint.
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1010
1038
|
const payloadPath = path.join(runDir, "payload.json");
|
|
1011
1039
|
fs.writeFileSync(payloadPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
1012
|
-
const prompt = renderSiblingPrompt(runDir, payloadPath, {
|
|
1040
|
+
const prompt = renderSiblingPrompt(runDir, payloadPath, {
|
|
1041
|
+
company: payload.company ?? payload.ontology_company ?? null,
|
|
1042
|
+
hqRoot: liveRoot,
|
|
1043
|
+
});
|
|
1013
1044
|
fs.writeFileSync(path.join(runDir, "prompt.md"), prompt);
|
|
1014
1045
|
const logPath = path.join(runDir, "run.log");
|
|
1015
1046
|
const logFd = fs.openSync(logPath, "a");
|
|
@@ -22,11 +22,13 @@ export declare function describeCoreCommand(command: Command, metadata: CoreComm
|
|
|
22
22
|
export declare function registeredCoreCommandListing(core: Command): CoreCommandListingEntry[];
|
|
23
23
|
export declare function fastScaffoldListingMismatches(listing: readonly CoreCommandListingEntry[], fastScripts: readonly {
|
|
24
24
|
name: string;
|
|
25
|
-
|
|
25
|
+
kind: "shell" | "native";
|
|
26
|
+
asset?: string;
|
|
26
27
|
}[]): string[];
|
|
27
28
|
export declare function assertFastScaffoldListingParity(listing: readonly CoreCommandListingEntry[], fastScripts: readonly {
|
|
28
29
|
name: string;
|
|
29
|
-
|
|
30
|
+
kind: "shell" | "native";
|
|
31
|
+
asset?: string;
|
|
30
32
|
}[]): void;
|
|
31
33
|
export {};
|
|
32
34
|
//# sourceMappingURL=core-command-catalog.d.ts.map
|
|
@@ -43,8 +43,11 @@ export function fastScaffoldListingMismatches(listing, fastScripts) {
|
|
|
43
43
|
if (!listed) {
|
|
44
44
|
mismatches.push(`${fast.name} is missing from hq core commands`);
|
|
45
45
|
}
|
|
46
|
-
else if (listed.asset !== fast.asset) {
|
|
47
|
-
mismatches.push(`${fast.name} lists ${listed.asset ?? "no asset"}, fast path uses ${fast.asset}`);
|
|
46
|
+
else if (fast.kind === "shell" && listed.asset !== fast.asset) {
|
|
47
|
+
mismatches.push(`${fast.name} lists ${listed.asset ?? "no asset"}, fast path uses ${fast.asset ?? "no asset"}`);
|
|
48
|
+
}
|
|
49
|
+
else if (fast.kind === "native" && (listed.execution !== "native" || listed.asset)) {
|
|
50
|
+
mismatches.push(`${fast.name} is native in the fast path but not in the command catalog`);
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
return mismatches;
|
|
@@ -12,7 +12,7 @@ import * as path from "node:path";
|
|
|
12
12
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
13
13
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
14
14
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
15
|
-
import { MULTI_WRITER_FLAG, planOntologyRun, } from "../lib/checkpoint-ontology/plan.js";
|
|
15
|
+
import { MULTI_WRITER_FLAG, planOntologyRun, payloadOntologyCompany, } from "../lib/checkpoint-ontology/plan.js";
|
|
16
16
|
import { applyOntologyRun, ApplyError } from "../lib/checkpoint-ontology/apply.js";
|
|
17
17
|
import { resolveOntologyCompany, WriteBoundsError } from "../lib/checkpoint-ontology/company.js";
|
|
18
18
|
import { findDuplicateGroups, summarizeMergeBack, } from "../lib/checkpoint-ontology/multi-writer.js";
|
|
@@ -85,8 +85,8 @@ export async function runOntologyPlan(hqRoot, runDirOption, deps = {}) {
|
|
|
85
85
|
}
|
|
86
86
|
export function runOntologyApply(hqRoot, runDirOption, deps = {}) {
|
|
87
87
|
const runDir = requireRunDir(runDirOption);
|
|
88
|
-
const company = readPayload(runDir).
|
|
89
|
-
if (
|
|
88
|
+
const company = payloadOntologyCompany(readPayload(runDir)).slug;
|
|
89
|
+
if (company === null) {
|
|
90
90
|
throw new OntologyUsageError("ontology: the run payload names no company");
|
|
91
91
|
}
|
|
92
92
|
const report = applyOntologyRun({ hqRoot, runDir, company, now: deps.now?.() });
|
package/dist/commands/core.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { Option } from "commander";
|
|
32
32
|
import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
|
|
33
33
|
import { registerCoreOntologyCommand } from "./core-ontology.js";
|
|
34
|
+
import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
|
|
34
35
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
35
36
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
36
37
|
import { renderIndexTarget } from "../lib/index-render/index.js";
|
|
@@ -286,6 +287,7 @@ export function registerCoreCommands(program) {
|
|
|
286
287
|
// hosts native TypeScript plumbing when a scaffold contract needs it.
|
|
287
288
|
registerCoreCheckpointCommand(core);
|
|
288
289
|
registerCoreOntologyCommand(core);
|
|
290
|
+
registerCoreCheckpointGateSettingCommand(core);
|
|
289
291
|
// These command names intentionally own native implementations. Their
|
|
290
292
|
// similarly named scaffold assets are forwarders, so sending them through
|
|
291
293
|
// runEntry would recurse back into this command forever.
|
|
@@ -459,7 +461,7 @@ export function registerCoreCommands(program) {
|
|
|
459
461
|
const chunks = [];
|
|
460
462
|
for await (const chunk of process.stdin)
|
|
461
463
|
chunks.push(chunk);
|
|
462
|
-
const code = timeoutGuardCommand(Buffer.concat(chunks).toString("utf8"));
|
|
464
|
+
const code = await timeoutGuardCommand(Buffer.concat(chunks).toString("utf8"));
|
|
463
465
|
if (code !== 0)
|
|
464
466
|
process.exit(code);
|
|
465
467
|
});
|
package/dist/commands/daemon.js
CHANGED
|
@@ -96,6 +96,7 @@ async function loadDefaults(over) {
|
|
|
96
96
|
windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
|
|
97
97
|
windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
|
|
98
98
|
systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
99
|
+
systemRecordPath: over.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
|
|
99
100
|
};
|
|
100
101
|
}
|
|
101
102
|
function detect(d, flag, config) {
|
|
@@ -250,7 +251,7 @@ function migrateDeps(d, paths) {
|
|
|
250
251
|
recordPath: path.join(paths.root, "adopted.json"),
|
|
251
252
|
// Box system units: root-owned, outside the login user's daemon folder (which handOver chowns to that user).
|
|
252
253
|
systemAdoptedDir: path.join(SYSTEM_ADOPTED_ROOT, "adopted"),
|
|
253
|
-
systemRecordPath: path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
|
|
254
|
+
systemRecordPath: d.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
|
|
254
255
|
systemUnitDir: d.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
255
256
|
};
|
|
256
257
|
}
|
|
@@ -489,8 +490,13 @@ function binaryFingerprint(bin) {
|
|
|
489
490
|
}
|
|
490
491
|
}
|
|
491
492
|
async function runTmuxService(d, opts) {
|
|
492
|
-
const { runRemoteControlHost } = await import("../lib/daemon/remote-control.js");
|
|
493
|
+
const { installFingerprint, runRemoteControlHost } = await import("../lib/daemon/remote-control.js");
|
|
493
494
|
const { findOnPath } = await import("../lib/daemon/run.js");
|
|
495
|
+
const { resolveRunningInstall } = await import("../utils/version-gate.js");
|
|
496
|
+
// The daemon keeps this host running across a CLI update, so a new hq-cli
|
|
497
|
+
// install also counts as an update and restarts the session once it is quiet.
|
|
498
|
+
const cliRoot = resolveRunningInstall().packageRoot;
|
|
499
|
+
const watched = cliRoot ? [opts.watch, path.join(cliRoot, "package.json")] : [opts.watch];
|
|
494
500
|
const hqRoot = d.env.HQ_ROOT?.trim();
|
|
495
501
|
if (!hqRoot)
|
|
496
502
|
throw new DaemonCommandError(`${opts.label} needs HQ_ROOT (the daemon sets it from the saved HQ folder)`);
|
|
@@ -508,7 +514,7 @@ async function runTmuxService(d, opts) {
|
|
|
508
514
|
cwd: hqRoot,
|
|
509
515
|
argv: opts.argv,
|
|
510
516
|
label: opts.label,
|
|
511
|
-
binaryFingerprint: () => binaryFingerprint
|
|
517
|
+
binaryFingerprint: () => installFingerprint(watched, binaryFingerprint),
|
|
512
518
|
now: () => Date.now(),
|
|
513
519
|
run: d.run,
|
|
514
520
|
pollMs: 5_000,
|
|
@@ -3,6 +3,8 @@ import { type DiagnosticsBlob } from "../utils/feedback-diagnostics.js";
|
|
|
3
3
|
import { collectFeedbackLogs, feedbackLogsEnabled } from "../utils/feedback-logs.js";
|
|
4
4
|
import { type LogBundleRef } from "../utils/feedback-log-bundle.js";
|
|
5
5
|
export declare const BODY_MAX_BYTES: number;
|
|
6
|
+
/** Validate fields that must be valid before feedback attachments are uploaded. */
|
|
7
|
+
export declare function validateFeedbackContent(title: string, body: string): void;
|
|
6
8
|
/**
|
|
7
9
|
* Mirror of the server's `MAX_BODY_BYTES` in hq-pro
|
|
8
10
|
* `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
|
|
@@ -36,6 +38,10 @@ export interface FeedbackSubmitOptions {
|
|
|
36
38
|
* stays a summary and this carries the rest.
|
|
37
39
|
*/
|
|
38
40
|
logBundle?: LogBundleRef;
|
|
41
|
+
/** Diagnostics collected before uploads by the command path. */
|
|
42
|
+
diagnostics?: DiagnosticsBlob;
|
|
43
|
+
/** Precomputed setting so environment lookup happens before attachments upload. */
|
|
44
|
+
logsEnabled?: boolean;
|
|
39
45
|
}
|
|
40
46
|
export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
|
|
41
47
|
/**
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
|
-
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
4
|
+
import { assertVaultApiRequestSupported, vaultApiFetch, } from "../utils/vault-api.js";
|
|
5
5
|
import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
|
|
6
6
|
import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
|
|
7
7
|
import { FEEDBACK_LOGS_MAX_TOTAL_BYTES, FEEDBACK_LOGS_MIN_USEFUL_BYTES, collectFeedbackLogs, feedbackLogsEnabled, } from "../utils/feedback-logs.js";
|
|
8
8
|
import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
|
|
9
9
|
export const BODY_MAX_BYTES = 64 * 1024;
|
|
10
|
+
/** Validate fields that must be valid before feedback attachments are uploaded. */
|
|
11
|
+
export function validateFeedbackContent(title, body) {
|
|
12
|
+
if (title.trim().length === 0) {
|
|
13
|
+
throw new Error("title must not be empty. Provide a short, non-whitespace title via --title.");
|
|
14
|
+
}
|
|
15
|
+
if (body.trim().length === 0) {
|
|
16
|
+
throw new Error("body must not be empty. Provide at least one non-whitespace character.");
|
|
17
|
+
}
|
|
18
|
+
const bodyBytes = Buffer.byteLength(body, "utf8");
|
|
19
|
+
if (bodyBytes > BODY_MAX_BYTES) {
|
|
20
|
+
throw new Error(`Body exceeds 64 KiB limit (${bodyBytes} bytes). Reduce the body size before submitting.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
10
23
|
/**
|
|
11
24
|
* Mirror of the server's `MAX_BODY_BYTES` in hq-pro
|
|
12
25
|
* `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
|
|
@@ -16,12 +29,36 @@ export const BODY_MAX_BYTES = 64 * 1024;
|
|
|
16
29
|
* logs are budgeted against whatever headroom is left between them.
|
|
17
30
|
*/
|
|
18
31
|
export const REQUEST_MAX_BYTES = 64 * 1024;
|
|
32
|
+
/** Reject JSON envelopes that hq-pro would refuse with HTTP 413. */
|
|
33
|
+
function validateFeedbackRequestSize(requestBody) {
|
|
34
|
+
const requestBytes = Buffer.byteLength(JSON.stringify(requestBody), "utf8");
|
|
35
|
+
if (requestBytes > REQUEST_MAX_BYTES) {
|
|
36
|
+
throw new Error(`Request exceeds 64 KiB limit (${requestBytes} bytes). Reduce the feedback content before submitting.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
19
39
|
/**
|
|
20
40
|
* Headroom held back for the `"logs":{...}` JSON envelope itself — its keys,
|
|
21
41
|
* braces, and the escaping of the tail text. The final size is re-measured
|
|
22
42
|
* after attaching, so this reserve only needs to be roughly right.
|
|
23
43
|
*/
|
|
24
44
|
const LOGS_ENVELOPE_RESERVE_BYTES = 1024;
|
|
45
|
+
function buildFeedbackRequestBody(opts) {
|
|
46
|
+
const requestBody = {
|
|
47
|
+
type: opts.type,
|
|
48
|
+
title: opts.title,
|
|
49
|
+
body: opts.body,
|
|
50
|
+
diagnostics: opts.diagnostics,
|
|
51
|
+
};
|
|
52
|
+
if (opts.company) {
|
|
53
|
+
requestBody.company = opts.company;
|
|
54
|
+
}
|
|
55
|
+
if (opts.screenshots && opts.screenshots.length > 0) {
|
|
56
|
+
requestBody.screenshots = opts.screenshots;
|
|
57
|
+
}
|
|
58
|
+
if (opts.logBundle)
|
|
59
|
+
requestBody.logBundle = opts.logBundle;
|
|
60
|
+
return requestBody;
|
|
61
|
+
}
|
|
25
62
|
export async function readBodyFile(bodyFile, stdin) {
|
|
26
63
|
if (bodyFile === "-") {
|
|
27
64
|
const stream = stdin ?? process.stdin;
|
|
@@ -79,48 +116,32 @@ export function attachDebugLogs(requestBody, diagnostics, enabled, deps = {}) {
|
|
|
79
116
|
}
|
|
80
117
|
}
|
|
81
118
|
export async function submitFeedback(opts) {
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// warning (HQ-AB). Catch it here so the caller gets a clear, actionable error
|
|
88
|
-
// and the bad request never reaches the server.
|
|
89
|
-
if (opts.title.trim().length === 0) {
|
|
90
|
-
throw new Error("title must not be empty. Provide a short, non-whitespace title via --title.");
|
|
91
|
-
}
|
|
92
|
-
if (opts.body.trim().length === 0) {
|
|
93
|
-
throw new Error("body must not be empty. Provide at least one non-whitespace character.");
|
|
94
|
-
}
|
|
95
|
-
const bodyBytes = Buffer.byteLength(opts.body, "utf8");
|
|
96
|
-
if (bodyBytes > BODY_MAX_BYTES) {
|
|
97
|
-
throw new Error(`Body exceeds 64 KiB limit (${bodyBytes} bytes). Reduce the body size before submitting.`);
|
|
98
|
-
}
|
|
99
|
-
const diagnostics = collectDiagnostics();
|
|
100
|
-
const requestBody = {
|
|
119
|
+
// Keep submitFeedback safe for direct callers as well as the command path,
|
|
120
|
+
// which invokes the same validator before uploading any attachments.
|
|
121
|
+
validateFeedbackContent(opts.title, opts.body);
|
|
122
|
+
const diagnostics = opts.diagnostics ?? collectDiagnostics();
|
|
123
|
+
const requestBody = buildFeedbackRequestBody({
|
|
101
124
|
type: opts.type,
|
|
102
125
|
title: opts.title,
|
|
103
126
|
body: opts.body,
|
|
127
|
+
company: opts.company,
|
|
104
128
|
diagnostics,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
// The bundle reference is four small fields, so unlike the inline logs below
|
|
113
|
-
// it needs no budgeting — but it is added BEFORE attachDebugLogs so those
|
|
114
|
-
// bytes are counted against the inline budget rather than overrunning it.
|
|
115
|
-
if (opts.logBundle) {
|
|
116
|
-
requestBody.logBundle = opts.logBundle;
|
|
117
|
-
}
|
|
129
|
+
screenshots: opts.screenshots,
|
|
130
|
+
logBundle: opts.logBundle,
|
|
131
|
+
});
|
|
132
|
+
// Direct callers can supply attachment references; ensure the complete base
|
|
133
|
+
// envelope still fits before adding optional inline logs or making a request.
|
|
134
|
+
validateFeedbackRequestSize(requestBody);
|
|
118
135
|
// Debug logs are attached LAST and sized against the headroom the rest of
|
|
119
136
|
// the request left behind. The server 413s the whole body over 64 KiB, so a
|
|
120
137
|
// fixed log budget would turn a previously-working large-body submission
|
|
121
138
|
// into a hard failure. Measuring the real serialized envelope — the exact
|
|
122
139
|
// bytes vaultApiFetch will send — is what makes that impossible.
|
|
123
|
-
attachDebugLogs(requestBody, diagnostics, opts.includeLogs !== false
|
|
140
|
+
attachDebugLogs(requestBody, diagnostics, opts.includeLogs !== false, {
|
|
141
|
+
envEnabled: opts.logsEnabled === undefined
|
|
142
|
+
? feedbackLogsEnabled
|
|
143
|
+
: () => opts.logsEnabled === true,
|
|
144
|
+
});
|
|
124
145
|
const res = await vaultApiFetch({
|
|
125
146
|
token: opts.token,
|
|
126
147
|
path: "/v1/feedback",
|
|
@@ -155,8 +176,34 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
155
176
|
try {
|
|
156
177
|
const token = await ensureCognitoToken({ interactive: false });
|
|
157
178
|
const body = await readBodyFile(opts.bodyFile);
|
|
158
|
-
|
|
159
|
-
//
|
|
179
|
+
validateFeedbackContent(opts.title, body);
|
|
180
|
+
// Resolve every required local input before creating S3 objects. These
|
|
181
|
+
// diagnostics are passed through so submitFeedback does not collect
|
|
182
|
+
// them again after attachments have been uploaded.
|
|
183
|
+
const diagnostics = collectDiagnostics();
|
|
184
|
+
const logsEnabled = opts.logs !== false && feedbackLogsEnabled();
|
|
185
|
+
// Validate the serialized server request envelope before screenshots
|
|
186
|
+
// or the log bundle can create remote objects. Attachment references
|
|
187
|
+
// are added and checked again inside submitFeedback after upload.
|
|
188
|
+
validateFeedbackRequestSize(buildFeedbackRequestBody({
|
|
189
|
+
type,
|
|
190
|
+
title: opts.title,
|
|
191
|
+
body,
|
|
192
|
+
company: opts.company,
|
|
193
|
+
diagnostics,
|
|
194
|
+
}));
|
|
195
|
+
// vaultApiFetch can reject an hqk_ token or an invalid API base URL
|
|
196
|
+
// locally before fetch. Check the mandatory POST, and the screenshot
|
|
197
|
+
// presign when requested, before any upload can create an orphan.
|
|
198
|
+
assertVaultApiRequestSupported({ token, path: "/v1/feedback" });
|
|
199
|
+
if ((opts.screenshot ?? []).length > 0) {
|
|
200
|
+
assertVaultApiRequestSupported({
|
|
201
|
+
token,
|
|
202
|
+
path: "/v1/feedback/screenshots/presign",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Upload screenshots (direct-to-S3 via presigned PUT) before the
|
|
206
|
+
// submission, so the row references uploaded objects.
|
|
160
207
|
const screenshots = await uploadScreenshots({
|
|
161
208
|
paths: opts.screenshot ?? [],
|
|
162
209
|
token,
|
|
@@ -167,7 +214,7 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
167
214
|
// this resolves undefined and the report still carries inline logs.
|
|
168
215
|
const logBundle = await uploadLogBundle({
|
|
169
216
|
token,
|
|
170
|
-
enabled:
|
|
217
|
+
enabled: logsEnabled,
|
|
171
218
|
});
|
|
172
219
|
const result = await submitFeedback({
|
|
173
220
|
type,
|
|
@@ -178,6 +225,8 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
178
225
|
screenshots,
|
|
179
226
|
includeLogs: opts.logs !== false,
|
|
180
227
|
logBundle,
|
|
228
|
+
diagnostics,
|
|
229
|
+
logsEnabled,
|
|
181
230
|
});
|
|
182
231
|
console.log(`Submitted: ${result.id}`);
|
|
183
232
|
}
|