@indigoai-us/hq-cli 5.345.137 → 5.345.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.139] — 2026-10-15
6
+ - `hq storage offload` and `hq storage fetch` no longer fail about 15 to 25 minutes into a run. Personal and company
7
+ vault credentials are now re-vended before they expire, for the whole run and in the middle of a multipart upload.
8
+ When S3 still refuses a request's credentials, HQ re-vends once and retries that request once. A request refused
9
+ again fails, and an interrupted company upload resumes from its last finished part on the next run.
10
+ - Vault errors now name the request, the vault, the HTTP status and the error code, for example
11
+ `HEAD failed in company vault 'acme': HTTP 403 Forbidden (no error body: credentials expired or access denied)`,
12
+ instead of a bare `UnknownError`.
13
+
14
+ ## [5.345.138] — 2026-10-15
15
+ - `hq storage offload --current` can now free company files larger than the 50 MB sync limit. It uploads each one to its company vault, checks the vault copy against the file's sha256 and size, records it as cloud-only, writes the `.hqcloud` placeholder, and only then removes the local copy. An interrupted upload leaves the file in place and resumes on the next run. The dry run shows `upload+offload` with the byte count and the vault key. A path the `.hqinclude` allowlist excludes is refused with the line to add. `hq storage fetch` checks the vault copy's sha256 before restoring it. The upload path turns on once the bundled hq-cloud includes the new upload exports; until then these files report that HQ sync needs an update.
16
+
5
17
  ## [5.345.137] — 2026-10-15
6
18
  - The bundled sync engine now uses hq-cloud 6.18.76. On Windows, the hq-core update and rescue lay down native links after the overlay, turn git symlink stubs from Windows clones into real links, and upload symlinks with the correct target. Sync merges `board.json` by entry id instead of replacing it, merges append-only `.jsonl` conflicts instead of parking them, keeps any `.hqcloud` placeholder on pull, and no longer syncs live-meeting runtime state or server sync manifests.
7
19
 
