@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +12 -0
  3. package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
  4. package/dist/commands/core-checkpoint.d.ts +4 -2
  5. package/dist/commands/core-checkpoint.js +34 -3
  6. package/dist/commands/core-command-catalog.d.ts +4 -2
  7. package/dist/commands/core-command-catalog.js +5 -2
  8. package/dist/commands/core-ontology.js +3 -3
  9. package/dist/commands/core.js +1 -1
  10. package/dist/commands/daemon.d.ts +1 -0
  11. package/dist/commands/daemon.js +2 -1
  12. package/dist/commands/feedback.d.ts +6 -0
  13. package/dist/commands/feedback.js +86 -37
  14. package/dist/commands/lanes.js +3 -2
  15. package/dist/commands/run-feature-telemetry.d.ts +3 -0
  16. package/dist/commands/run-feature-telemetry.js +6 -0
  17. package/dist/commands/run.js +3 -2
  18. package/dist/commands/secrets.js +7 -1
  19. package/dist/commands/whoami.d.ts +4 -6
  20. package/dist/commands/whoami.js +39 -8
  21. package/dist/fast-core.d.ts +6 -7
  22. package/dist/fast-core.js +22 -8
  23. package/dist/fast-scaffold.d.ts +16 -7
  24. package/dist/fast-scaffold.js +13 -8
  25. package/dist/index.js +16 -1
  26. package/dist/lib/billing/plan-lock.d.ts +3 -1
  27. package/dist/lib/billing/plan-lock.js +7 -1
  28. package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
  29. package/dist/lib/checkpoint-ontology/company.js +15 -7
  30. package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
  31. package/dist/lib/checkpoint-ontology/plan.js +53 -4
  32. package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
  33. package/dist/lib/checkpoint-ontology/queued-touches.js +193 -0
  34. package/dist/lib/core-utils/timeout-guard-native.d.ts +7 -0
  35. package/dist/lib/core-utils/timeout-guard-native.js +40 -0
  36. package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
  37. package/dist/lib/core-utils/timeout-guard.js +28 -13
  38. package/dist/lib/flag-registry.d.ts +12 -7
  39. package/dist/lib/flag-registry.js +66 -75
  40. package/dist/lib/flag-snapshot-file.d.ts +20 -0
  41. package/dist/lib/flag-snapshot-file.js +91 -0
  42. package/dist/lib/lanes/reconcile.js +20 -4
  43. package/dist/lib/process-flag-company-context.d.ts +8 -0
  44. package/dist/lib/process-flag-company-context.js +67 -0
  45. package/dist/main.js +3 -1
  46. package/dist/native-fast-route.d.ts +3 -0
  47. package/dist/native-fast-route.js +39 -0
  48. package/dist/utils/cli-telemetry.d.ts +26 -2
  49. package/dist/utils/cli-telemetry.js +178 -3
  50. package/dist/utils/hq-root-resolver.d.ts +13 -0
  51. package/dist/utils/hq-root-resolver.js +103 -0
  52. package/dist/utils/scaffold-fast-import-guard.js +6 -1
  53. package/dist/utils/vault-api.d.ts +8 -0
  54. package/dist/utils/vault-api.js +13 -1
  55. 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, and it does not include integrations
327
- or MCP, Atlas, or telemetry. Everything else — knowledge, projects, policies,
328
- workers, secrets, vault storage, `hq deploy`, and `hq share` — works the same as
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; claimed queue payloads are covered by the next run, since the
49
- * plan works from file hashes rather than from the payload's file list.
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; claimed queue payloads are covered by the next run, since the
193
- * plan works from file hashes rather than from the payload's file list.
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, { company: payload.company, hqRoot: liveRoot });
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
- asset: string;
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
- asset: string;
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).company;
89
- if (typeof company !== "string" || company === "") {
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?.() });
@@ -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
  });
@@ -41,6 +41,7 @@ export interface DaemonCommandDeps {
41
41
  windowsUser?: string;
42
42
  windowsHeadlessLauncher?: string;
43
43
  systemUnitDir?: string;
44
+ systemRecordPath?: string;
44
45
  }
45
46
  /**
46
47
  * The HQ folder install records: --hq-root, else the CLI's shared resolution
@@ -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
- // Validate the title locally, symmetric with the body check below. Commander's
83
- // `requiredOption("--title")` only requires the flag to be PRESENT — an empty
84
- // or whitespace-only value (`--title ""`, or a title the /hq-bug skill derived
85
- // to nothing) passes the flag check, then the server rejects it with a 400
86
- // "title (non-empty string) is required" that floods Sentry as a context-free
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
- if (opts.company) {
107
- requestBody.company = opts.company;
108
- }
109
- if (opts.screenshots && opts.screenshots.length > 0) {
110
- requestBody.screenshots = opts.screenshots;
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
- // Validate + upload screenshots (direct-to-S3 via presigned PUT)
159
- // before submitting, so the row references uploaded objects.
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: opts.logs !== false && feedbackLogsEnabled(),
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
  }
@@ -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,3 @@
1
+ /** Attribute hq run's batch secret read to its resolved company UID. */
2
+ export declare function withRunSecretReadTelemetry<T>(companyUid: string, action: () => Promise<T>): Promise<T>;
3
+ //# sourceMappingURL=run-feature-telemetry.d.ts.map
@@ -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
@@ -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,
@@ -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 revealed = await loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
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
- * Human rendering of the plan state: the full over-limit block when the
25
- * workspace is over its Starter limits, the one-line `Plan: Starter — …`
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