@indigoai-us/hq-cli 5.108.24 → 5.108.26

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 (36) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/commands/files.d.ts +11 -0
  3. package/dist/commands/files.js +206 -30
  4. package/dist/commands/integrations-api.d.ts +15 -0
  5. package/dist/commands/integrations-connect.js +84 -3
  6. package/dist/commands/integrations-oauth.js +62 -3
  7. package/dist/commands/mcp-registration.d.ts +17 -7
  8. package/dist/commands/mcp-registration.js +16 -27
  9. package/dist/commands/mesh.js +174 -50
  10. package/dist/commands/pack-install.js +5 -5
  11. package/dist/commands/secrets.d.ts +7 -0
  12. package/dist/commands/secrets.js +26 -2
  13. package/dist/lib/mesh/live/backfill-held.d.ts +42 -1
  14. package/dist/lib/mesh/live/backfill-held.js +95 -13
  15. package/dist/lib/mesh/live/daemon/doctor.d.ts +15 -0
  16. package/dist/lib/mesh/live/daemon/doctor.js +41 -10
  17. package/dist/lib/mesh/live/daemon/mode.d.ts +37 -0
  18. package/dist/lib/mesh/live/daemon/mode.js +88 -0
  19. package/dist/lib/mesh/live/daemon/run.d.ts +8 -0
  20. package/dist/lib/mesh/live/daemon/run.js +39 -28
  21. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  22. package/dist/lib/mesh/live/emit-client.d.ts +99 -0
  23. package/dist/lib/mesh/live/emit-client.js +193 -0
  24. package/dist/lib/mesh/live/emit-evidence.d.ts +49 -0
  25. package/dist/lib/mesh/live/emit-evidence.js +77 -0
  26. package/dist/lib/mesh/live/emit-replay.d.ts +26 -0
  27. package/dist/lib/mesh/live/emit-replay.js +157 -0
  28. package/dist/lib/mesh/live/emit-retry.d.ts +25 -0
  29. package/dist/lib/mesh/live/emit-retry.js +79 -0
  30. package/dist/lib/mesh/live/emit.d.ts +54 -0
  31. package/dist/lib/mesh/live/emit.js +153 -0
  32. package/dist/lib/narrow-hint-banner.d.ts +3 -7
  33. package/dist/lib/narrow-hint-banner.js +13 -34
  34. package/dist/lib/plan-limit-nag.d.ts +0 -3
  35. package/dist/lib/plan-limit-nag.js +10 -20
  36. package/package.json +1 -1
@@ -52,7 +52,6 @@
52
52
  * first-class skip), and {@link registerMcpServers} is the pack-install routing
53
53
  * seam over it.
54
54
  */
55
- import { type FlagReader } from '../lib/flag-registry.js';
56
55
  /** Base for every MCP-registration error; carries a stable machine-checkable `code`. */
57
56
  export declare abstract class McpRegistrationError extends Error {
58
57
  abstract readonly code: string;
@@ -596,10 +595,17 @@ export declare function registerServer(opts: RegisterServerOptions): RegisterSer
596
595
  * when neither a url nor a command is present.
597
596
  */
598
597
  export declare function manifestTarget(manifest: McpManifest): string;
599
- /** Resolve the gate once at the start of an MCP registration operation. */
600
- export declare function isMcpRegistrationEnabled(flagReader?: FlagReader): boolean;
601
- /** Freeze a registration decision so multi-server work cannot split on refresh. */
602
- export declare function captureMcpRegistrationDecision(flagReader?: FlagReader): FlagReader;
598
+ /**
599
+ * Resolve the MCP registration kill switch from its env var alone.
600
+ *
601
+ * `HQ_DISABLE_MCP_REGISTRATION` is an operator's own machine-local opt-out, not
602
+ * a rollout flag, so it is read straight from the environment — no registry
603
+ * lookup. The polarity is inverted and the value match is asymmetric on
604
+ * purpose: ONLY the exact value "1" disables registration; unset and every
605
+ * other value (including "0" and "false") leave it enabled. Do not "tidy" this
606
+ * into a boolean parse — that would silently disable anyone who wrote "false".
607
+ */
608
+ export declare function isMcpRegistrationEnabled(env?: Readonly<Record<string, string | undefined>>): boolean;
603
609
  /**
604
610
  * Register one pack's MCP servers into the shared agent configs (the public seam
605
611
  * `pack-install` routes `wire:'merge'` keys to). For each declared server name it
@@ -627,8 +633,12 @@ export declare function registerMcpServers(pkg: string, names: string[], options
627
633
  /** Lock tuning + backup stamp passthrough (tests). */
628
634
  lock?: AcquireLockOptions;
629
635
  stamp?: string;
630
- /** Test seam: held registry snapshot reader. */
631
- flagReader?: FlagReader;
636
+ /**
637
+ * Pre-resolved kill-switch decision, frozen once before a multi-server loop
638
+ * so an install writes either all of a pack's servers or none of them.
639
+ * Omitted → read `HQ_DISABLE_MCP_REGISTRATION` directly.
640
+ */
641
+ registrationEnabled?: boolean;
632
642
  }): RegisterServerResult[];
