@indigoai-us/hq-cloud 6.14.28 → 6.14.29

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 (85) hide show
  1. package/dist/bin/sync-runner-company.d.ts +4 -0
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  3. package/dist/bin/sync-runner-company.js +3 -0
  4. package/dist/bin/sync-runner-company.js.map +1 -1
  5. package/dist/bin/sync-runner-planning.d.ts +7 -6
  6. package/dist/bin/sync-runner-planning.d.ts.map +1 -1
  7. package/dist/bin/sync-runner-planning.js +1 -1
  8. package/dist/bin/sync-runner-planning.js.map +1 -1
  9. package/dist/bin/sync-runner-planning.test.js +23 -0
  10. package/dist/bin/sync-runner-planning.test.js.map +1 -1
  11. package/dist/bin/sync-runner-rollup.d.ts.map +1 -1
  12. package/dist/bin/sync-runner-rollup.js +3 -1
  13. package/dist/bin/sync-runner-rollup.js.map +1 -1
  14. package/dist/bin/sync-runner-rollup.test.d.ts +2 -0
  15. package/dist/bin/sync-runner-rollup.test.d.ts.map +1 -0
  16. package/dist/bin/sync-runner-rollup.test.js +24 -0
  17. package/dist/bin/sync-runner-rollup.test.js.map +1 -0
  18. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  19. package/dist/bin/sync-runner-watch-loop.js +15 -1
  20. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  21. package/dist/bin/sync-runner.d.ts +11 -7
  22. package/dist/bin/sync-runner.d.ts.map +1 -1
  23. package/dist/bin/sync-runner.js +51 -9
  24. package/dist/bin/sync-runner.js.map +1 -1
  25. package/dist/bin/sync-runner.test.js +97 -0
  26. package/dist/bin/sync-runner.test.js.map +1 -1
  27. package/dist/cli/doctor.d.ts.map +1 -1
  28. package/dist/cli/doctor.js +3 -1
  29. package/dist/cli/doctor.js.map +1 -1
  30. package/dist/cli/reindex.d.ts.map +1 -1
  31. package/dist/cli/reindex.js +24 -13
  32. package/dist/cli/reindex.js.map +1 -1
  33. package/dist/cli/rescue-core.js +3 -1
  34. package/dist/cli/rescue-core.js.map +1 -1
  35. package/dist/cli/share.d.ts +35 -0
  36. package/dist/cli/share.d.ts.map +1 -1
  37. package/dist/cli/share.js +29 -11
  38. package/dist/cli/share.js.map +1 -1
  39. package/dist/cli/share.test.js +46 -0
  40. package/dist/cli/share.test.js.map +1 -1
  41. package/dist/ignore.d.ts.map +1 -1
  42. package/dist/ignore.js +5 -0
  43. package/dist/ignore.js.map +1 -1
  44. package/dist/ignore.test.js +9 -0
  45. package/dist/ignore.test.js.map +1 -1
  46. package/dist/outcome-telemetry.d.ts +167 -0
  47. package/dist/outcome-telemetry.d.ts.map +1 -0
  48. package/dist/outcome-telemetry.js +479 -0
  49. package/dist/outcome-telemetry.js.map +1 -0
  50. package/dist/outcome-telemetry.test.d.ts +9 -0
  51. package/dist/outcome-telemetry.test.d.ts.map +1 -0
  52. package/dist/outcome-telemetry.test.js +412 -0
  53. package/dist/outcome-telemetry.test.js.map +1 -0
  54. package/dist/qmd-reindex.d.ts +48 -35
  55. package/dist/qmd-reindex.d.ts.map +1 -1
  56. package/dist/qmd-reindex.js +188 -60
  57. package/dist/qmd-reindex.js.map +1 -1
  58. package/dist/qmd-reindex.test.d.ts +3 -3
  59. package/dist/qmd-reindex.test.js +203 -41
  60. package/dist/qmd-reindex.test.js.map +1 -1
  61. package/dist/vault-client.d.ts +34 -0
  62. package/dist/vault-client.d.ts.map +1 -1
  63. package/dist/vault-client.js +23 -0
  64. package/dist/vault-client.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/bin/sync-runner-company.ts +4 -0
  67. package/src/bin/sync-runner-planning.test.ts +26 -0
  68. package/src/bin/sync-runner-planning.ts +8 -7
  69. package/src/bin/sync-runner-rollup.test.ts +37 -0
  70. package/src/bin/sync-runner-rollup.ts +3 -1
  71. package/src/bin/sync-runner-watch-loop.ts +22 -1
  72. package/src/bin/sync-runner.test.ts +111 -0
  73. package/src/bin/sync-runner.ts +62 -17
  74. package/src/cli/doctor.ts +3 -1
  75. package/src/cli/reindex.ts +24 -12
  76. package/src/cli/rescue-core.ts +3 -1
  77. package/src/cli/share.test.ts +60 -0
  78. package/src/cli/share.ts +29 -11
  79. package/src/ignore.test.ts +10 -0
  80. package/src/ignore.ts +5 -0
  81. package/src/outcome-telemetry.test.ts +498 -0
  82. package/src/outcome-telemetry.ts +639 -0
  83. package/src/qmd-reindex.test.ts +226 -40
  84. package/src/qmd-reindex.ts +209 -61
  85. package/src/vault-client.ts +55 -0
