@indigoai-us/hq-cli 5.345.149 → 5.345.151

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 (34) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/command-catalog.generated.d.ts +18 -6
  3. package/dist/command-catalog.generated.js +21 -6
  4. package/dist/commands/_patterns.d.ts +6 -0
  5. package/dist/commands/_patterns.js +6 -0
  6. package/dist/commands/cloud-off-gate.d.ts +138 -0
  7. package/dist/commands/cloud-off-gate.js +234 -0
  8. package/dist/commands/cloud.d.ts +7 -0
  9. package/dist/commands/cloud.js +59 -17
  10. package/dist/commands/core-handoff.js +6 -1
  11. package/dist/commands/groups.js +9 -6
  12. package/dist/commands/lanes-questions.js +7 -4
  13. package/dist/commands/lanes.js +13 -6
  14. package/dist/commands/meetings.js +7 -3
  15. package/dist/commands/members.d.ts +35 -0
  16. package/dist/commands/members.js +65 -14
  17. package/dist/commands/secrets.d.ts +5 -0
  18. package/dist/commands/secrets.js +11 -6
  19. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +63282 -62480
  20. package/dist/hq-cloud-sync-runner/package.json +1 -1
  21. package/dist/lib/daemon/catalog.d.ts +7 -0
  22. package/dist/lib/daemon/catalog.js +10 -1
  23. package/dist/lib/daemon/run.js +13 -0
  24. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  25. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  26. package/dist/lib/lanes/index.d.ts +1 -1
  27. package/dist/lib/lanes/index.js +1 -1
  28. package/dist/lib/lanes/links.js +27 -11
  29. package/dist/lib/lanes/store.d.ts +11 -0
  30. package/dist/lib/lanes/store.js +20 -0
  31. package/dist/lib/lanes/watch.js +5 -3
  32. package/dist/lib/vault-sync-switches.d.ts +41 -0
  33. package/dist/lib/vault-sync-switches.js +86 -0
  34. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,52 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.151] — 2026-10-20
6
+ - `hq lanes link _deliver` no longer hands a conductor message to a subagent. Subagent hook events share the root session id, so a subagent tool call used to claim the message and the root session never saw it. Child payloads, including Grok child sessions (`subagent_type`) and Codex child threads, now get nothing and the message stays queued until the root session's next tool call or stop. SubagentStop no longer delivers. A parent session id alone does not mark a child, so resumed, forked, or launcher-spawned Grok roots still receive their messages.
7
+ - If you leave out `--company`, `hq meetings invite <url>` now uses your only active company. If you belong to several companies, it lists them and asks you to pick one. The command used to send the invite as Personal instead. Personal can never record, so you saw the HQ Workforce upgrade prompt even when your company already had meeting recording.
8
+ - `hq sync push`, `pull` and `now` honour the HQ app's Settings > Sync switches on this computer. A command aimed at a vault whose sync is off (the personal vault, one company vault, or every vault with "Turn off all cloud sync") prints `Sync is off for <vault>` with how to turn it back on, and exits 3 without signing in or touching the vault. The target is the `--company` value, else `activeCompany` or `companySlug` in `.hq/config.json`; a `cmp_*` uid is matched through `companies/manifest.yaml`, and an unmatched uid is refused while any company vault is off. `--creds-from-stdin` is checked once stdin is read. With `--json`, the refusal is a `{"type":"skipped","reason":"sync_off"}` line on stderr. `--all` skips the off vaults and syncs the rest. The `hq daemon` background sync skips off vaults the same way and stays stopped while all cloud sync is off; a switch change restarts it once the current sync pass finishes. A settings file that cannot be read prints a warning (and logs one in the daemon) instead of silently syncing everything. `/handoff` still finishes locally and logs a skipped push as skipped.
9
+ - If you already had the HQ app's personal-files switch off (`personalSyncEnabled: false`) or a company's sync off (`workspaceSyncEnabled.<slug>: false`), `hq sync` now refuses those vaults too. That includes the `/handoff` personal publish, the `/startwork` and `/resumework` prefetch, and `/delegate` for those vaults. Turn the vault back on in HQ Settings > Sync to sync it from the command line again.
10
+
11
+ ## [5.345.150] — 2026-10-19
12
+ - The bundled sync engine now uses hq-cloud 6.18.90, which brings two fixes.
13
+ - Owners and admins can now destroy one of a company's Outposts with
14
+ `hq outposts destroy --company <slug>`. Add `--outpost <name|id>` when the
15
+ company has more than one. Without `--yes`, hq shows the company, Outpost
16
+ name and id, and destroys it only after you type the Outpost name or `yes`.
17
+ A script without a terminal must pass `--yes`. With `--yes --json` it prints
18
+ one result object. After the request, hq keeps checking status and reports
19
+ whether the Outpost is destroyed or still being destroyed, instead of
20
+ failing on a slow or errored response. It never falls back to your personal
21
+ Outpost. The Company Outpost commands from the last release are unchanged:
22
+ `hq outposts list --company`, `--outpost <name|id>` on status, login,
23
+ login-code, refresh-config and the jobs commands, and `hq outposts jobs`
24
+ edit, pause, resume, delete and run-now.
25
+ - HQ Sync uses much less CPU and memory on Windows when HQ holds git
26
+ worktrees. The sync watcher no longer watches `worktrees/` at the top of
27
+ HQ, `.worktrees/`, `.claude/worktrees/`, `workspace/worktrees/`,
28
+ `node_modules` or nested `.git` folders, including during startup. A top
29
+ level `worktrees/` folder and any `.worktrees/` folder also stop syncing;
30
+ files from them that are already in the vault are not deleted. Sync passes
31
+ started by local changes now run at most once every 30 seconds, and a file
32
+ hq just downloaded no longer triggers an upload of itself. If the sync
33
+ runner uses more than a quarter of a CPU core or 600 MB of memory for a
34
+ while, it writes one warning line to its log. You can opt in to a local
35
+ diagnostics capture, and `hq-sync-runner --diagnostics` prints a redacted
36
+ summary to paste into a bug report. Nothing is uploaded.
37
+ - A company Outpost can now be given access to a secret. Use its `out_` id:
38
+ `hq secrets share <path> --with out_…` gives it that one secret, and
39
+ `hq groups add <group> out_…` lets it read the secrets shared with that
40
+ group. It gets nothing it was not explicitly given.
41
+ - `hq members list` now shows the company Outpost as a read-only row labeled
42
+ "Outpost", and `--json` lists it under `outposts`. Promoting, removing or
43
+ revoking an Outpost is refused with a plain message.
44
+ - `hq lanes create` now prints the new lane's `lane_id` before the list of other lanes it admitted, so a script that takes the first lane id from the output gets the lane it just created. Lane commands that accept a lane id (`hq lanes message`, `hq lanes questions list --from`, `hq lanes watch` and the other lane-target commands) still match the full id first. They now also accept a ULID prefix of 8 or more characters when it names exactly one local lane, and they refuse an ambiguous prefix with an error that lists every matching lane. `hq lanes watch` always prints full lane ids.
45
+ - `hq lanes create --runner subagent` accepts `--provider codex` (and `grok`),
46
+ so Codex `spawn_agent` work from `/conduct` and `/run-project` shows on the
47
+ lanes board like Claude `Agent` work. A subagent lane is a board record only;
48
+ the provider is attribution and no process is started. The
49
+ `subagent_runner_requires_claude` refusal is gone.
50
+
5
51
  ## [5.345.149] — 2026-10-19
