@indigoai-us/hq-cli 5.274.0 → 5.275.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 (33) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +74 -18
  3. package/dist/command-catalog.generated.d.ts +34 -1
  4. package/dist/command-catalog.generated.js +43 -1
  5. package/dist/commands/cloud.js +22 -9
  6. package/dist/commands/company.js +66 -3
  7. package/dist/commands/core-checkpoint-gate-setting.d.ts +69 -0
  8. package/dist/commands/core-checkpoint-gate-setting.js +163 -0
  9. package/dist/commands/core.js +2 -0
  10. package/dist/commands/daemon.js +7 -2
  11. package/dist/commands/lanes-message.js +40 -7
  12. package/dist/commands/lanes-shared.d.ts +4 -0
  13. package/dist/commands/lanes-shared.js +5 -0
  14. package/dist/commands/lanes.js +41 -17
  15. package/dist/lib/daemon/catalog.d.ts +7 -0
  16. package/dist/lib/daemon/catalog.js +6 -0
  17. package/dist/lib/daemon/flag.d.ts +23 -0
  18. package/dist/lib/daemon/flag.js +57 -0
  19. package/dist/lib/daemon/install.js +6 -1
  20. package/dist/lib/daemon/remote-control.d.ts +6 -0
  21. package/dist/lib/daemon/remote-control.js +12 -0
  22. package/dist/lib/daemon/run.js +115 -2
  23. package/dist/lib/daemon/succession.d.ts +116 -0
  24. package/dist/lib/daemon/succession.js +262 -0
  25. package/dist/lib/daemon/supervisor.d.ts +34 -1
  26. package/dist/lib/daemon/supervisor.js +190 -23
  27. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -1
  28. package/dist/lib/lanes/hire-grant-dispatch.js +37 -2
  29. package/dist/lib/lanes/host-capacity.js +7 -1
  30. package/dist/lib/lanes/keep-alive.js +2 -2
  31. package/dist/lib/lanes/models.d.ts +2 -0
  32. package/dist/lib/lanes/models.js +6 -1
  33. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,58 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.275.0] — 2026-09-28
6
+
7
+ ### Fixed
8
+
9
+ - `hq lanes` accepts full Claude model ids such as `claude-sonnet-5-5` and
10
+ `claude-opus-5-5` (spawn, `resume --from-failed`, account moves) when
11
+ `claude --help` names only the aliases. Before, the model check refused them
12
+ with `model_unsupported`.
13
+
14
+ ### Added
15
+
16
+ - Outpost daemons can take over running services after a CLI update instead
17
+ of restarting all of them, behind the `daemon.supervisor-handoff` flag (off
18
+ by default). On Linux, the updated daemon adopts the services the previous
19
+ one left running and replaces only the ones whose command changed, one at a
20
+ time. Bots finish their running turns first. hq-cloud services keep running,
21
+ and the remote-control hosts restart their session once it is quiet. This
22
+ needs the systemd unit from this release: run `sudo hq daemon install` again
23
+ on each outpost.
24
+ - Company owners can turn the checkpoint Stop gate off or on for their company:
25
+ `hq company settings set --checkpoint-gate-enabled false|true`. Any member can
26
+ read the value with `hq company settings get`. The Stop gate reads a local
27
+ cache (`~/.hq/cache/checkpoint-gate/<company>.json`, refreshed in the
28
+ background at most every ten minutes by `hq core checkpoint-gate-setting
29
+ --refresh`) and skips the checkpoint block only when the session's company has
30
+ the gate turned off. Unset, unreachable with no cache, and sessions with no
31
+ company keep today's behaviour; `HQ_CHECKPOINT_GATE` still wins. Gated by the
32
+ hq-flag `checkpoint.gate.company-setting` (default off).
33
+
34
+ ### Changed
35
+
36
+ - The systemd system unit now uses `KillMode=process`, so systemd signals only
37
+ the daemon. The daemon still stops its services on `systemctl stop`, and it
38
+ stops any service left over from a crashed daemon before starting new ones.
39
+
40
+ ### Fixed
41
+
42
+ - `hq lanes create --placement auto` now runs on the caller when it is the best
43
+ capacity candidate. It matches the host id, machine identity or registered
44
+ Outpost id. Explicit `--outpost` self-targets still return
45
+ `outpost_self_target`.
46
+ - `hq sync status` validates an explicit `--hq-root` and continues to read
47
+ journals from the configured state directory, which it now prints.
48
+
49
+ ## [5.274.1] — 2026-09-28
50
+
51
+ ### Fixed
52
+
53
+ - With lineage-aware supersession enabled, same-profile workers under the same
54
+ session senior no longer supersede each other. The recorded senior can
55
+ message its worker if it is later superseded.
56
+
5
57
  ## [5.274.0] — 2026-09-28