@@ -107,6 +107,7 @@ import {
107
107
  import { HQ_CLOUD_VERSION } from "../version.js";
108
108
  import { collectAndSendTelemetry } from "../telemetry.js";
109
109
  import { collectAndSendSkillTelemetry } from "../skill-telemetry.js";
110
+ import { collectAndSendOutcomeTelemetry } from "../outcome-telemetry.js";
110
111
  import { emitCloudTelemetry } from "../telemetry-events.js";
111
112
  import { reindexAfterSync } from "../qmd-reindex.js";
112
113
  import type { ReindexOptions as QmdReindexOptions } from "../qmd-reindex.js";
@@ -644,13 +645,12 @@ interface IdTokenClaims {
644
645
  given_name?: string;
645
646
  family_name?: string;
646
647
  /**
647
- * Entity-bound machine-identity claims. A headless agent box authenticates
648
- * with machine creds (username `machine-agt_*`); its idToken carries the
649
- * agent's own entity binding here. The vault authorizer reads these to
650
- * resolve the caller AS the agent. The runner reads them to drive
651
- * agent-aware `--personal` target resolution: when `custom:entityType ===
652
- * "agent"`, the personal slot resolves to the agent's OWN entity
653
- * (`custom:entityUid`, `agt_*`) instead of the person-only canonical pick.
648
+ * Entity-bound machine-principal claims. A headless agent or Outpost
649
+ * authenticates with machine credentials; its idToken carries the machine's
650
+ * own entity binding here. The runner preserves those authenticated claims
651
+ * during planning. Agent claims additionally drive `--personal` target
652
+ * resolution to the agent's OWN entity (`custom:entityUid`, `agt_*`) instead
653
+ * of the person-only canonical pick.
654
654
  */
655
655
  "custom:entityType"?: string;
656
656
  "custom:entityUid"?: string;
@@ -725,6 +725,8 @@ export interface RunnerPassResult {
725
725
  operation: "delete" | "tombstone";
726
726
  reason?: string;
727
727
  }>;
728
+ /** Company legs that threw before finishing their push/pull work. */
729
+ companyFailures?: Array<{ company: string; message: string }>;
728
730
  }
729
731
 
730
732
  export interface RunnerPassOutcome {
@@ -1176,6 +1178,37 @@ export async function defaultCollectTelemetry(
1176
1178
  });
1177
1179
  // Fire-and-forget; nothing escapes the boundary.
1178
1180
  }
1181
+
1182
+ // Outcome telemetry runs as an independent pass (own cursor, own endpoint,
1183
+ // outcome-leaderboard US-004) so a failure here can never affect token or
1184
+ // skill telemetry above. It diffs prd.json story `passes` + board.json project
1185
+ // `status` transitions against a local cursor and POSTs story-completed /
1186
+ // project-shipped events. No machineId/installerVersion — the /v1/outcome-events
1187
+ // contract accepts only { events }.
1188
+ try {
1189
+ await collectAndSendOutcomeTelemetry({
1190
+ client: client as unknown as Parameters<typeof collectAndSendOutcomeTelemetry>[0]["client"],
1191
+ // Scan <hqRoot>/companies/* for prd.json/board.json and resolve each
1192
+ // project's owning company via <hqRoot>/companies/manifest.yaml.
1193
+ hqRoot,
1194
+ log: (message) =>
1195
+ reportDiagnostic({
1196
+ component: "telemetry",
1197
+ event: "runner.telemetry.outcome_nonfatal",
1198
+ message,
1199
+ context: { hqRoot },
1200
+ }),
1201
+ });
1202
+ } catch (err) {
1203
+ reportDiagnostic({
1204
+ component: "telemetry",
1205
+ event: "runner.telemetry.outcome_failed",
1206
+ message: "outcome telemetry collector threw",
1207
+ err,
1208
+ context: { hqRoot },
1209
+ });
1210
+ // Fire-and-forget; nothing escapes the boundary.
1211
+ }
1179
1212
  }