6
52
  - `hq access` answers faster. It checks the exact path first and, on a miss,
7
53
  looks only in the nearest folder instead of listing the whole vault. It
@@ -2039,7 +2039,7 @@ export declare const COMMAND_CATALOG: readonly [{
2039
2039
  readonly subcommands: readonly [];
2040
2040
  }, {
2041
2041
  readonly name: "share";
2042
- readonly description: "Share a secret with an email address, group, or the entire company (@all)";
2042
+ readonly description: "Share a secret with an email address, group, company Outpost (out_…), or the entire company (@all)";
2043
2043
  readonly aliases: readonly [];
2044
2044
  readonly hidden: false;
2045
2045
  readonly helpOption: true;
@@ -2054,7 +2054,7 @@ export declare const COMMAND_CATALOG: readonly [{
2054
2054
  }];
2055
2055
  readonly options: readonly [{
2056
2056
  readonly flags: "--with <principal>";
2057
- readonly description: "Email address, group id, or @all (entire company) to share with";
2057
+ readonly description: "Email address, group id, out_ Outpost id, or @all (entire company) to share with";
2058
2058
  readonly hidden: false;
2059
2059
  }, {
2060
2060
  readonly flags: "--permission <level>";
@@ -2079,7 +2079,7 @@ export declare const COMMAND_CATALOG: readonly [{
2079
2079
  }];
2080
2080
  readonly options: readonly [{
2081
2081
  readonly flags: "--from <principal>";
2082
- readonly description: "Email address, group id, or @all (entire company) to remove";
2082
+ readonly description: "Email address, group id, out_ Outpost id, or @all (entire company) to remove";
2083
2083
  readonly hidden: false;
2084
2084
  }];
2085
2085
  readonly subcommands: readonly [];
@@ -2555,7 +2555,7 @@ export declare const COMMAND_CATALOG: readonly [{
2555
2555
  readonly subcommands: readonly [];
2556
2556
  }, {
2557
2557
  readonly name: "add";
2558
- readonly description: "Add a person or agent to a group (principal: email, personUid, or agentUid)";
2558
+ readonly description: "Add a person, agent, or company Outpost to a group (principal: email, personUid, agentUid, or out_ Outpost id)";
2559
2559
  readonly aliases: readonly [];
2560
2560
  readonly hidden: false;
2561
2561
  readonly helpOption: true;
@@ -2577,7 +2577,7 @@ export declare const COMMAND_CATALOG: readonly [{
2577
2577
  readonly subcommands: readonly [];
2578
2578
  }, {
2579
2579
  readonly name: "remove";
2580
- readonly description: "Remove a person or agent from a group (principal: email, personUid, or agentUid)";
2580
+ readonly description: "Remove a person, agent, or company Outpost from a group (principal: email, personUid, agentUid, or out_ Outpost id)";
2581
2581
  readonly aliases: readonly [];
2582
2582
  readonly hidden: false;
2583
2583
  readonly helpOption: true;
@@ -7970,6 +7970,18 @@ export declare const COMMAND_CATALOG: readonly [{
7970
7970
  readonly flags: "--id <outpostId>";
7971
7971
  readonly description: "Outpost id (defaults to your primary box)";
7972
7972
  readonly hidden: false;
7973
+ }, {
7974
+ readonly flags: "--company <slug>";
7975
+ readonly description: "Destroy one of the company's Outposts (owner or admin)";
7976
+ readonly hidden: false;
7977
+ }, {
7978
+ readonly flags: "--outpost <name|id>";
7979
+ readonly description: "Which company Outpost, when the company has more than one";
7980
+ readonly hidden: false;
7981
+ }, {
7982
+ readonly flags: "--json";
7983
+ readonly description: "With --company and --yes: print one result object on stdout";
7984
+ readonly hidden: false;
7973
7985
  }, {
7974
7986
  readonly flags: "--yes";
7975
7987
  readonly description: "Confirm the irreversible teardown (required)";
@@ -14367,7 +14379,7 @@ export declare const COMMAND_CATALOG: readonly [{
14367
14379
  readonly hidden: false;
14368
14380
  }, {
14369
14381
  readonly flags: "--runner <kind>";
14370
- readonly description: "process (default) | subagent: register a lane an in-session Claude subagent runs; skips admission and starts no process";
14382
+ readonly description: "process (default) | subagent: register a lane an in-session subagent runs (Claude Agent, Codex spawn_agent); skips admission and starts no process";
14371
14383
  readonly hidden: false;
14372
14384
  }, {
14373
14385
  readonly flags: "--remote-control";
@@ -2446,7 +2446,7 @@ export const COMMAND_CATALOG = [
2446
2446
  },
2447
2447
  {
2448
2448
  "name": "share",
2449
- "description": "Share a secret with an email address, group, or the entire company (@all)",
2449
+ "description": "Share a secret with an email address, group, company Outpost (out_…), or the entire company (@all)",
2450
2450
  "aliases": [],
2451
2451
  "hidden": false,
2452
2452
  "helpOption": true,
@@ -2464,7 +2464,7 @@ export const COMMAND_CATALOG = [
2464
2464
  "options": [
2465
2465
  {
2466
2466
  "flags": "--with <principal>",
2467
- "description": "Email address, group id, or @all (entire company) to share with",
2467
+ "description": "Email address, group id, out_ Outpost id, or @all (entire company) to share with",
2468
2468
  "hidden": false
2469
2469
  },
2470
2470
  {
@@ -2495,7 +2495,7 @@ export const COMMAND_CATALOG = [
2495
2495
  "options": [
2496
2496
  {
2497
2497
  "flags": "--from <principal>",
2498
- "description": "Email address, group id, or @all (entire company) to remove",
2498
+ "description": "Email address, group id, out_ Outpost id, or @all (entire company) to remove",
2499
2499
  "hidden": false
2500
2500
  }
2501
2501
  ],
@@ -3071,7 +3071,7 @@ export const COMMAND_CATALOG = [
3071
3071
  },
3072
3072
  {
3073
3073
  "name": "add",
3074
- "description": "Add a person or agent to a group (principal: email, personUid, or agentUid)",
3074
+ "description": "Add a person, agent, or company Outpost to a group (principal: email, personUid, agentUid, or out_ Outpost id)",
3075
3075
  "aliases": [],
3076
3076
  "hidden": false,
3077
3077
  "helpOption": true,
@@ -3097,7 +3097,7 @@ export const COMMAND_CATALOG = [
3097
3097
  },
3098
3098
  {
3099
3099
  "name": "remove",
3100
- "description": "Remove a person or agent from a group (principal: email, personUid, or agentUid)",
3100
+ "description": "Remove a person, agent, or company Outpost from a group (principal: email, personUid, agentUid, or out_ Outpost id)",
3101
3101
  "aliases": [],
3102
3102
  "hidden": false,
3103
3103
  "helpOption": true,
@@ -9714,6 +9714,21 @@ export const COMMAND_CATALOG = [
9714
9714
  "description": "Outpost id (defaults to your primary box)",
9715
9715
  "hidden": false
9716
9716
  },
9717
+ {
9718
+ "flags": "--company <slug>",
9719
+ "description": "Destroy one of the company's Outposts (owner or admin)",
9720
+ "hidden": false
9721
+ },
9722
+ {
9723
+ "flags": "--outpost <name|id>",
9724
+ "description": "Which company Outpost, when the company has more than one",
9725
+ "hidden": false
9726
+ },
9727
+ {
9728
+ "flags": "--json",
9729
+ "description": "With --company and --yes: print one result object on stdout",
9730
+ "hidden": false
9731
+ },
9717
9732
  {
9718
9733
  "flags": "--yes",
9719
9734
  "description": "Confirm the irreversible teardown (required)",
@@ -17472,7 +17487,7 @@ export const COMMAND_CATALOG = [
17472
17487
  },
17473
17488
  {
17474
17489
  "flags": "--runner <kind>",
17475
- "description": "process (default) | subagent: register a lane an in-session Claude subagent runs; skips admission and starts no process",
17490
+ "description": "process (default) | subagent: register a lane an in-session subagent runs (Claude Agent, Codex spawn_agent); skips admission and starts no process",
17476
17491
  "hidden": false
17477
17492
  },
17478
17493
  {
@@ -1,6 +1,12 @@
1
1
  export declare const SECRET_NAME_PATTERN: RegExp;
2
2
  export declare const GROUP_ID_PATTERN: RegExp;
3
3
  export declare const EMAIL_PATTERN: RegExp;
4
+ /**
5
+ * A company Outpost identity: `out_` plus a 26-character Crockford ULID, the
6
+ * same shape hq-pro accepts as an Outpost grantee. It rides the `person`
7
+ * grantee slot on the wire.
8
+ */
9
+ export declare const OUTPOST_UID_PATTERN: RegExp;
4
10
  /**
5
11
  * Keep an ACL pattern exactly as the caller supplied it.
6
12
  *
@@ -1,6 +1,12 @@
1
1
  export const SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*(?:\/[A-Z][A-Z0-9_]+)*$/;
2
2
  export const GROUP_ID_PATTERN = /^grp_[A-Za-z0-9_-]+$/;
3
3
  export const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
4
+ /**
5
+ * A company Outpost identity: `out_` plus a 26-character Crockford ULID, the
6
+ * same shape hq-pro accepts as an Outpost grantee. It rides the `person`
7
+ * grantee slot on the wire.
8
+ */
9
+ export const OUTPOST_UID_PATTERN = /^out_[0-9A-HJKMNP-TV-Z]{26}$/;
4
10
  /**
5
11
  * Keep an ACL pattern exactly as the caller supplied it.
6
12
  *
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Sync switch gate for the manual `hq sync push|pull|now` commands
3
+ * (usage-only-pricing US-016).
4
+ *
5
+ * The desktop app's Settings > Sync section has one switch per vault plus one
6
+ * "Turn off all cloud sync" switch (see `../lib/vault-sync-switches.ts`). The
7
+ * switches affect only this computer. The app gates its own sync paths, but
8
+ * skills and scripts call these CLI commands directly (`/handoff`, the
9
+ * `/startwork` and `/resumework` prefetch, `/delegate`, job remediation). This
10
+ * gate makes those calls honour the same switches: a command aimed at an off
11
+ * vault refuses, and an `--all` run skips the off vaults while the rest sync.
12
+ *
13
+ * How the target vault is found, before any login or network call:
14
+ * - `--personal`, or a `prs_*` uid, is the personal vault;
15
+ * - `--company <ref>`, else the active company in `<hq-root>/.hq/config.json`
16
+ * (`activeCompany ?? companySlug`, the same order hq-cloud's resolver uses);
17
+ * - a `cmp_*` uid is mapped to its slug through `companies/manifest.yaml`
18
+ * (`cloud_uid`). A uid that cannot be mapped while any company vault is off
19
+ * is refused (fail closed): the gate cannot prove it is not an off vault.
20
+ * - `--creds-from-stdin`: the target is the parsed EntityContext, checked
21
+ * after stdin is read (`refusalForEntity`) and still before any transfer.
22
+ *
23
+ * The daemon's own `syncPaused` is not read here: it pauses background sync
24
+ * only (`hq daemon sync pause`, the tray pause), and a manual push while
25
+ * background sync is paused is expected to run.
26
+ */
27
+ import { readVaultSyncSwitches, vaultSkipEnv, type VaultSyncSwitches } from "../lib/vault-sync-switches.js";
28
+ export { readVaultSyncSwitches, vaultSkipEnv, type VaultSyncSwitches };
29
+ /**
30
+ * Exit code for "skipped because sync is off". Distinct from 1 (sync failed)
31
+ * so a caller can tell the two apart; every non-zero exit still means "nothing
32
+ * was synced", which is what `/delegate` and job remediation need to see.
33
+ */
34
+ export declare const CLOUD_OFF_EXIT_CODE = 3;
35
+ export type GatedSyncCommand = "push" | "pull" | "now";
36
+ /** True when "Turn off all cloud sync" is on. Never throws. */
37
+ export declare function isCloudOff(home?: string): boolean;
38
+ /** True when `company` (a slug) is a company vault whose switch is off. */
39
+ export declare function isCompanyVaultOff(switches: VaultSyncSwitches, company: string): boolean;
40
+ /**
41
+ * The refusal text when every vault is off. The first line keeps the phrase
42
+ * "refused immediately": `handoff-sync-prefetch` (shell and native) reports
43
+ * `skipped` rather than `error:<code>` when the pull log contains it, so
44
+ * released HQ versions treat this exit as skipped with no script change.
45
+ */
46
+ export declare function cloudOffMessage(command: GatedSyncCommand): string;
47
+ /** The refusal text when the one vault a command targets is off. */
48
+ export declare function vaultOffMessage(command: GatedSyncCommand, vaultName: string): string;
49
+ /** The refusal text when a company uid cannot be mapped while some vault is off. */
50
+ export declare function unresolvedVaultMessage(command: GatedSyncCommand, ref: string): string;
51
+ /** Which vaults a `push|pull|now` invocation targets. */
52
+ export interface SyncSelection {
53
+ all?: boolean;
54
+ /** `true` for `--personal`; `false` is `--no-personal` (only meaningful with `--all`). */
55
+ personal?: boolean;
56
+ company?: string;
57
+ /** Used to find the active company and the uid-to-slug manifest. */
58
+ hqRoot?: string;
59
+ /** `--creds-from-stdin`: the target is checked after stdin is parsed. */
60
+ credsFromStdin?: boolean;
61
+ }
62
+ /** What an `--all` run must leave out. */
63
+ export interface VaultSkips {
64
+ skipPersonal: boolean;
65
+ skipCompanies: ReadonlySet<string>;
66
+ }
67
+ /** A refusal: the text, and the vault it names (for JSON output). */
68
+ export interface VaultRefusal {
69
+ message: string;
70
+ vault: string;
71
+ }
72
+ /** The vault a company reference or entity uid names. */
73
+ export type ResolvedVault = {
74
+ kind: "personal";
75
+ } | {
76
+ kind: "company";
77
+ slug: string;
78
+ } | {
79
+ kind: "unknown";
80
+ ref: string;
81
+ };
82
+ /**
83
+ * Active company from `<hqRoot>/.hq/config.json`, best effort. Uses
84
+ * `activeCompany ?? companySlug`, the order hq-cloud's `resolveActiveCompany`
85
+ * uses, so a config with only `companySlug` is still gated.
86
+ */
87
+ export declare function readActiveCompanyRef(hqRoot: string | undefined): string | undefined;
88
+ /** Map a slug, `cmp_*` uid or `prs_*` uid to the vault it names. */
89
+ export declare function resolveVaultRef(ref: string, hqRoot: string | undefined): ResolvedVault;
90
+ /**
91
+ * The refusal for `selection`, or null when it may run. `--all` never
92
+ * refuses for one vault: it skips the off vaults (see {@link vaultSkips}).
93
+ * With `--creds-from-stdin` and no `--company`, the target is unknown until
94
+ * stdin is parsed; {@link refusalForEntity} checks it then.
95
+ */
96
+ export declare function refusalFor(command: GatedSyncCommand, selection: SyncSelection, switches: VaultSyncSwitches): VaultRefusal | null;
97
+ /**
98
+ * The refusal for a pre-vended EntityContext read from stdin. A `prs_*` uid
99
+ * is the personal vault; otherwise the slug (or the uid, through the
100
+ * manifest) names the company.
101
+ */
102
+ export declare function refusalForEntity(command: GatedSyncCommand, entity: {
103
+ uid?: unknown;
104
+ slug?: unknown;
105
+ }, hqRoot: string | undefined, switches: VaultSyncSwitches): VaultRefusal | null;
106
+ /** The vaults an `--all` run leaves out. */
107
+ export declare function vaultSkips(selection: SyncSelection, switches: VaultSyncSwitches): VaultSkips;
108
+ /** Thrown after stdin is parsed when its target vault is off. */
109
+ export declare class VaultSyncOffError extends Error {
110
+ readonly refusal: VaultRefusal;
111
+ constructor(refusal: VaultRefusal);
112
+ }
113
+ export interface CloudOffGateDeps {
114
+ home?: string;
115
+ writeErr?: (text: string) => void;
116
+ exit?: (code: number) => never;
117
+ /** `--json`: stderr is a JSON Lines stream, so write events, not prose. */
118
+ json?: boolean;
119
+ }
120
+ /** Write a refusal: plain text, or one JSON Lines event in `--json` mode. */
121
+ export declare function writeVaultRefusal(refusal: VaultRefusal, deps?: Pick<CloudOffGateDeps, "writeErr" | "json">): void;
122
+ /**
123
+ * Print the refusal and exit {@link CLOUD_OFF_EXIT_CODE} when `selection`
124
+ * targets an off vault (or every vault is off); otherwise return what an
125
+ * `--all` run must skip. Runs before any login, lock or network work, and
126
+ * outside the client-health report, so a refusal is not counted as a failed
127
+ * sync. Warns when the settings file is unreadable.
128
+ */
129
+ export declare function refuseSyncForOffVaults(command: GatedSyncCommand, selection: SyncSelection, deps?: CloudOffGateDeps): VaultSkips;
130
+ /**
131
+ * `--creds-from-stdin` check: throw {@link VaultSyncOffError} when the parsed
132
+ * EntityContext targets an off vault. The caller writes it and exits 3.
133
+ */
134
+ export declare function assertEntityVaultSyncAllowed(command: GatedSyncCommand, entity: {
135
+ uid?: unknown;
136
+ slug?: unknown;
137
+ }, hqRoot: string | undefined, home?: string): void;
138
+ //# sourceMappingURL=cloud-off-gate.d.ts.map
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Sync switch gate for the manual `hq sync push|pull|now` commands
3
+ * (usage-only-pricing US-016).
4
+ *
5
+ * The desktop app's Settings > Sync section has one switch per vault plus one
6
+ * "Turn off all cloud sync" switch (see `../lib/vault-sync-switches.ts`). The
7
+ * switches affect only this computer. The app gates its own sync paths, but
8
+ * skills and scripts call these CLI commands directly (`/handoff`, the
9
+ * `/startwork` and `/resumework` prefetch, `/delegate`, job remediation). This
10
+ * gate makes those calls honour the same switches: a command aimed at an off
11
+ * vault refuses, and an `--all` run skips the off vaults while the rest sync.
12
+ *
13
+ * How the target vault is found, before any login or network call:
14
+ * - `--personal`, or a `prs_*` uid, is the personal vault;
15
+ * - `--company <ref>`, else the active company in `<hq-root>/.hq/config.json`
16
+ * (`activeCompany ?? companySlug`, the same order hq-cloud's resolver uses);
17
+ * - a `cmp_*` uid is mapped to its slug through `companies/manifest.yaml`
18
+ * (`cloud_uid`). A uid that cannot be mapped while any company vault is off
19
+ * is refused (fail closed): the gate cannot prove it is not an off vault.
20
+ * - `--creds-from-stdin`: the target is the parsed EntityContext, checked
21
+ * after stdin is read (`refusalForEntity`) and still before any transfer.
22
+ *
23
+ * The daemon's own `syncPaused` is not read here: it pauses background sync
24
+ * only (`hq daemon sync pause`, the tray pause), and a manual push while
25
+ * background sync is paused is expected to run.
26
+ */
27
+ import * as fs from "node:fs";
28
+ import * as path from "node:path";
29
+ import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
30
+ import { readVaultSyncSwitches, unreadableSwitchesWarning, vaultSkipEnv, } from "../lib/vault-sync-switches.js";
31
+ export { readVaultSyncSwitches, vaultSkipEnv };
32
+ /**
33
+ * Exit code for "skipped because sync is off". Distinct from 1 (sync failed)
34
+ * so a caller can tell the two apart; every non-zero exit still means "nothing
35
+ * was synced", which is what `/delegate` and job remediation need to see.
36
+ */
37
+ export const CLOUD_OFF_EXIT_CODE = 3;
38
+ /** True when "Turn off all cloud sync" is on. Never throws. */
39
+ export function isCloudOff(home) {
40
+ return readVaultSyncSwitches(home).allOff;
41
+ }
42
+ /** True when `company` (a slug) is a company vault whose switch is off. */
43
+ export function isCompanyVaultOff(switches, company) {
44
+ return switches.offCompanies.has(company.trim().toLowerCase());
45
+ }
46
+ const HOW_TO_TURN_ON = "To turn it back on, open HQ and go to Settings > Sync.";
47
+ const NOTHING_MOVED = "Nothing was uploaded or downloaded. Your local files are unchanged.";
48
+ /**
49
+ * The refusal text when every vault is off. The first line keeps the phrase
50
+ * "refused immediately": `handoff-sync-prefetch` (shell and native) reports
51
+ * `skipped` rather than `error:<code>` when the pull log contains it, so
52
+ * released HQ versions treat this exit as skipped with no script change.
53
+ */
54
+ export function cloudOffMessage(command) {
55
+ return [
56
+ `Cloud sync is off for every vault on this computer, so \`hq sync ${command}\` was refused immediately.`,
57
+ NOTHING_MOVED,
58
+ HOW_TO_TURN_ON,
59
+ ].join("\n");
60
+ }
61
+ /** The refusal text when the one vault a command targets is off. */
62
+ export function vaultOffMessage(command, vaultName) {
63
+ return [
64
+ `Sync is off for ${vaultName} on this computer, so \`hq sync ${command}\` was refused immediately.`,
65
+ NOTHING_MOVED,
66
+ HOW_TO_TURN_ON,
67
+ ].join("\n");
68
+ }
69
+ /** The refusal text when a company uid cannot be mapped while some vault is off. */
70
+ export function unresolvedVaultMessage(command, ref) {
71
+ return [
72
+ `Sync is off for one or more company vaults on this computer, and \`hq sync ${command}\` could not tell which vault ${ref} is, so it was refused immediately.`,
73
+ NOTHING_MOVED,
74
+ `Run it with --company <slug>, or turn the vaults back on in HQ Settings > Sync.`,
75
+ ].join("\n");
76
+ }
77
+ const COMPANY_UID = /^cmp_/i;
78
+ const PERSON_UID = /^prs_/i;
79
+ /**
80
+ * Active company from `<hqRoot>/.hq/config.json`, best effort. Uses
81
+ * `activeCompany ?? companySlug`, the order hq-cloud's `resolveActiveCompany`
82
+ * uses, so a config with only `companySlug` is still gated.
83
+ */
84
+ export function readActiveCompanyRef(hqRoot) {
85
+ if (!hqRoot)
86
+ return undefined;
87
+ try {
88
+ const cfg = JSON.parse(fs.readFileSync(path.join(hqRoot, ".hq", "config.json"), "utf8"));
89
+ for (const value of [cfg?.activeCompany, cfg?.companySlug]) {
90
+ if (typeof value === "string" && value.trim())
91
+ return value.trim();
92
+ }
93
+ return undefined;
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ }
99
+ /** Map a slug, `cmp_*` uid or `prs_*` uid to the vault it names. */
100
+ export function resolveVaultRef(ref, hqRoot) {
101
+ const trimmed = ref.trim();
102
+ if (PERSON_UID.test(trimmed))
103
+ return { kind: "personal" };
104
+ if (!COMPANY_UID.test(trimmed))
105
+ return { kind: "company", slug: trimmed };
106
+ const manifest = hqRoot ? readCompaniesManifestMap(hqRoot) : null;
107
+ for (const [slug, entry] of Object.entries(manifest ?? {})) {
108
+ const uid = entry?.cloud_uid;
109
+ if (typeof uid === "string" && uid.toLowerCase() === trimmed.toLowerCase()) {
110
+ return { kind: "company", slug };
111
+ }
112
+ }
113
+ return { kind: "unknown", ref: trimmed };
114
+ }
115
+ function refusalForVault(command, vault, switches) {
116
+ switch (vault.kind) {
117
+ case "personal":
118
+ return switches.personalOff
119
+ ? { message: vaultOffMessage(command, "your personal vault"), vault: "personal" }
120
+ : null;
121
+ case "company":
122
+ return isCompanyVaultOff(switches, vault.slug)
123
+ ? { message: vaultOffMessage(command, vault.slug), vault: vault.slug }
124
+ : null;
125
+ case "unknown":
126
+ // Fail closed: with any company vault off, an unmapped uid may be it.
127
+ return switches.offCompanies.size > 0
128
+ ? { message: unresolvedVaultMessage(command, vault.ref), vault: vault.ref }
129
+ : null;
130
+ }
131
+ }
132
+ /**
133
+ * The refusal for `selection`, or null when it may run. `--all` never
134
+ * refuses for one vault: it skips the off vaults (see {@link vaultSkips}).
135
+ * With `--creds-from-stdin` and no `--company`, the target is unknown until
136
+ * stdin is parsed; {@link refusalForEntity} checks it then.
137
+ */
138
+ export function refusalFor(command, selection, switches) {
139
+ if (switches.allOff)
140
+ return { message: cloudOffMessage(command), vault: "all" };
141
+ if (selection.all)
142
+ return null;
143
+ if (selection.personal === true) {
144
+ return refusalForVault(command, { kind: "personal" }, switches);
145
+ }
146
+ const ref = selection.company ?? (selection.credsFromStdin ? undefined : readActiveCompanyRef(selection.hqRoot));
147
+ if (!ref)
148
+ return null;
149
+ return refusalForVault(command, resolveVaultRef(ref, selection.hqRoot), switches);
150
+ }
151
+ /**
152
+ * The refusal for a pre-vended EntityContext read from stdin. A `prs_*` uid
153
+ * is the personal vault; otherwise the slug (or the uid, through the
154
+ * manifest) names the company.
155
+ */
156
+ export function refusalForEntity(command, entity, hqRoot, switches) {
157
+ if (switches.allOff)
158
+ return { message: cloudOffMessage(command), vault: "all" };
159
+ const uid = typeof entity.uid === "string" ? entity.uid : "";
160
+ const slug = typeof entity.slug === "string" ? entity.slug : "";
161
+ if (PERSON_UID.test(uid))
162
+ return refusalForVault(command, { kind: "personal" }, switches);
163
+ if (slug.trim())
164
+ return refusalForVault(command, { kind: "company", slug }, switches);
165
+ if (uid.trim())
166
+ return refusalForVault(command, resolveVaultRef(uid, hqRoot), switches);
167
+ return null;
168
+ }
169
+ /** The vaults an `--all` run leaves out. */
170
+ export function vaultSkips(selection, switches) {
171
+ return {
172
+ skipPersonal: selection.personal === false || switches.personalOff,
173
+ skipCompanies: switches.offCompanies,
174
+ };
175
+ }
176
+ /** Thrown after stdin is parsed when its target vault is off. */
177
+ export class VaultSyncOffError extends Error {
178
+ refusal;
179
+ constructor(refusal) {
180
+ super(refusal.message);
181
+ this.refusal = refusal;
182
+ this.name = "VaultSyncOffError";
183
+ }
184
+ }
185
+ /** Write a refusal: plain text, or one JSON Lines event in `--json` mode. */
186
+ export function writeVaultRefusal(refusal, deps = {}) {
187
+ const writeErr = deps.writeErr ?? ((text) => process.stderr.write(text));
188
+ if (deps.json) {
189
+ writeErr(`${JSON.stringify({
190
+ type: "skipped",
191
+ reason: "sync_off",
192
+ vault: refusal.vault,
193
+ exitCode: CLOUD_OFF_EXIT_CODE,
194
+ message: refusal.message,
195
+ })}\n`);
196
+ }
197
+ else {
198
+ writeErr(`${refusal.message}\n`);
199
+ }
200
+ }
201
+ function warnIfUnreadable(switches, deps) {
202
+ if (!switches.unreadable)
203
+ return;
204
+ const writeErr = deps.writeErr ?? ((text) => process.stderr.write(text));
205
+ const message = unreadableSwitchesWarning(switches.unreadable);
206
+ writeErr(deps.json ? `${JSON.stringify({ type: "warning", message })}\n` : `${message}\n`);
207
+ }
208
+ /**
209
+ * Print the refusal and exit {@link CLOUD_OFF_EXIT_CODE} when `selection`
210
+ * targets an off vault (or every vault is off); otherwise return what an
211
+ * `--all` run must skip. Runs before any login, lock or network work, and
212
+ * outside the client-health report, so a refusal is not counted as a failed
213
+ * sync. Warns when the settings file is unreadable.
214
+ */
215
+ export function refuseSyncForOffVaults(command, selection, deps = {}) {
216
+ const switches = readVaultSyncSwitches(deps.home);
217
+ warnIfUnreadable(switches, deps);
218
+ const refusal = refusalFor(command, selection, switches);
219
+ if (refusal) {
220
+ writeVaultRefusal(refusal, deps);
221
+ (deps.exit ?? ((code) => process.exit(code)))(CLOUD_OFF_EXIT_CODE);
222
+ }
223
+ return vaultSkips(selection, switches);
224
+ }
225
+ /**
226
+ * `--creds-from-stdin` check: throw {@link VaultSyncOffError} when the parsed
227
+ * EntityContext targets an off vault. The caller writes it and exits 3.
228
+ */
229
+ export function assertEntityVaultSyncAllowed(command, entity, hqRoot, home) {
230
+ const refusal = refusalForEntity(command, entity, hqRoot, readVaultSyncSwitches(home));
231
+ if (refusal)
232
+ throw new VaultSyncOffError(refusal);
233
+ }
234
+ //# sourceMappingURL=cloud-off-gate.js.map
@@ -196,6 +196,11 @@ export interface PullAllOptions {
196
196
  * personal vault from the run without touching the rest of the plan.
197
197
  */
198
198
  skipPersonal?: boolean;
199
+ /**
200
+ * Lowercased company slugs (or uids) to leave out: vaults whose sync is off
201
+ * on this computer (desktop Settings > Sync, US-016).
202
+ */
203
+ skipCompanies?: ReadonlySet<string>;
199
204
  /**
200
205
  * Forward `--force-scope-shrink` to each company leg: when a scope shrink
201
206
  * would un-track locally-modified files, proceed anyway (dirty files KEPT on
@@ -263,6 +268,8 @@ export interface PushAllOptions {
263
268
  * `hq sync push --all --no-personal`.
264
269
  */
265
270
  skipPersonal?: boolean;
271
+ /** Lowercased company slugs (or uids) to leave out (US-016). */
272
+ skipCompanies?: ReadonlySet<string>;
266
273
  }
267
274
  export interface PushAllRow {
268
275
  slug: string;