6
58
 
7
59
  ### Fixed
@@ -137,6 +137,72 @@ exec 3>&2
137
137
  return 0
138
138
  }
139
139
 
140
+ # checkpoint_gate_bound_company <hq-root> <session-id> — the session's
141
+ # declared company slug: scope-capability.json first, then meta.yaml. Prints
142
+ # empty when the session has no binding.
143
+ checkpoint_gate_bound_company() {
144
+ local root="$1" sid="$2" co="" cap meta
145
+ [ -n "$sid" ] || return 0
146
+ cap="$root/workspace/sessions/$sid/scope-capability.json"
147
+ if [ -r "$cap" ]; then
148
+ co="$(jq -r '.company_slug // empty' "$cap" 2>/dev/null || true)"
149
+ fi
150
+ if [ -z "$co" ]; then
151
+ meta="$root/workspace/sessions/$sid/meta.yaml"
152
+ if [ -r "$meta" ]; then
153
+ co="$(awk '
154
+ $1 == "company_slug:" {
155
+ sub(/^[^:]+:[[:space:]]*/, "")
156
+ gsub(/^"|"$/, "")
157
+ print
158
+ exit
159
+ }
160
+ ' "$meta" 2>/dev/null || true)"
161
+ fi
162
+ fi
163
+ printf '%s' "$co"
164
+ }
165
+
166
+ # checkpoint_gate_company_off <hq-root> <session-id> — succeeds only when the
167
+ # session's company owner has turned the checkpoint gate off
168
+ # (companySettings.checkpointGateEnabled = false) according to the local
169
+ # cache written by `hq core checkpoint-gate-setting --refresh`. The hook
170
+ # never calls the network: a missing or stale cache starts one detached,
171
+ # rate-limited refresh and the current turn uses whatever the cache holds.
172
+ # On, unset, no cache, no company, `personal`, or anything unreadable keeps
173
+ # the gate on. The refresh removes the cache while the
174
+ # `checkpoint.gate.company-setting` hq-flag is off, so the setting has no
175
+ # effect until the flag is on.
176
+ checkpoint_gate_company_off() {
177
+ local co dir cache stamp value fetched now stamp_mtime
178
+ co="$(checkpoint_gate_bound_company "$1" "$2")"
179
+ case "$co" in
180
+ ""|personal) return 1 ;;
181
+ *[!a-z0-9-]*) return 1 ;;
182
+ esac
183
+ [ -n "${HOME:-}" ] || return 1
184
+ dir="${HOME}/.hq/cache/checkpoint-gate"
185
+ cache="$dir/$co.json"
186
+ stamp="$dir/$co.refresh"
187
+ value=""
188
+ fetched=""
189
+ if [ -r "$cache" ]; then
190
+ value="$(jq -r 'if .checkpointGateEnabled == false then "off" elif .checkpointGateEnabled == true then "on" else "" end' "$cache" 2>/dev/null || true)"
191
+ fetched="$(jq -r '.fetched_at // empty | numbers | floor' "$cache" 2>/dev/null || true)"
192
+ fi
193
+ now="$(date +%s 2>/dev/null || true)"
194
+ case "$now" in ''|*[!0-9]*) now="" ;; esac
195
+ case "$fetched" in ''|*[!0-9]*) fetched=0 ;; esac
196
+ if [ -n "$now" ] && [ $((now - fetched)) -gt 600 ] && command -v hq >/dev/null 2>&1; then
197
+ stamp_mtime="$(stat -c %Y "$stamp" 2>/dev/null || stat -f %m "$stamp" 2>/dev/null || printf 0)"
198
+ case "$stamp_mtime" in ''|*[!0-9]*) stamp_mtime=0 ;; esac
199
+ if [ $((now - stamp_mtime)) -gt 600 ] && mkdir -p "$dir" 2>/dev/null && touch "$stamp" 2>/dev/null; then
200
+ (nohup hq core checkpoint-gate-setting --refresh --company "$co" >/dev/null 2>&1 3>&- &)
201
+ fi
202
+ fi
203
+ [ "$value" = off ]
204
+ }
205
+
140
206
  input="$(cat 2>/dev/null || printf '{}')"
