@indigoai-us/hq-cli 5.275.0 → 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 +36 -0
- package/README.md +12 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
- 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 +1 -1
- package/dist/commands/daemon.d.ts +1 -0
- package/dist/commands/daemon.js +2 -1
- package/dist/commands/feedback.d.ts +6 -0
- package/dist/commands/feedback.js +86 -37
- package/dist/commands/lanes.js +3 -2
- 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/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/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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.276.0] — 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Suggest inviting a teammate after the billing link in an over-limit Starter
|
|
10
|
+
notice for a one-member company, behind the default-off
|
|
11
|
+
`cli.limit-invite-teammate-v1` flag.
|
|
12
|
+
- Add a default-off native route for `hq core timeout-guard`, backed by a
|
|
13
|
+
private company flag snapshot and a paired CI latency benchmark.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Starter setup guidance now says it includes one integration.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `hq core checkpoint` no longer loses the company files of a checkpoint that
|
|
22
|
+
was queued behind a busy sibling, behind the `checkpoint.ontology.queued-touches`
|
|
23
|
+
flag (off by default). The queued checkpoint records its company files, and
|
|
24
|
+
the next sibling run adds them to the ontology plan. A run whose own
|
|
25
|
+
checkpoint names no company takes the ontology step for the company whose
|
|
26
|
+
files have waited longest.
|
|
27
|
+
- `hq feedback bug|feature` validates report content and the serialized request
|
|
28
|
+
size before uploading screenshots or logs, avoiding orphaned attachments when
|
|
29
|
+
the server would reject the submission.
|
|
30
|
+
|
|
31
|
+
- Reconcile clears a stale `superseded` keep-alive block when no active successor
|
|
32
|
+
remains; the five-minute minimum retry delay still applies. The block is
|
|
33
|
+
cleared even when the lead then waits on backoff or a full queue, and the
|
|
34
|
+
clear does not move a done lead's retry clock past its done envelope.
|
|
35
|
+
|
|
5
36
|
## [5.275.0] — 2026-09-28
|
|
6
37
|
|
|
7
38
|
### Fixed
|
|
@@ -13,6 +44,9 @@
|
|
|
13
44
|
|
|
14
45
|
### Added
|
|
15
46
|
|
|
47
|
+
- Track CLI feature events behind the default-off `cli.feature-used-telemetry-v1`
|
|
48
|
+
hq-flags key with a separate one-time disclosure.
|
|
49
|
+
|
|
16
50
|
- Outpost daemons can take over running services after a CLI update instead
|
|
17
51
|
of restarting all of them, behind the `daemon.supervisor-handoff` flag (off
|
|
18
52
|
by default). On Linux, the updated daemon adopts the services the previous
|
|
@@ -45,6 +79,8 @@
|
|
|
45
79
|
`outpost_self_target`.
|
|
46
80
|
- `hq sync status` validates an explicit `--hq-root` and continues to read
|
|
47
81
|
journals from the configured state directory, which it now prints.
|
|
82
|
+
- Root daemon tests use a temporary system adopted-record file; the CLI still
|
|
83
|
+
defaults to `/var/lib/hq/daemon/adopted.json`.
|
|
48
84
|
|
|
49
85
|
## [5.274.1] — 2026-09-28
|
|
50
86
|
|
package/README.md
CHANGED
|
@@ -47,6 +47,18 @@ plane can accept the event. The body does not include command names, arguments,
|
|
|
47
47
|
timings, file contents, or secrets. The guidance fields are sent only when that
|
|
48
48
|
optional feature is enabled and a person sees or follows its next step.
|
|
49
49
|
|
|
50
|
+
When the default-off hq-flags key `cli.feature-used-telemetry-v1` is enabled,
|
|
51
|
+
the CLI sends `cli_feature_used` for supported secret reads (`hq secrets exec`
|
|
52
|
+
and `hq run`) and lane create and resume actions. Each event carries
|
|
53
|
+
`properties.kind` and `properties.outcome` (`success` or `failure`). Events also
|
|
54
|
+
carry the authenticated company UID when available. hq-pro resolves the signed-in
|
|
55
|
+
identity. Events omit command names and arguments, secret names and values,
|
|
56
|
+
prompt text, file paths, and command lines. User hooks and skills run in the
|
|
57
|
+
provider host, which hq-cli cannot see. The default signed-in notice above is
|
|
58
|
+
unchanged. On interactive TTY runs, the first enabled feature event has a
|
|
59
|
+
separate one-time notice naming the feature kind, outcome, and company UID
|
|
60
|
+
when available.
|
|
61
|
+
|
|
50
62
|
The first signed-in interactive run (stdout is a TTY, and the command is not
|
|
51
63
|
`--json`) prints this line once on stderr and records that it was shown under
|
|
52
64
|
`~/.hq/cli-telemetry-disclosed`:
|
|
@@ -323,10 +323,10 @@ HQ/
|
|
|
323
323
|
HQ has three plans: **Starter**, **HQ Workforce** ($500/month per workspace), and
|
|
324
324
|
**Enterprise**.
|
|
325
325
|
|
|
326
|
-
Starter is free. It holds up to 5 members
|
|
327
|
-
|
|
328
|
-
workers, secrets, vault storage, `hq deploy`, and `hq share` — works
|
|
329
|
-
on the paid plans.
|
|
326
|
+
Starter is free. It holds up to 5 members and includes 1 integration. MCP,
|
|
327
|
+
Atlas, and telemetry are not included. Everything else — knowledge, projects,
|
|
328
|
+
policies, workers, secrets, vault storage, `hq deploy`, and `hq share` — works
|
|
329
|
+
the same as on the paid plans.
|
|
330
330
|
|
|
331
331
|
### When a workspace goes over
|
|
332
332
|
|
|
@@ -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
|
@@ -461,7 +461,7 @@ export function registerCoreCommands(program) {
|
|
|
461
461
|
const chunks = [];
|
|
462
462
|
for await (const chunk of process.stdin)
|
|
463
463
|
chunks.push(chunk);
|
|
464
|
-
const code = timeoutGuardCommand(Buffer.concat(chunks).toString("utf8"));
|
|
464
|
+
const code = await timeoutGuardCommand(Buffer.concat(chunks).toString("utf8"));
|
|
465
465
|
if (code !== 0)
|
|
466
466
|
process.exit(code);
|
|
467
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
|
}
|
|
@@ -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
|
}
|
package/dist/commands/lanes.js
CHANGED
|
@@ -27,6 +27,7 @@ import { registerLaneReportCommands } from "./lanes-report.js";
|
|
|
27
27
|
import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
|
|
28
28
|
import { registerLaneReviewCommand } from "./lanes-review.js";
|
|
29
29
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
30
|
+
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
30
31
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
31
32
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
32
33
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
@@ -6911,7 +6912,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6911
6912
|
.option("--placement <mode>", "local | auto | outpost:<id>")
|
|
6912
6913
|
.option("--default-placement <mode>", "Default placement for lanes launched by this lane: local | auto | outpost:<id>")
|
|
6913
6914
|
.action(wrap(async (opts) => {
|
|
6914
|
-
await runCreate(ctx, opts);
|
|
6915
|
+
await withCliFeatureUsed("lane_create", () => runCreate(ctx, opts));
|
|
6915
6916
|
}));
|
|
6916
6917
|
lanes
|
|
6917
6918
|
.command("approve-target", { hidden: true })
|
|
@@ -7215,7 +7216,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7215
7216
|
.option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
|
|
7216
7217
|
.option("--no-compact", "Skip the resume-time compaction check for this resume")
|
|
7217
7218
|
.action(wrap(async (laneId, opts) => {
|
|
7218
|
-
await resumeHandlers.runResume(ctx, laneId, opts);
|
|
7219
|
+
await withCliFeatureUsed("lane_resume", () => resumeHandlers.runResume(ctx, laneId, opts));
|
|
7219
7220
|
}));
|
|
7220
7221
|
lanes
|
|
7221
7222
|
.command("reparent")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { withCliFeatureUsed } from '../utils/cli-telemetry.js';
|
|
2
|
+
/** Attribute hq run's batch secret read to its resolved company UID. */
|
|
3
|
+
export function withRunSecretReadTelemetry(companyUid, action) {
|
|
4
|
+
return withCliFeatureUsed('secret_read', action, companyUid);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=run-feature-telemetry.js.map
|
package/dist/commands/run.js
CHANGED
|
@@ -11,6 +11,7 @@ import { discoverSchemas } from '../run/discover-schemas.js';
|
|
|
11
11
|
import { installHqPlugin, prewarmHqSecrets } from '../run/hq-plugin.js';
|
|
12
12
|
import { assertEnvGraphLoaded, resolveEnvValuesOrThrow, EnvGraphLoadError, EnvResolutionError, } from '../run/env-graph-guard.js';
|
|
13
13
|
import { hadFatalRejection } from '../unhandled-rejection-boundary.js';
|
|
14
|
+
import { withRunSecretReadTelemetry } from './run-feature-telemetry.js';
|
|
14
15
|
const SECRET_LOAD_TIMEOUT_MS = 30_000;
|
|
15
16
|
export async function buildRunUsage(scriptPath, scriptId) {
|
|
16
17
|
if (scriptId && !scriptPath) {
|
|
@@ -87,7 +88,7 @@ export function registerRunCommand(program) {
|
|
|
87
88
|
allowOfflineScopeFallback: true,
|
|
88
89
|
});
|
|
89
90
|
const usage = await buildRunUsage(opts.script, opts.scriptId);
|
|
90
|
-
const fetchBatch = async (companyUid, names, requestUsage) => {
|
|
91
|
+
const fetchBatch = async (companyUid, names, requestUsage) => withRunSecretReadTelemetry(companyUid, async () => {
|
|
91
92
|
const res = await vaultApiFetch({
|
|
92
93
|
token,
|
|
93
94
|
path: `/secrets/${encodeURIComponent(companyUid)}/load`,
|
|
@@ -113,7 +114,7 @@ export function registerRunCommand(program) {
|
|
|
113
114
|
throw new Error(`Failed to batch-load secrets: ${message}`);
|
|
114
115
|
}
|
|
115
116
|
return res.json();
|
|
116
|
-
};
|
|
117
|
+
});
|
|
117
118
|
const pluginOpts = {
|
|
118
119
|
companyOverride: opts.company,
|
|
119
120
|
principalUid: cognitoCachePrincipalUid(token) ?? undefined,
|
package/dist/commands/secrets.js
CHANGED
|
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { writeSync } from "node:fs";
|
|
5
5
|
import * as nodePath from "node:path";
|
|
6
6
|
import { cognitoCachePrincipalUid, ensureCognitoToken } from "../utils/cognito-session.js";
|
|
7
|
+
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
7
8
|
import { DEFAULT_SECRETS_CACHE_TTL_MS, hasCacheEntry, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
|
|
8
9
|
import { computeSha256 } from "../utils/integrity.js";
|
|
9
10
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
|
|
@@ -1498,9 +1499,14 @@ export function registerSecretsCommand(program) {
|
|
|
1498
1499
|
const companyUid = cred.kind === "api-key"
|
|
1499
1500
|
? "__api_key__"
|
|
1500
1501
|
: await getEntityUid(cred.token, scopeOpts(secrets.opts()), { allowOfflineScopeFallback: true });
|
|
1501
|
-
const
|
|
1502
|
+
const readSecrets = async () => loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
|
|
1502
1503
|
? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
|
|
1503
1504
|
: undefined, { allowOfflineFallback: true });
|
|
1505
|
+
// API-key auth has no signed-in person identity for the CLI telemetry
|
|
1506
|
+
// endpoint. Do not attribute that read to an unrelated cached session.
|
|
1507
|
+
const revealed = cred.kind === "cognito"
|
|
1508
|
+
? await withCliFeatureUsed("secret_read", readSecrets, companyUid)
|
|
1509
|
+
: await readSecrets();
|
|
1504
1510
|
const secretEnv = {};
|
|
1505
1511
|
for (const key of keys) {
|
|
1506
1512
|
const value = revealed.get(key);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { type PlanLimitStatus } from "../lib/billing/plan-lock.js";
|
|
6
|
+
export declare const CLI_LIMIT_INVITE_TEAMMATE_FLAG = "cli.limit-invite-teammate-v1";
|
|
6
7
|
export interface WhoamiTokenIdentity {
|
|
7
8
|
email?: string;
|
|
8
9
|
sub?: string;
|
|
@@ -20,11 +21,8 @@ export interface WhoamiDisplayIdentity {
|
|
|
20
21
|
* email with the token subject of the delegating machine.
|
|
21
22
|
*/
|
|
22
23
|
export declare function resolveWhoamiIdentity(identity: WhoamiTokenIdentity): WhoamiDisplayIdentity;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* orientation when it is Starter and healthy, and nothing at all otherwise.
|
|
27
|
-
*/
|
|
28
|
-
export declare function renderWhoamiPlanBlock(status: PlanLimitStatus): string | null;
|
|
24
|
+
export declare function renderWhoamiPlanBlock(status: PlanLimitStatus, opts?: {
|
|
25
|
+
inviteTeammateEnabled?: boolean;
|
|
26
|
+
}): string | null;
|
|
29
27
|
export declare function registerWhoamiCommand(program: Command): void;
|
|
30
28
|
//# sourceMappingURL=whoami.d.ts.map
|