633
643
  /** smol-toml's value-table type (its `parse` return + `stringify` input). */
634
644
  type TomlTable = Record<string, unknown>;
@@ -57,7 +57,6 @@ import * as fs from 'fs';
57
57
  import * as os from 'os';
58
58
  import * as path from 'path';
59
59
  import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
60
- import { resolveFlagGate, resolveProcessFlagGate, } from '../lib/flag-registry.js';
61
60
  // ---------------------------------------------------------------------------
62
61
  // Named error classes (no bare catch-all anywhere in this module).
63
62
  //
@@ -1226,28 +1225,18 @@ export function manifestTarget(manifest) {
1226
1225
  }
1227
1226
  return manifest.url ?? '';
1228
1227
  }
1229
- /** Registry vocabulary for the legacy MCP operator kill switch. */
1230
- const MCP_REGISTRATION_LOOKUP = {
1231
- globalEnvVar: 'HQ_DISABLE_MCP_REGISTRATION',
1232
- globalValueSemantics: {
1233
- onValues: [],
1234
- offValues: ['1'],
1235
- unrecognizedValue: true,
1236
- unsetValue: true,
1237
- },
1238
- fallback: true,
1239
- };
1240
- const mcpRegistrationLegacyFallback = () => process.env.HQ_DISABLE_MCP_REGISTRATION !== '1';
1241
- /** Resolve the gate once at the start of an MCP registration operation. */
1242
- export function isMcpRegistrationEnabled(flagReader) {
1243
- return flagReader
1244
- ? resolveFlagGate(flagReader, 'cli.mcp-registration', MCP_REGISTRATION_LOOKUP, mcpRegistrationLegacyFallback)
1245
- : resolveProcessFlagGate('cli.mcp-registration', MCP_REGISTRATION_LOOKUP, mcpRegistrationLegacyFallback);
1246
- }
1247
- /** Freeze a registration decision so multi-server work cannot split on refresh. */
1248
- export function captureMcpRegistrationDecision(flagReader) {
1249
- const enabled = isMcpRegistrationEnabled(flagReader);
1250
- return { isEnabled: () => enabled };
1228
+ /**
1229
+ * Resolve the MCP registration kill switch from its env var alone.
1230
+ *
1231
+ * `HQ_DISABLE_MCP_REGISTRATION` is an operator's own machine-local opt-out, not
1232
+ * a rollout flag, so it is read straight from the environment — no registry
1233
+ * lookup. The polarity is inverted and the value match is asymmetric on
1234
+ * purpose: ONLY the exact value "1" disables registration; unset and every
1235
+ * other value (including "0" and "false") leave it enabled. Do not "tidy" this
1236
+ * into a boolean parse — that would silently disable anyone who wrote "false".
1237
+ */
1238
+ export function isMcpRegistrationEnabled(env = process.env) {
1239
+ return env.HQ_DISABLE_MCP_REGISTRATION !== '1';
1251
1240
  }
1252
1241
  /**
1253
1242
  * Register one pack's MCP servers into the shared agent configs (the public seam
@@ -1275,14 +1264,14 @@ export function registerMcpServers(pkg, names, options) {
1275
1264
  // ORDERING: this is checked FIRST, BEFORE the loadManifest programmer-error guard
1276
1265
  // below. The kill-switch is a USER/OPERATOR condition; the missing-loadManifest
1277
1266
  // throw is a PROGRAMMER error. An operator who set the kill-switch must never hit a
1278
- // spurious McpManifestError, so the operator path wins. Its legacy environment
1279
- // predicate remains the outage fallback; `options.env` is the SafeWriteEnv
1280
- // home-path injector, not process env.
1267
+ // spurious McpManifestError, so the operator path wins. The decision is read
1268
+ // straight from `HQ_DISABLE_MCP_REGISTRATION`; `options.env` is the
1269
+ // SafeWriteEnv home-path injector, not the process env this switch reads.
1281
1270
  //
1282
1271
  // Returns an empty RegisterServerResult[] (`[]`) — the correct "no servers
1283
1272
  // registered" semantic — so callers (pack-install) consume it gracefully. The skip
1284
1273
  // NOTICE is emitted exactly once per registerMcpServers call.
1285
- if (!isMcpRegistrationEnabled(options?.flagReader)) {
1274
+ if (!(options?.registrationEnabled ?? isMcpRegistrationEnabled())) {
1286
1275
  process.stderr.write('MCP registration skipped (HQ_DISABLE_MCP_REGISTRATION=1)\n');
1287
1276
  return [];
1288
1277
  }
@@ -14,10 +14,17 @@ import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, cre
14
14
  import { clearDefaultCompany, getDefaultCompany, readDeviceConfig, recordMigrationCapabilitySnapshot, setDefaultCompany, } from "../lib/work-context/config.js";
15
15
  import { DefaultCompanyLockedError, DefaultCompanyUnavailableError, } from "../lib/work-context/errors.js";
16
16
  import { isValidSessionId } from "../lib/mesh/live/session-identity.js";
17
- import { CLI_KIND_TO_SCHEMA, EnqueueValidationError, enqueueSessionEvent, } from "../lib/mesh/live/index.js";
17
+ import { CLI_KIND_TO_SCHEMA, resolveEnqueueSessionId, } from "../lib/mesh/live/index.js";
18
18
  import { flushSessionEvents } from "../lib/mesh/live/flush.js";
19
19
  import { backfillHeldSessions } from "../lib/mesh/live/backfill-held.js";
20
20
  import { createSessionEventsPoster, resolveVaultApiBase, } from "../lib/mesh/live/session-events-client.js";
21
+ import { buildEmitEvidence } from "../lib/mesh/live/emit-evidence.js";
22
+ import { createEmitPoster, buildMeshEmitEvent } from "../lib/mesh/live/emit-client.js";
23
+ import { emitEvents } from "../lib/mesh/live/emit.js";
24
+ import { readEmitRetry, writeEmitRetry } from "../lib/mesh/live/emit-retry.js";
25
+ import { generateUlid } from "../lib/mesh/live/ulid.js";
26
+ import { readLegacyBacklog } from "../lib/mesh/live/emit-replay.js";
27
+ import { resolveMeshEmitMode, writeMeshConfigMode, } from "../lib/mesh/live/daemon/mode.js";
21
28
  import { workMeshRoot } from "../lib/mesh/live/paths.js";
22
29
  import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
23
30
  import { workContextRoot } from "../lib/work-context/paths.js";
@@ -319,6 +326,9 @@ async function runContextBackfillHeld(opts) {
319
326
  workContextRoot: root,
320
327
  dryRun,
321
328
  limit,
329
+ // Mirror the reconcile closure's non-forcing hint in the dry-run predictor
330
+ // so `--dry-run` and the real run report the same counts.
331
+ remoteOwnerHint: companyHint || undefined,
322
332
  reconcile: (obs) => reconcileObservation(
323
333
  // Pass --company as a non-forcing hint (remoteOwnerSlug feeds
324
334
  // deterministic resolution BELOW the identity-file / device default,
@@ -725,9 +735,9 @@ function workMeshHomeRoot() {
725
735
  return workMeshRoot(os.homedir(), process.env);
726
736
  }
727
737
  async function runSessionEnqueue(cliKind, opts) {
728
- if (!opts.enqueue) {
729
- fail("`hq mesh session …` requires --enqueue (local spool append; no network).");
730
- }
738
+ // `--enqueue` is retained for hook back-compat but now means "emit directly"
739
+ // (owner decision 2026-09-08: sessions POST events to the server over HTTPS;
740
+ // no local spool/daemon on the emit path).
731
741
  const kind = CLI_KIND_TO_SCHEMA[cliKind];
732
742
  if (!kind)
733
743
  fail(`Unknown session verb: ${cliKind}`);
@@ -741,57 +751,155 @@ async function runSessionEnqueue(cliKind, opts) {
741
751
  const seq = Number(opts.seq);
742
752
  if (!Number.isInteger(seq) || seq < 1)
743
753
  fail("--seq must be an integer >= 1");
744
- let toolWrites;
745
- if (opts.toolWrites !== undefined) {
746
- toolWrites = Number(opts.toolWrites);
747
- if (!Number.isInteger(toolWrites) || toolWrites < 0) {
748
- fail("--tool-writes must be an integer >= 0");
754
+ const sessionId = resolveEnqueueSessionId(opts.sessionId, process.env);
755
+ if (!sessionId)
756
+ fail("sessionId required (--session-id or HQ_SESSION_ID)");
757
+ const evidence = buildEmitEvidence({
758
+ sessionId: sessionId,
759
+ hqRoot: opts.hqRoot,
760
+ cwd: opts.cwd,
761
+ touchedPaths: opts.touchedPath,
762
+ repoPath: opts.repoPath,
763
+ companySlug: opts.companySlug,
764
+ project: opts.project,
765
+ task: opts.task,
766
+ });
767
+ const event = buildMeshEmitEvent({
768
+ eventId: opts.eventId?.trim() || generateUlid(Date.now()),
769
+ kind: kind,
770
+ sessionId: sessionId,
771
+ harness,
772
+ adapterVersion,
773
+ at: opts.at?.trim() || new Date().toISOString(),
774
+ seq,
775
+ runtimeVersion: opts.runtimeVersion,
776
+ source: "hooks",
777
+ taskId: opts.taskId,
778
+ status: opts.status,
779
+ reason: opts.reason,
780
+ summary: opts.summary,
781
+ evidence,
782
+ });
783
+ const meshRoot = workMeshHomeRoot();
784
+ // Resolve a token non-interactively (same path as every CLI call). If the box
785
+ // is not logged in, retain the event locally for the next invocation to drain.
786
+ let token;
787
+ try {
788
+ token = await requireToken();
789
+ }
790
+ catch {
791
+ token = null;
792
+ }
793
+ if (!token) {
794
+ const pending = readEmitRetry(meshRoot);
795
+ const { written } = writeEmitRetry(meshRoot, [...pending, event]);
796
+ if (opts.json) {
797
+ console.log(JSON.stringify({ ok: true, action: "emit", kind, deferred: true, retryDepth: written }, null, 2));
798
+ return;
749
799
  }
800
+ console.error(`work-mesh: no token; deferred ${kind} (retryDepth=${written})`);
801
+ return;
750
802
  }
751
- try {
752
- const result = enqueueSessionEvent({
753
- kind: kind,
754
- sessionId: opts.sessionId,
755
- harness,
756
- adapterVersion,
757
- runtimeVersion: opts.runtimeVersion,
758
- seq,
759
- eventId: opts.eventId,
760
- at: opts.at,
761
- taskId: opts.taskId,
762
- status: opts.status,
763
- reason: opts.reason,
764
- summary: opts.summary,
765
- cwd: opts.cwd,
766
- hqRoot: opts.hqRoot,
767
- companySlug: opts.companySlug,
768
- project: opts.project,
769
- task: opts.task,
770
- toolWrites,
771
- root: workMeshHomeRoot(),
772
- env: process.env,
773
- });
803
+ const poster = createEmitPoster({
804
+ token,
805
+ baseUrl: resolveVaultApiBase(process.env),
806
+ });
807
+ const summary = await emitEvents({
808
+ workMeshRoot: meshRoot,
809
+ poster,
810
+ newEvents: [event],
811
+ });
812
+ if (opts.json) {
813
+ console.log(JSON.stringify({ ok: true, action: "emit", kind, ...summary }, null, 2));
814
+ return;
815
+ }
816
+ console.error(`work-mesh: emit ${kind} accepted=${summary.accepted}` +
817
+ ` unassigned=${summary.unassigned} rejected=${summary.rejected}` +
818
+ ` retryDepth=${summary.retryDepth}`);
819
+ }
820
+ async function runMeshMode(action, opts) {
821
+ const meshRoot = workMeshHomeRoot();
822
+ const act = (action ?? "get").toLowerCase();
823
+ if (act === "get") {
824
+ const mode = resolveMeshEmitMode({ env: process.env, meshRoot });
774
825
  if (opts.json) {
775
- console.log(JSON.stringify({
776
- ok: true,
777
- action: "enqueue",
778
- kind,
779
- spoolPath: result.spoolPath,
780
- eventId: result.event.eventId,
781
- }, null, 2));
826
+ console.log(JSON.stringify({ ok: true, action: "mode", mode }, null, 2));
782
827
  return;
783
828
  }
784
- // Quiet success for hooks/scripts — eventId on stderr only when not json.
785
- console.error(`work-mesh: enqueued ${kind} ${result.event.eventId}`);
829
+ console.log(`mesh emit mode: ${mode}`);
830
+ return;
786
831
  }
787
- catch (err) {
788
- if (err instanceof EnqueueValidationError) {
789
- console.error(chalk.red(`enqueue validation failed: ${err.message}`));
790
- process.exitCode = 1;
832
+ if (act === "legacy" || act === "direct") {
833
+ writeMeshConfigMode(meshRoot, act);
834
+ const mode = resolveMeshEmitMode({ env: process.env, meshRoot });
835
+ if (opts.json) {
836
+ console.log(JSON.stringify({ ok: true, action: "mode", set: act, effective: mode }, null, 2));
791
837
  return;
792
838
  }
793
- throw err;
839
+ console.log(`mesh emit mode set to ${act}` +
840
+ (mode !== act
841
+ ? ` (note: HQ_MESH_MODE=${process.env.HQ_MESH_MODE} overrides → ${mode})`
842
+ : "") +
843
+ `. Restart the daemon (hq mesh daemon run) to apply.`);
844
+ return;
845
+ }
846
+ if (act === "check") {
847
+ // Probe the server route with an empty batch: 2xx/400 => route live,
848
+ // 404 => not deployed yet (safe to stay legacy).
849
+ let token;
850
+ try {
851
+ token = await requireToken();
852
+ }
853
+ catch (err) {
854
+ fail(err instanceof Error ? err.message : String(err));
855
+ return;
856
+ }
857
+ const poster = createEmitPoster({
858
+ token,
859
+ baseUrl: resolveVaultApiBase(process.env),
860
+ });
861
+ const res = await poster([]);
862
+ const ready = res.status === 200 || res.status === 400;
863
+ if (opts.json) {
864
+ console.log(JSON.stringify({ ok: true, action: "mode", check: true, status: res.status, ready }, null, 2));
865
+ return;
866
+ }
867
+ console.log(`mesh route /v1/mesh/events: status=${res.status} ${ready ? "READY (safe to `hq mesh mode direct`)" : "not ready (stay legacy)"}`);
868
+ return;
869
+ }
870
+ fail(`Unknown mode action: ${act} (use get|legacy|direct|check)`);
871
+ }
872
+ async function runMeshEmit(opts) {
873
+ const token = await requireToken();
874
+ const meshRoot = workMeshHomeRoot();
875
+ const poster = createEmitPoster({
876
+ token,
877
+ baseUrl: resolveVaultApiBase(process.env),
878
+ });
879
+ let scanned = 0;
880
+ let skipped = 0;
881
+ let newEvents = [];
882
+ if (opts.replayLegacy) {
883
+ const backlog = readLegacyBacklog(meshRoot);
884
+ scanned = backlog.scanned;
885
+ skipped = backlog.skipped;
886
+ newEvents = backlog.events;
887
+ }
888
+ const summary = await emitEvents({ workMeshRoot: meshRoot, poster, newEvents });
889
+ const out = {
890
+ ok: true,
891
+ action: "emit",
892
+ ...(opts.replayLegacy ? { replayLegacy: true, legacyScanned: scanned, legacySkipped: skipped } : {}),
893
+ ...summary,
894
+ };
895
+ if (opts.json) {
896
+ console.log(JSON.stringify(out, null, 2));
897
+ return;
794
898
  }
899
+ console.log(`work-mesh emit: attempted=${summary.attempted} accepted=${summary.accepted}` +
900
+ ` unassigned=${summary.unassigned} rejected=${summary.rejected}` +
901
+ ` retryDepth=${summary.retryDepth}` +
902
+ (opts.replayLegacy ? ` (legacy scanned=${scanned} skipped=${skipped})` : ""));
795
903
  }
796
904
  async function runSessionFlush(opts) {
797
905
  const token = await requireToken();
@@ -829,10 +937,12 @@ function addSessionEnqueueFlags(cmd) {
829
937
  .option("--summary <text>", "Short summary (max 280)")
830
938
  .option("--cwd <path>", "Local-only working directory")
831
939
  .option("--hq-root <path>", "Local-only HQ tree root")
832
- .option("--company-slug <slug>", "Local-only company slug hint")
833
- .option("--project <name>", "Local-only project hint")
834
- .option("--task <label>", "Local-only task label hint")
835
- .option("--tool-writes <n>", "Local-only tool write count")
940
+ .option("--company-slug <slug>", "Attribution evidence: bound company slug")
941
+ .option("--project <name>", "Attribution evidence: project id/slug")
942
+ .option("--task <label>", "Attribution evidence: task id/label")
943
+ .option("--touched-path <path>", "Attribution evidence: a file path this tool call touched (repeatable)", collectRepeatable)
944
+ .option("--repo-path <path>", "Attribution evidence: enclosing repo path")
945
+ .option("--tool-writes <n>", "Local-only tool write count (unused on emit)")
836
946
  .option("--json", "Print machine-readable JSON");
837
947
  }
838
948
  function wrap(action) {
@@ -1012,6 +1122,20 @@ export function registerMeshCommand(program) {
1012
1122
  .description("Claim spool/held by rename, hold or drop by context state, POST batches of ≤100")
1013
1123
  .option("--json", "Print machine-readable JSON")
1014
1124
  .action((opts) => wrap(() => runSessionFlush(opts))());
1125
+ mesh
1126
+ .command("mode")
1127
+ .description("Get/set the daemon emit mode (legacy = spool/flush; direct = receive-only). " +
1128
+ "`check` probes whether the server route is live.")
1129
+ .argument("[action]", "get | legacy | direct | check (default get)")
1130
+ .option("--json", "Print machine-readable JSON")
1131
+ .action((action, opts) => wrap(() => runMeshMode(action, opts))());
1132
+ mesh
1133
+ .command("emit")
1134
+ .description("Direct-emit drain: POST any deferred events (retry file) to /v1/mesh/events; " +
1135
+ "--replay-legacy also replays the legacy spool/held backlog through the new endpoint")
1136
+ .option("--replay-legacy", "Also replay spool.jsonl + held.jsonl with their evidence")
1137
+ .option("--json", "Print machine-readable JSON")
1138
+ .action((opts) => wrap(() => runMeshEmit(opts))());
1015
1139
  session
1016
1140
  .command("status")
1017
1141
  .description("Print the company-wide live read (US-004) as a table or --json")
@@ -52,7 +52,7 @@ import { safeExtractTarball } from './safe-extract.js';
52
52
  import { getCompanyUid, vaultApiFetch, vaultApiFetchPublic, } from '../utils/vault-api.js';
53
53
  import { ensureCognitoToken } from '../utils/cognito-session.js';
54
54
  import { formatApiKeyCapabilityDenial, peekHqApiKey, } from '../utils/resolve-vault-credential.js';
55
- import { redactSecrets, SECRET_REDACTION, captureMcpRegistrationDecision, registerMcpServers, McpManifestError, } from './mcp-registration.js';
55
+ import { redactSecrets, SECRET_REDACTION, isMcpRegistrationEnabled, registerMcpServers, McpManifestError, } from './mcp-registration.js';
56
56
  import { listSecretCacheScopes } from '../utils/secrets-cache.js';
57
57
  import { loadRevealedSecrets } from './secrets.js';
58
58
  const PACK_UPDATE_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
@@ -1492,9 +1492,9 @@ async function wireMcpServers(pkg, destDir, company) {
1492
1492
  const loadManifest = (name) => loadMcpManifestFrom(destDir, name);
1493
1493
  const registered = [];
1494
1494
  const skipped = [];
1495
- // A refresh may arrive while secret resolution awaits. Freeze the gate before
1496
- // the loop so an install never writes only an arbitrary prefix of a pack.
1497
- const flagReader = captureMcpRegistrationDecision();
1495
+ // Resolve the kill-switch once and freeze it before the loop so an install
1496
+ // never writes only an arbitrary prefix of a pack's servers.
1497
+ const registrationEnabled = isMcpRegistrationEnabled();
1498
1498
  for (const name of pkg.contributes.mcp ?? []) {
1499
1499
  try {
1500
1500
  const resolveSecret = await makeInstallSecretResolver(collectManifestSecretNames(loadManifest(name)), company);
@@ -1503,7 +1503,7 @@ async function wireMcpServers(pkg, destDir, company) {
1503
1503
  const results = registerMcpServers(pkg.name, [name], {
1504
1504
  loadManifest,
1505
1505
  resolveSecret,
1506
- flagReader,
1506
+ registrationEnabled,
1507
1507
  });
1508
1508
  if (results.length > 0)
1509
1509
  registered.push(name);
@@ -3,6 +3,13 @@ import { writeSync } from "node:fs";
3
3
  import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
4
4
  export type { VaultApiOptions } from "../utils/vault-api.js";
5
5
  export { vaultApiFetch, getCompanyUid, getEntityUid };
6
+ /**
7
+ * Mirrors `RESOURCE_SURFACES` in hq-pro
8
+ * `src/journey/limited-resource-telemetry.ts`. Keep this list in sync with
9
+ * the server so CLI telemetry cannot silently fall back to `api`.
10
+ */
11
+ export declare const HQ_PRO_RESOURCE_SURFACES: readonly ["api", "bot_invite", "bot_join_now", "calendar_auto_schedule", "calendar_preferences", "cli_secrets_set", "console_secrets_form", "deploy_precheck", "external_connection", "factory_install", "integrations_connect", "oauth_callback", "plan_limit_check", "secrets_input_link", "web"];
12
+ export declare const CLI_SECRETS_SET_SURFACE = "cli_secrets_set";
6
13
  export type SecretTier = "standard" | "sensitive" | "nuclear";
7
14
  export type SecretScriptLockMode = "off" | "enforced";
8
15
  export type SecretUsageChannel = "run" | "exec" | "env" | "sandbox" | "reveal" | "submit-link";
@@ -12,6 +12,29 @@ import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from
12
12
  import { HQ_API_KEY_PREFIX, formatApiKeyCapabilityDenial, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
13
13
  import { SandboxRunnerClient, } from "../utils/sandbox-runner-client.js";
14
14
  export { vaultApiFetch, getCompanyUid, getEntityUid };
15
+ /**
16
+ * Mirrors `RESOURCE_SURFACES` in hq-pro
17
+ * `src/journey/limited-resource-telemetry.ts`. Keep this list in sync with
18
+ * the server so CLI telemetry cannot silently fall back to `api`.
19
+ */
20
+ export const HQ_PRO_RESOURCE_SURFACES = [
21
+ "api",
22
+ "bot_invite",
23
+ "bot_join_now",
24
+ "calendar_auto_schedule",
25
+ "calendar_preferences",
26
+ "cli_secrets_set",
27
+ "console_secrets_form",
28
+ "deploy_precheck",
29
+ "external_connection",
30
+ "factory_install",
31
+ "integrations_connect",
32
+ "oauth_callback",
33
+ "plan_limit_check",
34
+ "secrets_input_link",
35
+ "web",
36
+ ];
37
+ export const CLI_SECRETS_SET_SURFACE = "cli_secrets_set";
15
38
  /**
16
39
  * Cognito session for secrets surfaces hq-pro serves on the JWT routes only —
17
40
  * the capability would apply, but there is no Bearer `hqk_` route (no HEAD
@@ -829,9 +852,10 @@ export function registerSecretsCommand(program) {
829
852
  body: {
830
853
  name,
831
854
  value,
855
+ surface: CLI_SECRETS_SET_SURFACE,
832
856
  // Only present when --high-security was passed — an ordinary
833
- // `set` with no flags sends exactly `{ name, value }`, byte-for-
834
- // byte unchanged from before this story.
857
+ // `set` with no flags sends only its name/value plus this additive
858
+ // telemetry attribution field.
835
859
  ...(opts.highSecurity ? { highSecurity: true } : {}),
836
860
  ...(destinations ? { destinations } : {}),
837
861
  ...(injection ? { injection } : {}),
@@ -21,10 +21,28 @@
21
21
  */
22
22
  import type { ReconcileObservation, ReconcileOutcome } from "../../work-context/reconcile.js";
23
23
  import { type SessionStateFile } from "../../work-context/state.js";
24
- /** One distinct session discovered in held.jsonl, with its harness. */
24
+ /**
25
+ * One distinct session discovered in held.jsonl, carrying the company evidence
26
+ * its held events recorded at enqueue time (cwd / hqRoot / companySlug /
27
+ * project / task). ENDED sessions never reconcile again, so this per-event
28
+ * evidence is the only company signal a backfill can use — the synthesized
29
+ * observation forwards it so the shared resolver (cwd → companies/{slug}/,
30
+ * meta.yaml company_slug via hqRoot, or the event's own bound companySlug)
31
+ * can attribute the backlog instead of holding it forever.
32
+ */
25
33
  export interface HeldSessionRef {
26
34
  sessionId: string;
27
35
  harness?: string;
36
+ /** Working directory the events were emitted from (deterministic company/project evidence). */
37
+ cwd?: string;
38
+ /** HQ root, so the resolver can read workspace/sessions/<sid>/meta.yaml. */
39
+ hqRoot?: string;
40
+ /** Company the session was bound to at emit time (enqueue --company-slug). */
41
+ companySlug?: string;
42
+ /** Project id carried on the event, if any. */
43
+ project?: string;
44
+ /** Task id carried on the event, if any. */
45
+ task?: string;
28
46
  }
29
47
  export interface BackfillHeldResult {
30
48
  /** Distinct sessions found in held.jsonl (before --limit). */
@@ -63,9 +81,32 @@ export interface BackfillHeldDeps {
63
81
  * `hq mesh context reconcile` handler.
64
82
  */
65
83
  reconcile: (obs: ReconcileObservation) => Promise<ReconcileOutcome>;
84
+ /**
85
+ * Non-forcing company hint (the `--company` flag). Fed to the resolver as
86
+ * deterministic remote-owner evidence (below identity / explicit / session
87
+ * meta) in BOTH the dry-run predictor and the write path, so the two agree.
88
+ */
89
+ remoteOwnerHint?: string;
90
+ /** Env passed to the dry-run resolver (tests). Default process.env. */
91
+ env?: NodeJS.ProcessEnv;
92
+ /**
93
+ * Read-only company predictor for --dry-run. MUST mirror what the write
94
+ * path's reconcile would resolve, so dry-run and write report the same
95
+ * counts (the earlier bug: dry-run counted every session as would-reconcile
96
+ * while the write path, given no evidence, resolved none). Default runs the
97
+ * shared `resolveCompany` over the same reconstructed evidence.
98
+ */
99
+ predictCompany?: (ref: HeldSessionRef) => boolean;
66
100
  /** Optional progress logger. */
67
101
  log?: (message: string) => void;
68
102
  }
103
+ /**
104
+ * Build the reconcile observation for one held session, forwarding the company
105
+ * evidence its events recorded (cwd / hqRoot / bound companySlug / project /
106
+ * task). The event's own companySlug is trusted explicit — it is the company
107
+ * the session was bound to when the events were emitted on this box.
108
+ */
109
+ export declare function observationFromHeldRef(ref: HeldSessionRef, contractVersion: number, clientOperationId: string): ReconcileObservation;
69
110
  /**
70
111
  * Read held.jsonl and return distinct sessions (first occurrence wins),
71
112
  * carrying the harness from whichever held event we saw first for that session.