1180
1213
 
1181
1214
  // ---------------------------------------------------------------------------
@@ -1285,19 +1318,22 @@ export async function runRunner(
1285
1318
  // Resolved here (not inside `parsed.companies`) so single-company runs also
1286
1319
  // get author attribution. `null` is fine — share() simply omits the metadata.
1287
1320
  const getClaims = deps.getIdTokenClaims ?? defaultGetIdTokenClaims;
1288
- // A machine token is minted immediately above and is an ID token. Make its
1289
- // entity binding authoritative for this run: a stale human token cache can
1290
- // otherwise make an authenticated agent look like a person during fanout
1291
- // planning, which drops the agent's own personal-vault target. Human callers
1292
- // still retain the cache as their primary claims source because their bearer
1293
- // is normally an access token without the full ID-token profile.
1321
+ // A machine-principal token is minted immediately above and is an ID token.
1322
+ // Make its entity binding authoritative for this run: a stale human token
1323
+ // cache can otherwise make an authenticated machine look like a person
1324
+ // during fanout planning. Human callers still retain the cache as their
1325
+ // primary claims source because their bearer is normally an access token
1326
+ // without the full ID-token profile.
1294
1327
  const authenticatedClaims = decodeJwtClaims(initialAuthToken);
1295
- const authenticatedMachineClaims =
1296
- authenticatedClaims?.["custom:entityType"] === "agent" &&
1328
+ const authenticatedMachinePrincipalClaims =
1329
+ authenticatedClaims &&
1330
+ (authenticatedClaims["custom:entityType"] === "agent" ||
1331
+ authenticatedClaims["custom:entityType"] === "outpost") &&
1297
1332
  typeof authenticatedClaims["custom:entityUid"] === "string"
1298
1333
  ? authenticatedClaims
1299
1334
  : null;
1300
- const claims = authenticatedMachineClaims ?? getClaims() ?? authenticatedClaims;
1335
+ const claims =
1336
+ authenticatedMachinePrincipalClaims ?? getClaims() ?? authenticatedClaims;
1301
1337
  const uploadAuthor: UploadAuthor | undefined =
1302
1338
  claims?.sub && claims?.email
1303
1339
  ? { userSub: claims.sub, email: claims.email }
@@ -1513,7 +1549,10 @@ export async function runRunner(
1513
1549
  throw err;
1514
1550
  }
1515
1551
  const { errors, allConflicts, meaningfulOutcomeCount } = fanout;
1516
- deps.onPassResult?.({ pushPathResults: fanout.pushPathResults });
1552
+ deps.onPassResult?.({
1553
+ pushPathResults: fanout.pushPathResults,
1554
+ companyFailures: fanout.companyFailures,
1555
+ });
1517
1556
  const rollup = rollupAllComplete(plan, fanout.stateByCompany);
1518
1557
 
1519
1558
  // The personal-vault target runs last and can pull an older copy of the
@@ -1758,9 +1797,15 @@ export async function runRunner(
1758
1797
  changedCount: qmdChangedPaths.length,
1759
1798
  qmdState: qmdResult.qmdAvailable ? "available" : "unavailable",
1760
1799
  collectionsAddedCount: qmdResult.collectionsAdded.length,
1800
+ pathDriftDetectedCount: qmdResult.pathDriftDetected?.length ?? 0,
1801
+ collectionsRepairedCount: qmdResult.collectionsRepaired?.length ?? 0,
1761
1802
  updateState: qmdResult.updated ? "updated" : "unchanged",
1762
1803
  embedState: qmdResult.embedded ? "embedded" : "not_embedded",
1763
1804
  dirtyState: qmdResult.pendingDirty ? "pending_dirty" : "clean",
1805
+ lockBusy: qmdResult.lockBusy === true ? 1 : 0,
1806
+ timedOut: qmdResult.timedOut === true ? 1 : 0,
1807
+ corruptionQuarantined: qmdResult.corruptionQuarantined === true ? 1 : 0,
1808
+ corruptionQuarantineFailed: qmdResult.corruptionQuarantineFailed === true ? 1 : 0,
1764
1809
  },
1765
1810
  },
