@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.
- package/dist/bin/sync-runner-company.d.ts +4 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +3 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +7 -6
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +1 -1
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +23 -0
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-rollup.d.ts.map +1 -1
- package/dist/bin/sync-runner-rollup.js +3 -1
- package/dist/bin/sync-runner-rollup.js.map +1 -1
- package/dist/bin/sync-runner-rollup.test.d.ts +2 -0
- package/dist/bin/sync-runner-rollup.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-rollup.test.js +24 -0
- package/dist/bin/sync-runner-rollup.test.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +15 -1
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +11 -7
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +51 -9
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +97 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +24 -13
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/rescue-core.js +3 -1
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.d.ts +35 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +29 -11
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +46 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +5 -0
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +9 -0
- package/dist/ignore.test.js.map +1 -1
- package/dist/outcome-telemetry.d.ts +167 -0
- package/dist/outcome-telemetry.d.ts.map +1 -0
- package/dist/outcome-telemetry.js +479 -0
- package/dist/outcome-telemetry.js.map +1 -0
- package/dist/outcome-telemetry.test.d.ts +9 -0
- package/dist/outcome-telemetry.test.d.ts.map +1 -0
- package/dist/outcome-telemetry.test.js +412 -0
- package/dist/outcome-telemetry.test.js.map +1 -0
- package/dist/qmd-reindex.d.ts +48 -35
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +188 -60
- package/dist/qmd-reindex.js.map +1 -1
- package/dist/qmd-reindex.test.d.ts +3 -3
- package/dist/qmd-reindex.test.js +203 -41
- package/dist/qmd-reindex.test.js.map +1 -1
- package/dist/vault-client.d.ts +34 -0
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +23 -0
- package/dist/vault-client.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +4 -0
- package/src/bin/sync-runner-planning.test.ts +26 -0
- package/src/bin/sync-runner-planning.ts +8 -7
- package/src/bin/sync-runner-rollup.test.ts +37 -0
- package/src/bin/sync-runner-rollup.ts +3 -1
- package/src/bin/sync-runner-watch-loop.ts +22 -1
- package/src/bin/sync-runner.test.ts +111 -0
- package/src/bin/sync-runner.ts +62 -17
- package/src/cli/doctor.ts +3 -1
- package/src/cli/reindex.ts +24 -12
- package/src/cli/rescue-core.ts +3 -1
- package/src/cli/share.test.ts +60 -0
- package/src/cli/share.ts +29 -11
- package/src/ignore.test.ts +10 -0
- package/src/ignore.ts +5 -0
- package/src/outcome-telemetry.test.ts +498 -0
- package/src/outcome-telemetry.ts +639 -0
- package/src/qmd-reindex.test.ts +226 -40
- package/src/qmd-reindex.ts +209 -61
- package/src/vault-client.ts +55 -0
package/src/bin/sync-runner.ts
CHANGED
|
@@ -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-
|
|
648
|
-
* with machine
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
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.
|
|
1289
|
-
// entity binding authoritative for this run: a stale human token
|
|
1290
|
-
// otherwise make an authenticated
|
|
1291
|
-
// planning
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
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
|
|
1296
|
-
authenticatedClaims
|
|
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 =
|
|
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?.({
|
|
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
|
-
|
|
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;
|
package/src/cli/reindex.ts
CHANGED
|
@@ -377,22 +377,34 @@ function copyCodexSessionLogs(root: string, destBase: string): void {
|
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
//
|
|
381
|
-
// are not followed (lstat gate) so a stray link can't escape the tree
|
|
382
|
-
// Missing/unreadable dirs yield []. Best-effort — mirrors
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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())
|
|
407
|
+
if (st.isDirectory()) enterDirectory(abs);
|
|
396
408
|
else if (st.isFile()) out.push(abs);
|
|
397
409
|
}
|
|
398
410
|
return out;
|
package/src/cli/rescue-core.ts
CHANGED
|
@@ -1432,7 +1432,9 @@ function writeApplyFailureManifest(
|
|
|
1432
1432
|
lines.push(`Error: ${message}`);
|
|
1433
1433
|
lines.push("");
|
|
1434
1434
|
lines.push("## Completed Before Failure");
|
|
1435
|
-
|
|
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) {
|
package/src/cli/share.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
-
|
|
1989
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2677
|
+
for (const key of litterToDelete) {
|
|
2678
|
+
plan.toDelete.push({ key, intentVersion: null });
|
|
2679
|
+
}
|
|
2662
2680
|
|
|
2663
2681
|
return plan;
|
|
2664
2682
|
}
|
package/src/ignore.test.ts
CHANGED
|
@@ -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",
|