141
207
  input_reply_evidence="$(printf '%s' "$input" | jq -r '
142
208
  if has("last_assistant_message") then
@@ -201,24 +267,7 @@ exec 3>&2
201
267
  fi
202
268
  if [ "$gate_match" = 1 ]; then
203
269
  gate_root="${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$self_hq}}"
204
- bound_co=""
205
- gate_cap="$gate_root/workspace/sessions/$gate_session_id/scope-capability.json"
206
- if [ -r "$gate_cap" ]; then
207
- bound_co="$(jq -r '.company_slug // empty' "$gate_cap" 2>/dev/null || true)"
208
- fi
209
- if [ -z "$bound_co" ]; then
210
- gate_meta="$gate_root/workspace/sessions/$gate_session_id/meta.yaml"
211
- if [ -r "$gate_meta" ]; then
212
- bound_co="$(awk '
213
- $1 == "company_slug:" {
214
- sub(/^[^:]+:[[:space:]]*/, "")
215
- gsub(/^"|"$/, "")
216
- print
217
- exit
218
- }
219
- ' "$gate_meta" 2>/dev/null || true)"
220
- fi
221
- fi
270
+ bound_co="$(checkpoint_gate_bound_company "$gate_root" "$gate_session_id")"
222
271
  # A binding satisfies the gate only when it names a REAL scope: the
223
272
  # reserved `personal`, or an actual tenant directory. hq-session will
224
273
  # still store a typo'd or invented slug (it validates only the
@@ -239,6 +288,13 @@ exec 3>&2
239
288
  ;;
240
289
  esac
241
290
 
291
+ # Company setting: an owner can turn the checkpoint requirement off for
292
+ # their company's sessions. An explicit operator switch (0 or 1) wins over it.
293
+ if [ -z "$gate_switch" ] &&
294
+ checkpoint_gate_company_off "${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$self_hq}}" "$session_id"; then
295
+ exit 0
296
+ fi
297
+
242
298
  runtime="${HQ_CHECKPOINT_RUNTIME:-claude}"
243
299
 
244
300
  # Operator switches precede runtime and identity eligibility. A forced gate
@@ -3818,7 +3818,7 @@ export declare const COMMAND_CATALOG: readonly [{
3818
3818
  readonly options: readonly [];
3819
3819
  readonly subcommands: readonly [{
3820
3820
  readonly name: "set";
3821
- readonly description: "Set per-company settings (PUT /company-settings). Require at least one of --crm-enabled / --ontology-enabled / --accounting-enabled.";
3821
+ readonly description: "Set per-company settings (PUT /company-settings). Require at least one of --crm-enabled / --ontology-enabled / --accounting-enabled / --checkpoint-gate-enabled.";
3822
3822
  readonly aliases: readonly [];
3823
3823
  readonly hidden: false;
3824
3824
  readonly usage: "[options]";
@@ -3832,6 +3832,21 @@ export declare const COMMAND_CATALOG: readonly [{
3832
3832
  }, {
3833
3833
  readonly flags: "--accounting-enabled <true|false>";
3834
3834
  readonly description: "Enable/disable the accounting pack for this company";
3835
+ }, {
3836
+ readonly flags: "--checkpoint-gate-enabled <true|false>";
3837
+ readonly description: "Require (true) or stop requiring (false) a checkpoint before HQ sessions in this company end a turn";
3838
+ }];
3839
+ readonly subcommands: readonly [];
3840
+ }, {
3841
+ readonly name: "get";
3842
+ readonly description: "Show whether the checkpoint Stop gate is on for this company (GET /company-settings/checkpoint-gate; any member may read it).";
3843
+ readonly aliases: readonly [];
3844
+ readonly hidden: false;
3845
+ readonly usage: "[options]";
3846
+ readonly arguments: readonly [];
3847
+ readonly options: readonly [{
3848
+ readonly flags: "--json";
3849
+ readonly description: "Print the result as JSON";
3835
3850
  }];
3836
3851
  readonly subcommands: readonly [];
3837
3852
  }];
@@ -5191,6 +5206,24 @@ export declare const COMMAND_CATALOG: readonly [{
5191
5206
  }];
5192
5207
  readonly subcommands: readonly [];
5193
5208
  }];