1766
1811
  {
package/src/cli/doctor.ts CHANGED
@@ -191,7 +191,9 @@ async function listSkillObjects(
191
191
  prefix: SKILLS_KEY_PREFIX,
192
192
  continuationToken,
193
193
  });
194
- objects.push(...page.objects);
194
+ for (const object of page.objects) {
195
+ objects.push(object);
196
+ }
195
197
  continuationToken = page.nextContinuationToken;
196
198
  } while (continuationToken);
197
199
  return objects;
@@ -377,22 +377,34 @@ function copyCodexSessionLogs(root: string, destBase: string): void {
377
377
  }
378
378
  }
379
379
 
380
- // Recursively collect every regular file under `dir` (absolute paths). Symlinks
381
- // are not followed (lstat gate) so a stray link can't escape the tree or loop.
382
- // Missing/unreadable dirs yield []. Best-effort — mirrors globEntries' tolerance.
380
+ // Collect every regular file under `dir` (absolute paths) in depth-first order.
381
+ // Symlinks are not followed (lstat gate) so a stray link can't escape the tree
382
+ // or loop. Missing/unreadable dirs yield []. Best-effort — mirrors
383
+ // globEntries' tolerance.
383
384
  function walkFiles(dir: string): string[] {
384
385
  const out: string[] = [];
385
- let names: string[];
386
- try {
387
- names = fs.readdirSync(dir);
388
- } catch {
389
- return out;
390
- }
391
- for (const name of names) {
392
- const abs = path.join(dir, name);
386
+ const stack: Array<{ names: string[]; index: number; dir: string }> = [];
387
+ const enterDirectory = (currentDir: string): void => {
388
+ try {
389
+ stack.push({ names: fs.readdirSync(currentDir), index: 0, dir: currentDir });
390
+ } catch {
391
+ // Missing and unreadable children are ignored just like the recursive
392
+ // walker did when its nested call returned an empty list.
393
+ }
394
+ };
395
+
396
+ enterDirectory(dir);
397
+ while (stack.length > 0) {
398
+ const frame = stack[stack.length - 1];
399
+ if (frame.index >= frame.names.length) {
400
+ stack.pop();
401
+ continue;
402
+ }
403
+ const name = frame.names[frame.index++];
404
+ const abs = path.join(frame.dir, name);
393
405
  const st = lstatOrNull(abs);
394
406
  if (!st) continue;
395
- if (st.isDirectory()) out.push(...walkFiles(abs));
407
+ if (st.isDirectory()) enterDirectory(abs);
396
408
  else if (st.isFile()) out.push(abs);
397
409
  }
398
410
  return out;
@@ -1432,7 +1432,9 @@ function writeApplyFailureManifest(
1432
1432
  lines.push(`Error: ${message}`);
1433
1433
  lines.push("");
1434
1434
  lines.push("## Completed Before Failure");
1435
- lines.push(...completedLines);
1435
+ for (const line of completedLines) {
1436
+ lines.push(line);
1437
+ }
1436
1438
  lines.push("");
1437
1439
  lines.push("## Failed Action Paths");
1438
1440
  if (failedAction.affectedRels.length > 0) {
@@ -9,6 +9,13 @@ import * as os from "os";
9
9
  import { clearContextCache } from "../context.js";
10
10
  import type { VaultServiceConfig } from "../types.js";
11
11
 
12
+ // Re-export node:fs as a mutable module so the large-tree regression can
13
+ // replace only readdirSync; normal tests retain the real implementation.
14
+ vi.mock("fs", async (importOriginal) => {
15
+ const actual = await importOriginal<typeof import("fs")>();
16
+ return { ...actual };
17
+ });
18
+
12
19
  // Mock s3 module at the top level. uploadFile resolves to a synthetic ETag
13
20
  // so share() can record it on the journal entry — the real PutObject
14
21
  // response shape is `{ ETag: '"<hex>"' }`. uploadSymlink is the symlink-
@@ -259,6 +266,59 @@ describe("share", () => {
259
266
  expect(result.filesUploaded).toBe(2);
260
267
  });
261
268
 
269
+ it("walks a very large directory without using its entries as call arguments", () => {
270
+ const companyRoot = path.join(tmpDir, "companies", "acme");
271
+ fs.mkdirSync(companyRoot, { recursive: true });
272
+ const nestedDir = path.join(companyRoot, "nested");
273
+ fs.mkdirSync(nestedDir);
274
+ const entryCount = 150_000;
275
+ const fileEntry = (name: string) => ({
276
+ name,
277
+ isDirectory: () => false,
278
+ isFile: () => true,
279
+ isSymbolicLink: () => false,
280
+ });
281
+ const rootEntries = [
282
+ fileEntry("before.md"),
283
+ {
284
+ name: "nested",
285
+ isDirectory: () => true,
286
+ isFile: () => false,
287
+ isSymbolicLink: () => false,
288
+ },
289
+ ...Array.from(
290
+ { length: entryCount - 2 },
291
+ (_, index) => fileEntry(`entry-${index}.md`),
292
+ ),
293
+ ] as unknown as fs.Dirent[];
294
+ const nestedEntries = [fileEntry("child.md")] as unknown as fs.Dirent[];
295
+ const readdirSpy = vi
296
+ .spyOn(fs, "readdirSync")
297
+ .mockImplementation(
298
+ ((dir: fs.PathLike) =>
299
+ path.resolve(String(dir)) === nestedDir ? nestedEntries : rootEntries) as unknown as typeof fs.readdirSync,
300
+ );
301
+
302
+ try {
303
+ const collected = shareTesting.collectFiles(
304
+ [companyRoot],
305
+ tmpDir,
306
+ companyRoot,
307
+ () => true,
308
+ );
309
+
310
+ expect(collected).toHaveLength(entryCount);
311
+ expect(collected[0]).toMatchObject({ relativePath: "before.md" });
312
+ expect(collected[1]).toMatchObject({ relativePath: "nested/child.md" });
313
+ expect(collected[2]).toMatchObject({ relativePath: "entry-0.md" });
314
+ expect(collected.at(-1)).toMatchObject({
315
+ relativePath: `entry-${entryCount - 3}.md`,
316
+ });
317
+ } finally {
318
+ readdirSpy.mockRestore();
319
+ }
320
+ });
321
+
262
322
  it("keys nested paths relative to the company root, not hqRoot", async () => {
263
323
  const companyRoot = path.join(tmpDir, "companies", "acme");
264
324
  fs.mkdirSync(path.join(companyRoot, "knowledge"), { recursive: true });
package/src/cli/share.ts CHANGED
@@ -270,6 +270,7 @@ export const _testing = {
270
270
  isEphemeralPath,
271
271
  EPHEMERAL_PATH_PATTERN,
272
272
  wrapFilterWithIgnoreVisibility,
273
+ collectFiles,
273
274
  };
274
275
 
275
276
  /**
@@ -1965,7 +1966,7 @@ function collectFiles(
1965
1966
 
1966
1967
  if (lstat.isDirectory()) {
1967
1968
  if (!filter(absolutePath, true)) continue;
1968
- results.push(...walkDir(absolutePath, syncRoot, filter));
1969
+ walkDir(absolutePath, syncRoot, filter, results);
1969
1970
  } else if (lstat.isFile()) {
1970
1971
  const relativePath = vaultKeyForLocalPath(syncRoot, absolutePath);
1971
1972
  if (filter(absolutePath)) {
@@ -1981,18 +1982,35 @@ function walkDir(
1981
1982
  dir: string,
1982
1983
  syncRoot: string,
1983
1984
  filter: (p: string, isDir?: boolean) => boolean,
1984
- ): CollectedEntry[] {
1985
- const results: CollectedEntry[] = [];
1986
- if (!fs.existsSync(dir)) return results;
1985
+ results: CollectedEntry[],
1986
+ ): void {
1987
+ // A frame per open directory preserves the recursive walk's depth-first
1988
+ // ordering without turning a completed subtree into one giant call argument
1989
+ // list. This matters for both operator-visible plan ordering and large trees.
1990
+ const stack: Array<{ dir: string; entries: fs.Dirent[]; index: number }> = [];
1991
+ const enterDirectory = (currentDir: string): void => {
1992
+ if (!fs.existsSync(currentDir)) return;
1993
+ stack.push({
1994
+ dir: currentDir,
1995
+ entries: fs.readdirSync(currentDir, { withFileTypes: true }),
1996
+ index: 0,
1997
+ });
1998
+ };
1987
1999
 
1988
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1989
- for (const entry of entries) {
2000
+ enterDirectory(dir);
2001
+ while (stack.length > 0) {
2002
+ const frame = stack[stack.length - 1];
2003
+ if (frame.index >= frame.entries.length) {
2004
+ stack.pop();
2005
+ continue;
2006
+ }
2007
+ const entry = frame.entries[frame.index++];
1990
2008
  // Ephemeral artifacts (conflict mirrors) are local-only safety backups
1991
2009
  // that MUST NEVER round-trip to S3. Check basename here so the filter
1992
2010
  // applies regardless of which company root contains them. See
1993
2011
  // EPHEMERAL_PATH_PATTERN doc for the full rationale.
1994
2012
  if (isEphemeralPath(entry.name)) continue;
1995
- const absolutePath = path.join(dir, entry.name);
2013
+ const absolutePath = path.join(frame.dir, entry.name);
1996
2014
  const isDir = entry.isDirectory();
1997
2015
 
1998
2016
  // Symlinks need their own filter probe BEFORE the regular gate.
@@ -2028,7 +2046,7 @@ function walkDir(
2028
2046
  if (!filter(absolutePath, isDir)) continue;
2029
2047
 
2030
2048
  if (isDir) {
2031
- results.push(...walkDir(absolutePath, syncRoot, filter));
2049
+ enterDirectory(absolutePath);
2032
2050
  } else if (entry.isFile()) {
2033
2051
  results.push({
2034
2052
  kind: "file",
@@ -2037,8 +2055,6 @@ function walkDir(
2037
2055
  });
2038
2056
  }
2039
2057
  }
2040
-
2041
- return results;
2042
2058
  }
2043
2059
 
2044
2060
  function isWithin(parent: string, child: string): boolean {
@@ -2658,7 +2674,9 @@ async function computeDeletePlan(
2658
2674
  // delete loop tombstones each key identically (DeleteObject + remove from
2659
2675
  // journal). Merged at the end so the bulk-asymmetry breaker above had its
2660
2676
  // chance to NOT include litter in either the numerator or the sweep.
2661
- plan.toDelete.push(...litterToDelete.map((key) => ({ key, intentVersion: null })));
2677
+ for (const key of litterToDelete) {
2678
+ plan.toDelete.push({ key, intentVersion: null });
2679
+ }
2662
2680
 
2663
2681
  return plan;
2664
2682
  }
@@ -57,6 +57,16 @@ describe("createIgnoreFilter", () => {
57
57
  expect(shouldSync(path.join(hqRoot, ".env"))).toBe(false);
58
58
  });
59
59
 
60
+ it("permissive mode: project-local .qmd index is ignored by default", () => {
61
+ // feedback_332c7ccc / sqlite-vec#297 — never round-trip a project-local
62
+ // qmd index through the vault.
63
+ const shouldSync = createIgnoreFilter(hqRoot);
64
+ expect(shouldSync(path.join(hqRoot, ".qmd"), true)).toBe(false);
65
+ expect(shouldSync(path.join(hqRoot, ".qmd/index.sqlite"))).toBe(false);
66
+ expect(shouldSync(path.join(hqRoot, ".qmd/index.sqlite-wal"))).toBe(false);
67
+ expect(isExpectedIgnore(".qmd/index.sqlite")).toBe(true);
68
+ });
69
+
60
70
  it("permissive mode: pnpm content-addressed store is ignored by default", () => {
61
71
  // .pnpm-store/ is pnpm's content-addressed package store. Like
62
72
  // node_modules/ it is large and machine-local and must never sync — a
package/src/ignore.ts CHANGED
@@ -79,6 +79,10 @@ export const DEFAULT_IGNORES = [
79
79
 
80
80
  // Generic caches / temp
81
81
  ".cache/",
82
+ // Project-local qmd index (qmd init). Must never round-trip through the vault —
83
+ // sqlite-vec vec0 tables corrupt when copied across machines (feedback_332c7ccc /
84
+ // sqlite-vec#297). Default global index at ~/.cache/qmd is already outside hqRoot.
85
+ ".qmd/",
82
86
  "tmp/",
83
87
  ".tmp/",
84
88
 
@@ -263,6 +267,7 @@ export const EXPECTED_IGNORE_SEGMENTS = new Set([
263
267
  "vendor",
264
268
  "out",
265
269
  ".cache",
270
+ ".qmd",
266
271
  "tmp",
267
272
  ".tmp",
268
273
  ".hq",