@@ -1,38 +1,49 @@
1
1
  /**
2
- * `hq storage offload` / `hq storage fetch` — move big files to the caller's
3
- * PERSONAL vault (S3 Standard, content-addressed) and leave small linked
4
- * pointers behind. Shared contract: owner-approved offload design, 2026-10-09.
2
+ * `hq storage offload` / `hq storage fetch` — move big files out of the HQ
3
+ * folder into HQ cloud storage and leave small linked placeholders behind.
4
+ * Shared contract: owner-approved offload design, 2026-10-09; company upload
5
+ * for files over the sync limit approved 2026-10-15.
5
6
  *
6
7
  * Two kinds of big file:
7
8
  * - History copies (old versions inside the HQ root git history). The CLI
8
- * uploads each blob, verifies size + sha256 + git blob id, then hands only
9
- * verified blobs to core `hq-storage.sh strip-large`, which rewrites history
10
- * to the pointer text. Working-tree files are never touched.
9
+ * uploads each blob to the caller's PERSONAL vault, verifies size + sha256
10
+ * + git blob id, then hands only verified blobs to core `hq-storage.sh
11
+ * strip-large`, which rewrites history to the pointer text. Working-tree
12
+ * files are never touched.
11
13
  * - Current files (`--current [paths...]`, idle big files in the working
12
- * tree). Each file goes to the vault that owns its path. A file under
13
- * `companies/<slug>/` is never uploaded: when HQ sync already stored this
14
- * exact version in that company's vault (journal hash + size, and the vault
15
- * object still has the journal's ETag), the file is replaced by a
16
- * placeholder pointing at that object. Otherwise it is skipped with "run
17
- * hq sync first", so sync stays the only writer of company vault keys. Any
18
- * other file points at its personal-vault sync object the same way, or is
19
- * uploaded to the personal vault's `.hq-offload/blobs/`. Current-file
20
- * offload runs only when the installed HQ sync holds every `.hqcloud`
21
- * sibling (hq-cloud CLOUD_PLACEHOLDER_HOLDS_ANY_SIBLING) and skips
22
- * `.hq-offload/`; otherwise it reports "update HQ sync".
14
+ * tree). Each file goes to the vault that owns its path:
15
+ * - A file in a company folder belongs to that company's vault, and only
16
+ * an active member can offload it. When HQ sync already stored this
17
+ * exact version (journal hash + size, and the vault object still has
18
+ * the journal's ETag), the file is replaced by a placeholder pointing
19
+ * at that object. A file over the sync limit (SYNC_MAX_FILE_BYTES),
20
+ * which sync never uploads, is uploaded to the company vault at its
21
+ * sync key by a resumable, checksum-verified multipart upload
22
+ * (`upload+offload`). Once the vault copy is verified the file is
23
+ * recorded in the sync journal as synced and cloud-only, then the
24
+ * placeholder is written, then the local file is removed. A path the
25
+ * sync ignore rules exclude is refused and the excluding rule is named
26
+ * (for `.hqinclude`, with the exact line to add). Other unsynced
27
+ * company files are skipped with "run hq sync first".
28
+ * - Any other file points at its personal-vault sync object the same
29
+ * way, or is uploaded to the personal vault's `.hq-offload/blobs/`.
30
+ * Current-file offload runs only when the installed HQ sync holds every
31
+ * `.hqcloud` sibling (hq-cloud CLOUD_PLACEHOLDER_HOLDS_ANY_SIBLING) and
32
+ * skips `.hq-offload/`; otherwise it reports "update HQ sync".
23
33
  *
24
- * Fetch also reads placeholders that HQ sync rewrote after a teammate changed
25
- * the file (`vault: company`, the company vault key, `sha256: unknown`,
26
- * `remote-etag`). Those download from the company vault, only for an active
27
- * member, and verify by size and ETag. A placeholder marked `orphaned: true`
28
- * (a teammate deleted the file) is refused unless `--force-orphan` is given,
29
- * which writes the caller's own personal-vault copy to a file outside HQ so
30
- * it can never sync back.
34
+ * Fetch also reads placeholders that HQ sync wrote on a teammate's machine
35
+ * (`vault: company`, the company vault key, `sha256: unknown`, `remote-etag`).
36
+ * Those download from the company vault, only for an active member, and
37
+ * verify by size, ETag and, when the object carries it, its `hq-sha256`
38
+ * metadata. A placeholder marked `orphaned: true` (a teammate deleted the
39
+ * file) is refused unless `--force-orphan` is given, which writes the caller's
40
+ * own personal-vault copy to a file outside HQ so it can never sync back.
31
41
  *
32
- * Tenant isolation: uploads only ever go to the caller's personal vault, and
33
- * the personal store refuses a company vault bucket. A company vault is opened
34
- * read-only (HEAD and ETag-pinned GET), and only for a company the caller is
35
- * an active member of.
42
+ * Tenant isolation: history and personal uploads only ever go to the caller's
43
+ * personal vault, and the personal store refuses a company vault bucket. A
44
+ * company vault is opened only for a company the caller is an active member
45
+ * of, and only for a file in that company's folder; the company uploader must
46
+ * name that company and refuses a personal bucket.
36
47
  */