5209
+ }, {
5210
+ readonly name: "checkpoint-gate-setting";
5211
+ readonly description: "Refresh the local cache of a company's checkpoint gate setting (used by the Stop gate)";
5212
+ readonly aliases: readonly [];
5213
+ readonly hidden: false;
5214
+ readonly usage: "[options]";
5215
+ readonly arguments: readonly [];
5216
+ readonly options: readonly [{
5217
+ readonly flags: "--company <slug>";
5218
+ readonly description: "company whose setting to read";
5219
+ }, {
5220
+ readonly flags: "--refresh";
5221
+ readonly description: "read the setting from HQ and update the local cache";
5222
+ }, {
5223
+ readonly flags: "--json";
5224
+ readonly description: "print the result as JSON";
5225
+ }];
5226
+ readonly subcommands: readonly [];
5194
5227
  }, {
5195
5228
  readonly name: "generate-workers-registry";
5196
5229
  readonly description: "Regenerate the derived workers registry";
@@ -4926,7 +4926,7 @@ export const COMMAND_CATALOG = [
4926
4926
  "subcommands": [
4927
4927
  {
4928
4928
  "name": "set",
4929
- "description": "Set per-company settings (PUT /company-settings). Require at least one of --crm-enabled / --ontology-enabled / --accounting-enabled.",
4929
+ "description": "Set per-company settings (PUT /company-settings). Require at least one of --crm-enabled / --ontology-enabled / --accounting-enabled / --checkpoint-gate-enabled.",
4930
4930
  "aliases": [],
4931
4931
  "hidden": false,
4932
4932
  "usage": "[options]",
@@ -4943,6 +4943,25 @@ export const COMMAND_CATALOG = [
4943
4943
  {
4944
4944
  "flags": "--accounting-enabled <true|false>",
4945
4945
  "description": "Enable/disable the accounting pack for this company"
4946
+ },
4947
+ {
4948
+ "flags": "--checkpoint-gate-enabled <true|false>",
4949
+ "description": "Require (true) or stop requiring (false) a checkpoint before HQ sessions in this company end a turn"
4950
+ }
4951
+ ],
4952
+ "subcommands": []
4953
+ },
4954
+ {
4955
+ "name": "get",
4956
+ "description": "Show whether the checkpoint Stop gate is on for this company (GET /company-settings/checkpoint-gate; any member may read it).",
4957
+ "aliases": [],
4958
+ "hidden": false,
4959
+ "usage": "[options]",
4960
+ "arguments": [],
4961
+ "options": [
4962
+ {
4963
+ "flags": "--json",
4964
+ "description": "Print the result as JSON"
4946
4965
  }
4947
4966
  ],
4948
4967
  "subcommands": []
@@ -6713,6 +6732,29 @@ export const COMMAND_CATALOG = [
6713
6732
  }
6714
6733
  ]
6715
6734
  },
6735
+ {
6736
+ "name": "checkpoint-gate-setting",
6737
+ "description": "Refresh the local cache of a company's checkpoint gate setting (used by the Stop gate)",
6738
+ "aliases": [],
6739
+ "hidden": false,
6740
+ "usage": "[options]",
6741
+ "arguments": [],
6742
+ "options": [
6743
+ {
6744
+ "flags": "--company <slug>",
6745
+ "description": "company whose setting to read"
6746
+ },
6747
+ {
6748
+ "flags": "--refresh",
6749
+ "description": "read the setting from HQ and update the local cache"
6750
+ },
6751
+ {
6752
+ "flags": "--json",
6753
+ "description": "print the result as JSON"
6754
+ }
6755
+ ],
6756
+ "subcommands": []
6757
+ },
6716
6758
  {
6717
6759
  "name": "generate-workers-registry",
6718
6760
  "description": "Regenerate the derived workers registry",
@@ -1061,8 +1061,18 @@ export function registerCloudCommands(program) {
1061
1061
  .command("status")
1062
1062
  .description("Show local sync journal summary")
1063
1063
  .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
1064
- .action((options) => {
1064
+ .action((options, command) => {
1065
+ const explicitHqRoot = command.getOptionValueSource("hqRoot") === "cli";
1065
1066
  try {
1067
+ const hqRoot = options.hqRoot;
1068
+ if (explicitHqRoot) {
1069
+ if (!fs.existsSync(hqRoot)) {
1070
+ throw new Error(`HQ root does not exist: ${hqRoot}`);
1071
+ }
1072
+ if (!fs.statSync(hqRoot).isDirectory()) {
1073
+ throw new Error(`HQ root is not a directory: ${hqRoot}`);
1074
+ }
1075
+ }
1066
1076
  // The engine SHARDS the journal by slug — the personal-vault fanout
1067
1077
  // slot, one shard per cloud company, and the legacy "personal" shard.
1068
1078
  // Enumerate EVERY shard the engine can write (via the engine's own
@@ -1071,13 +1081,7 @@ export function registerCloudCommands(program) {
1071
1081
  // looked at `sync-journal._Users_<user>_hq.json` — a slug the engine
1072
1082
  // never writes — and printed "No sync journal yet" right after a
1073
1083
  // successful sync (feedback_9fbf1f82 / feedback_46288b7b).
1074
- const journals = listJournals();
1075
- if (journals.length === 0) {
1076
- console.log(chalk.dim("No sync journal yet — run `hq sync now`, `hq sync push`, or `hq sync pull` to create one."));
1077
- console.log(chalk.dim(` Looked in: ${getStateDir()} (sync-journal.*.json)`));
1078
- return;
1079
- }
1080
- const configPath = path.join(options.hqRoot, ".hq", "config.json");
1084
+ const configPath = path.join(hqRoot, ".hq", "config.json");
1081
1085
  let activeCompany;
1082
1086
  if (fs.existsSync(configPath)) {
1083
1087
  try {
@@ -1088,13 +1092,22 @@ export function registerCloudCommands(program) {
1088
1092
  // ignore
1089
1093
  }
1090
1094
  }
1095
+ // Journals live in the machine-wide state directory, independently
1096
+ // of the HQ content root selected for this status display.
1097
+ const stateDir = getStateDir();
1098
+ const journals = listJournals();
1091
1099
  let grandFiles = 0;
1092
1100
  let grandBytes = 0;
1093
1101
  let grandLastSync = "";
1094
1102
  console.log(chalk.bold("\nHQ Sync — Status"));
1095
- console.log(` HQ root: ${options.hqRoot}`);
1103
+ console.log(` HQ root: ${hqRoot}`);
1096
1104
  console.log(` Active company: ${activeCompany ?? chalk.dim("(none)")}`);
1105
+ console.log(` State dir: ${stateDir}`);
1097
1106
  console.log(` Journals: ${journals.length}`);
1107
+ if (journals.length === 0) {
1108
+ console.log(chalk.dim("No sync journal yet — run `hq sync now`, `hq sync push`, or `hq sync pull` to create one."));
1109
+ return;
1110
+ }
1098
1111
  for (const j of journals) {
1099
1112
  const entries = Object.entries(j.journal.files ?? {});
1100
1113
  // Tombstones record removed files — exclude them from "tracked".
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import { ensureCognitoToken } from "../utils/cognito-session.js";
3
3
  import { vaultApiFetch, getEntityUid } from "../utils/vault-api.js";
4
4
  import { registerCompanyTransferCommand } from "./company-transfer.js";
5
+ import { describeRefreshOutcome, refreshCheckpointGateSetting, } from "./core-checkpoint-gate-setting.js";
5
6
  /**
6
7
  * Parse a `--flag <true|false>` string into a boolean, or throw. Commander
7
8
  * passes the raw string; we validate strictly so a typo never silently writes
@@ -28,10 +29,12 @@ export function registerCompanyCommand(program) {
28
29
  settings
29
30
  .command("set")
30
31
  .description("Set per-company settings (PUT /company-settings). Require at least one " +
31
- "of --crm-enabled / --ontology-enabled / --accounting-enabled.")
32
+ "of --crm-enabled / --ontology-enabled / --accounting-enabled / " +
33
+ "--checkpoint-gate-enabled.")
32
34
  .option("--crm-enabled <true|false>", "Enable/disable the native CRM for this company")
33
35
  .option("--ontology-enabled <true|false>", "Enable/disable the ontology gardener for this company")
34
36
  .option("--accounting-enabled <true|false>", "Enable/disable the accounting pack for this company")
37
+ .option("--checkpoint-gate-enabled <true|false>", "Require (true) or stop requiring (false) a checkpoint before HQ sessions in this company end a turn")
35
38
  .action(async (opts) => {
36
39
  try {
37
40
  const companySlug = company.opts().company;
@@ -41,8 +44,9 @@ export function registerCompanyCommand(program) {
41
44
  }
42
45
  if (opts.crmEnabled === undefined &&
43
46
  opts.ontologyEnabled === undefined &&
44
- opts.accountingEnabled === undefined) {
45
- console.error(chalk.red("Error: provide at least one of --crm-enabled / --ontology-enabled / --accounting-enabled."));
47
+ opts.accountingEnabled === undefined &&
48
+ opts.checkpointGateEnabled === undefined) {
49
+ console.error(chalk.red("Error: provide at least one of --crm-enabled / --ontology-enabled / --accounting-enabled / --checkpoint-gate-enabled."));
46
50
  process.exit(1);
47
51
  }
48
52
  const body = {};
@@ -55,6 +59,9 @@ export function registerCompanyCommand(program) {
55
59
  if (opts.accountingEnabled !== undefined) {
56
60
  body.accountingEnabled = parseBoolFlag(opts.accountingEnabled, "--accounting-enabled");
57
61
  }
62
+ if (opts.checkpointGateEnabled !== undefined) {
63
+ body.checkpointGateEnabled = parseBoolFlag(opts.checkpointGateEnabled, "--checkpoint-gate-enabled");
64
+ }
58
65
  const token = await ensureCognitoToken();
59
66
  const companyUid = await getEntityUid(token, { companySlug });
60
67
  body.companyUid = companyUid;
@@ -78,6 +85,62 @@ export function registerCompanyCommand(program) {
78
85
  .map(([k, v]) => `${k}=${v}`)
79
86
  .join(", ");
80
87
  console.log(chalk.green(`Company settings updated for ${companySlug}: ${applied}`));
88
+ if (body.checkpointGateEnabled !== undefined) {
89
+ // Update this machine's Stop gate cache now instead of waiting up to
90
+ // ten minutes. Other machines pick the change up on their next refresh.
91
+ try {
92
+ const outcome = await refreshCheckpointGateSetting(companySlug);
93
+ console.log(chalk.dim(describeRefreshOutcome(outcome)));
94
+ }
95
+ catch {
96
+ // The setting is saved; the cache refreshes on the next Stop.
97
+ }
98
+ }
99
+ }
100
+ catch (err) {
101
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
102
+ process.exit(1);
103
+ }
104
+ });
105
+ settings
106
+ .command("get")
107
+ .description("Show whether the checkpoint Stop gate is on for this company " +
108
+ "(GET /company-settings/checkpoint-gate; any member may read it).")
109
+ .option("--json", "Print the result as JSON")
110
+ .action(async (opts) => {
111
+ try {
112
+ const companySlug = company.opts().company;
113
+ if (!companySlug) {
114
+ console.error(chalk.red("Error: --company <slug> is required."));
115
+ process.exit(1);
116
+ }
117
+ const token = await ensureCognitoToken();
118
+ const companyUid = await getEntityUid(token, { companySlug });
119
+ const res = await vaultApiFetch({
120
+ token,
121
+ path: "/company-settings/checkpoint-gate",
122
+ query: { companyUid },
123
+ });
124
+ const resBody = (await res.json().catch(() => ({})));
125
+ if (!res.ok) {
126
+ if (res.status === 403) {
127
+ console.error(chalk.red("Requires membership of this company to read its settings."));
128
+ process.exit(1);
129
+ }
130
+ console.error(chalk.red(`Failed to read company settings: ${resBody.error ?? res.statusText}`));
131
+ process.exit(1);
132
+ }
133
+ const enabled = resBody.checkpointGateEnabled;
134
+ if (typeof enabled !== "boolean") {
135
+ console.error(chalk.red("Failed to read company settings: no checkpointGateEnabled in the response."));
136
+ process.exit(1);
137
+ }
138
+ if (opts.json) {
139
+ console.log(JSON.stringify({ company: companySlug, checkpointGateEnabled: enabled }));
140
+ }
141
+ else {
142
+ console.log(`checkpointGateEnabled=${enabled}`);
143
+ }
81
144
  }
82
145
  catch (err) {
83
146
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `hq core checkpoint-gate-setting` — keeps the local cache that lets a
3
+ * company owner turn the checkpoint Stop gate off for their company (CO-58).
4
+ *
5
+ * The owner sets `companySettings.checkpointGateEnabled` on hq-pro
6
+ * (`hq company settings set --checkpoint-gate-enabled false`, or the console
7
+ * Settings page). The Stop gate script never calls the network: it reads
8
+ * `~/.hq/cache/checkpoint-gate/<slug>.json` and, when that is missing or older
9
+ * than ten minutes, starts this command detached with `--refresh`.
10
+ *
11
+ * `--refresh` reads the setting through the member-readable
12
+ * `GET /company-settings/checkpoint-gate` route and writes the cache only while
13
+ * the hq-flag `checkpoint.gate.company-setting` is on for the company. With the
14
+ * flag off it removes the cache, so the gate keeps today's behaviour. Any
15
+ * failure (no login, API unreachable, timeout) leaves the cache as it was: the
16
+ * gate then uses the last known value, or today's behaviour when there is none.
17
+ * Only the named company's setting is ever read.
18
+ */
19
+ import { Command } from "commander";
20
+ export declare const CHECKPOINT_GATE_SETTING_FLAG = "checkpoint.gate.company-setting";
21
+ /** The refresh gives up after this long; the gate keeps the cached value. */
22
+ export declare const CHECKPOINT_GATE_REFRESH_TIMEOUT_MS = 3000;
23
+ export declare function assertCompanySlug(slug: string): void;
24
+ export interface CheckpointGateCacheEntry {
25
+ company: string;
26
+ checkpointGateEnabled: boolean;
27
+ /** Unix seconds. The gate treats an entry older than 600 s as stale. */
28
+ fetched_at: number;
29
+ }
30
+ export declare function checkpointGateCacheDir(home?: string): string;
31
+ export declare function checkpointGateCachePath(slug: string, home?: string): string;
32
+ export type CheckpointGateRefreshOutcome = {
33
+ status: "written";
34
+ company: string;
35
+ checkpointGateEnabled: boolean;
36
+ } | {
37
+ status: "removed";
38
+ company: string;
39
+ reason: "flag_off";
40
+ } | {
41
+ status: "kept";
42
+ company: string;
43
+ reason: string;
44
+ };
45
+ export interface CheckpointGateRefreshDeps {
46
+ home?: string;
47
+ now?: () => Date;
48
+ timeoutMs?: number;
49
+ getToken?: () => Promise<string>;
50
+ resolveCompanyUid?: (token: string, slug: string) => Promise<string>;
51
+ /** Returns the company's effective setting; throws on any failure. */
52
+ fetchSetting?: (token: string, companyUid: string, signal: AbortSignal) => Promise<boolean>;
53
+ /** Returns the rollout flag, or null when the registry could not be read. */
54
+ readFlag?: (slug: string, companyUid: string) => Promise<boolean | null>;
55
+ }
56
+ /**
57
+ * `readLanesRegistryFlag` resolves a failed registry read to `false`. The
58
+ * refresh must not treat that as "flag off" and delete a valid cache, so a
59
+ * reported failure becomes null (unknown).
60
+ */
61
+ export declare function readCheckpointGateSettingFlag(slug: string, companyUid: string): Promise<boolean | null>;
62
+ /**
63
+ * Refresh one company's cache entry. Never throws for a network or auth
64
+ * failure: those return `kept` and leave the cache untouched.
65
+ */
66
+ export declare function refreshCheckpointGateSetting(slug: string, deps?: CheckpointGateRefreshDeps): Promise<CheckpointGateRefreshOutcome>;
67
+ export declare function describeRefreshOutcome(outcome: CheckpointGateRefreshOutcome): string;
68
+ export declare function registerCoreCheckpointGateSettingCommand(core: Command): void;
69
+ //# sourceMappingURL=core-checkpoint-gate-setting.d.ts.map