37
48
  import { GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
38
49
  import type { ScriptRunner } from "./storage.js";
@@ -47,6 +58,13 @@ export declare const ORPHANED_MESSAGE = "This file was deleted by a teammate. It
47
58
  export declare const UPDATE_SYNC = "update HQ sync";
48
59
  /** Reason on company files HQ sync has not stored in the company vault yet. */
49
60
  export declare const RUN_SYNC_FIRST = "run hq sync first";
61
+ /**
62
+ * HQ sync's per-file limit (hq-cloud SYNC_MAX_FILE_BYTES; sync skips files
63
+ * strictly larger). storage.ts passes hq-cloud's value when it exports one.
64
+ */
65
+ export declare const SYNC_MAX_FILE_BYTES: number;
66
+ /** Object metadata key holding the sha256 of a company-vault offload upload. */
67
+ export declare const VAULT_SHA256_METADATA_KEY = "hq-sha256";
50
68
  /** Top-level HQ-root folder that holds offloaded blobs in the personal vault. */
51
69
  export declare const OFFLOAD_TOP_LEVEL = ".hq-offload";
52
70
  /**
@@ -112,18 +130,64 @@ export interface BlobStore {
112
130
  export interface OffloadS3Client {
113
131
  send(cmd: PutObjectCommand | HeadObjectCommand | GetObjectCommand): Promise<unknown>;
114
132
  }
133
+ /**
134
+ * True when S3 refused the request's credentials: an expired, invalid or
135
+ * denied token, or a 403 with no error body (what S3 sends for HEAD).
136
+ */
137
+ export declare function isCredentialRejection(err: unknown): boolean;
138
+ /** A failed vault request, described with its HTTP status, error code and vault. */
139
+ export declare class VaultRequestError extends Error {
140
+ readonly status?: number;
141
+ readonly code?: string;
142
+ constructor(message: string, opts: {
143
+ status?: number;
144
+ code?: string;
145
+ cause: unknown;
146
+ });
147
+ }
148
+ /**
149
+ * Describes an AWS error from `op` against `where` so the user sees the HTTP
150
+ * status, the error code and the vault, never the SDK's bare "UnknownError".
151
+ * Errors that did not come from AWS pass through unchanged.
152
+ */
153
+ export declare function describeVaultError(err: unknown, op: string, where: string, retried?: boolean): unknown;
154
+ /** Turns any per-file failure into one line for the run's error list. */
155
+ export declare function failureMessage(err: unknown): string;
156
+ /**
157
+ * Runs vault requests on `initial` and, when S3 refuses the credentials,
158
+ * re-vends once and retries the request once on the new client. Requests
159
+ * refused on the same client share one re-vend. A request refused again
160
+ * after the re-vend fails with a described error; it is never retried twice.
161
+ */
162
+ export declare function credentialRetry<C>(initial: C, where: string, revend?: () => Promise<C>): <T>(op: string, fn: (client: C) => Promise<T>) => Promise<T>;
163
+ /**
164
+ * Wraps every method of `initial` (a multipart client) so each call gets
165
+ * {@link credentialRetry}: one re-vend and one retry when S3 refuses the
166
+ * credentials, and a described error otherwise.
167
+ */
168
+ export declare function credentialRetryingClient<C extends object>(initial: C, where: string, revend: () => Promise<C>): C;
169
+ export interface S3StoreOptions<C> {
170
+ /** How errors name the vault, e.g. "personal vault" or "company vault 'acme'". */
171
+ where?: string;
172
+ /** Vends fresh credentials and returns a client that uses them. */
173
+ revend?: () => Promise<C>;
174
+ }
115
175
  /**
116
176
  * S3-backed store over the caller's personal vault bucket. Accepts only
117
177
  * personal vault buckets (`hq-vault-prs-*`) so a mis-resolved entity can never
118
178
  * receive offloaded bytes.
119
179
  */
120
- export declare function s3BlobStore(bucket: string, s3: OffloadS3Client): BlobStore;
180
+ export declare function s3BlobStore(bucket: string, s3: OffloadS3Client, opts?: S3StoreOptions<OffloadS3Client>): BlobStore;
121
181
  /** Read-only access to vault objects at their sync keys, pinned to an ETag. */
122
182
  export interface VaultObjectStore {
123
- /** Size and unquoted ETag of `key`, or null when it is not there. */
183
+ /**
184
+ * Size and unquoted ETag of `key`, or null when it is not there. `sha256` is
185
+ * the object's `hq-sha256` metadata, set on company-vault offload uploads.
186
+ */
124
187
  head(key: string): Promise<{
125
188
  size: number;
126
189
  etag: string;
190
+ sha256?: string;
127
191
  } | null>;
128
192
  /** Download `key` to `dest` only if its ETag is `etag`; returns the served ETag. */
129
193
  download(key: string, dest: string, etag: string): Promise<string>;
@@ -131,7 +195,7 @@ export interface VaultObjectStore {
131
195
  export interface VaultObjectS3Client {
132
196
  send(cmd: GetObjectCommand | HeadObjectCommand): Promise<unknown>;
133
197
  }
134
- export declare function s3VaultObjectStore(bucket: string, s3: VaultObjectS3Client): VaultObjectStore;
198
+ export declare function s3VaultObjectStore(bucket: string, s3: VaultObjectS3Client, opts?: S3StoreOptions<VaultObjectS3Client>): VaultObjectStore;
135
199
  export interface OffloadDeps {
136
200
  hqRoot: string;
137
201
  now: () => Date;
@@ -169,6 +233,55 @@ export interface OffloadDeps {
169
233
  syncRecord?: (target: OffloadTarget) => Promise<SyncRecord | null>;
170
234
  /** File operations for current-file offload (tests inject to see the order). */
171
235
  placeholderFs?: PlaceholderFs;
236
+ /** HQ sync's per-file limit in bytes. Defaults to SYNC_MAX_FILE_BYTES. */
237
+ syncMaxFileBytes?: number;
238
+ /**
239
+ * Whether HQ sync's ignore rules exclude an HQ-root-relative path, and which
240
+ * rule does (hq-cloud createIgnoreExplainer). Null or absent: not excluded.
241
+ */
242
+ syncExclusion?: (rel: string) => SyncExclusion | null;
243
+ /**
244
+ * Open a multipart uploader for a company vault. Called only for a company
245
+ * the caller is an active member of. Absent: company files over the sync
246
+ * limit report "update HQ sync".
247
+ */
248
+ openCompanyUploader?: (slug: string) => Promise<CompanyUploader>;
249
+ /**
250
+ * Record `target` in the sync journal as synced and cloud-only (hq-cloud
251
+ * recordOffloadedObject), so sync neither re-uploads it nor pulls it back.
252
+ */
253
+ recordCloudOnly?: (target: OffloadTarget, object: VerifiedUpload) => Promise<void>;
254
+ /** Folder for resumable multipart upload state. Defaults to ~/.hq/offload-uploads. */
255
+ uploadStateDir?: () => string;
256
+ }
257
+ /** Why HQ sync skips a path: the ignore layer that excludes it. */
258
+ export interface SyncExclusion {
259
+ excluded: boolean;
260
+ /** "built-in", "defaults", ".hqignore", ".hqsyncignore" or ".hqinclude allowlist". */
261
+ layer: string | null;
262
+ /** For the .hqinclude allowlist: the line that would include the path. */
263
+ includeLine?: string;
264
+ }
265
+ /** A vault object whose size and sha256 were verified after upload. */
266
+ export interface VerifiedUpload {
267
+ etag: string;
268
+ size: number;
269
+ sha256: string;
270
+ }
271
+ /**
272
+ * Resumable, checksum-verified multipart upload into one company vault
273
+ * (hq-cloud uploadLargeObjectVerified). `upload` resumes from `stateFile`
274
+ * when it holds an upload for the same file, and resolves only after the
275
+ * object's size, part count and sha256 metadata are verified.
276
+ */
277
+ export interface CompanyUploader {
278
+ slug: string;
279
+ bucket: string;
280
+ upload(input: {
281
+ key: string;
282
+ file: string;
283
+ stateFile: string;
284
+ }): Promise<VerifiedUpload>;
172
285
  }
173
286
  /** The vault that owns a working-tree path, and the object key there. */
174
287
  export type OffloadTarget = {
@@ -273,7 +386,15 @@ export interface CurrentFilePlan {
273
386
  company?: string;
274
387
  /** HQ sync already stored this exact version in the owning vault. */
275
388
  synced: boolean;
276
- action: "point-at-vault" | "upload" | "skip";
389
+ /**
390
+ * upload+offload: a company file over the sync limit, uploaded to the
391
+ * company vault at `vault_key`, then replaced by a placeholder.
392
+ */
393
+ action: "point-at-vault" | "upload" | "upload+offload" | "skip";
394
+ /** Company vault key for upload+offload. */
395
+ vault_key?: string;
396
+ /** The HQ sync ignore layer that excludes this path. */
397
+ excluded_by?: string;
277
398
  reason?: string;
278
399
  }
279
400
  export interface CurrentOffloaded {
@@ -281,7 +402,7 @@ export interface CurrentOffloaded {
281
402
  bytes: number;
282
403
  vault: "personal" | "company";
283
404
  company?: string;
284
- action: "point-at-vault" | "upload";
405
+ action: "point-at-vault" | "upload" | "upload+offload";
285
406
  }
286
407
  export interface OffloadOptions {
287
408
  minBytes: number;
@@ -405,13 +526,4 @@ export declare function getOffloadStatus(deps: OffloadDeps, opts: {
405
526
  export declare function gitExportBlob(hqRoot: string): (sha: string, dest: string) => Promise<void>;
406
527
  export declare function gitListPlaceholders(hqRoot: string): () => Promise<string[]>;
407
528
  export declare function defaultTmpDir(): string;
408
- export declare function personalS3Store(input: {
409
- bucket: string;
410
- region: string;
411
- credentials: {
412
- accessKeyId: string;
413
- secretAccessKey: string;
414
- sessionToken: string;
415
- };
416
- }): BlobStore;
417
529
  //# sourceMappingURL=storage-offload.d.ts.map