@indigoai-us/hq-cloud 6.11.6 → 6.11.7
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.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +1 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +1 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +23 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +54 -0
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +142 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +16 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +1 -62
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/tombstones.d.ts +43 -0
- package/dist/cli/tombstones.d.ts.map +1 -0
- package/dist/cli/tombstones.js +78 -0
- package/dist/cli/tombstones.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/personal-vault.d.ts +36 -0
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +89 -1
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +143 -1
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +22 -1
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +29 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +1 -0
- package/src/bin/sync-runner.ts +1 -0
- package/src/cli/share.test.ts +169 -0
- package/src/cli/share.ts +81 -0
- package/src/cli/sync.ts +21 -88
- package/src/cli/tombstones.ts +106 -0
- package/src/index.ts +2 -0
- package/src/personal-vault.test.ts +175 -0
- package/src/personal-vault.ts +86 -1
- package/src/watcher.test.ts +41 -0
- package/src/watcher.ts +24 -1
|
@@ -16,8 +16,10 @@ import * as fs from "fs";
|
|
|
16
16
|
import * as os from "os";
|
|
17
17
|
import * as path from "path";
|
|
18
18
|
import {
|
|
19
|
+
computeContinuityPointerPaths,
|
|
19
20
|
computePersonalCompanySubdirs,
|
|
20
21
|
computePersonalVaultPaths,
|
|
22
|
+
CONTINUITY_POINTER_REL,
|
|
21
23
|
PERSONAL_VAULT_COMPANY_EXCLUDED_SLUGS,
|
|
22
24
|
PERSONAL_VAULT_EXCLUDED_TOP_LEVEL,
|
|
23
25
|
} from "./personal-vault.js";
|
|
@@ -347,3 +349,176 @@ describe("personal-vault helpers", () => {
|
|
|
347
349
|
).toBe(false);
|
|
348
350
|
});
|
|
349
351
|
});
|
|
352
|
+
|
|
353
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
354
|
+
// DEV-1778 — session-continuity pointer carve-out.
|
|
355
|
+
//
|
|
356
|
+
// `workspace/` is in PERSONAL_VAULT_EXCLUDED_TOP_LEVEL (machine-local by
|
|
357
|
+
// design). The ONE exception is the session pointer
|
|
358
|
+
// `workspace/threads/handoff.json` + the single thread file it references via
|
|
359
|
+
// `thread_path`, so a `/handoff` on one machine reaches a second machine.
|
|
360
|
+
// Mirrors the companies/manifest.yaml special-case: a narrow file-level
|
|
361
|
+
// re-include that never broadens the rest of workspace/.
|
|
362
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
363
|
+
describe("personal-vault: continuity-pointer carve-out", () => {
|
|
364
|
+
let hqRoot: string;
|
|
365
|
+
|
|
366
|
+
beforeEach(() => {
|
|
367
|
+
hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-cont-test-"));
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
afterEach(() => {
|
|
371
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
/** Helper: relative-paths-sorted projection of an absolute-path list. */
|
|
375
|
+
function rel(abs: string[]): string[] {
|
|
376
|
+
return abs.map((p) => path.relative(hqRoot, p)).sort();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const THREADS = path.join("workspace", "threads");
|
|
380
|
+
|
|
381
|
+
/** Write workspace/threads/handoff.json with the given object. */
|
|
382
|
+
function writeHandoff(obj: unknown, raw?: string): void {
|
|
383
|
+
const dir = path.join(hqRoot, THREADS);
|
|
384
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
385
|
+
fs.writeFileSync(
|
|
386
|
+
path.join(dir, "handoff.json"),
|
|
387
|
+
raw !== undefined ? raw : JSON.stringify(obj),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Write a thread file under workspace/threads/ and return its rel path. */
|
|
392
|
+
function writeThread(name: string): string {
|
|
393
|
+
const dir = path.join(hqRoot, THREADS);
|
|
394
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
395
|
+
fs.writeFileSync(path.join(dir, name), "{}");
|
|
396
|
+
return path.join(THREADS, name).split(path.sep).join("/");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
it("constant: CONTINUITY_POINTER_REL is the canonical pointer path", () => {
|
|
400
|
+
expect(CONTINUITY_POINTER_REL).toBe("workspace/threads/handoff.json");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("includes handoff.json + the thread file it points to", () => {
|
|
404
|
+
const threadRel = writeThread("T-20260617-1200-demo.json");
|
|
405
|
+
writeHandoff({ thread_path: threadRel });
|
|
406
|
+
|
|
407
|
+
expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual(
|
|
408
|
+
[
|
|
409
|
+
path.join(THREADS, "T-20260617-1200-demo.json"),
|
|
410
|
+
path.join(THREADS, "handoff.json"),
|
|
411
|
+
].sort(),
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("carve-out composes into computePersonalVaultPaths", () => {
|
|
416
|
+
const threadRel = writeThread("T-20260617-1200-demo.json");
|
|
417
|
+
writeHandoff({ thread_path: threadRel });
|
|
418
|
+
fs.mkdirSync(path.join(hqRoot, ".claude"));
|
|
419
|
+
|
|
420
|
+
const out = rel(computePersonalVaultPaths(hqRoot));
|
|
421
|
+
expect(out).toContain(path.join(THREADS, "handoff.json"));
|
|
422
|
+
expect(out).toContain(path.join(THREADS, "T-20260617-1200-demo.json"));
|
|
423
|
+
// Sibling top-level dirs still travel; the rest of workspace/ does not.
|
|
424
|
+
expect(out).toContain(".claude");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("does NOT broaden the rest of workspace/ (siblings/other threads stay local)", () => {
|
|
428
|
+
const threadRel = writeThread("T-active.json");
|
|
429
|
+
writeThread("T-old.json"); // an inactive thread — must NOT sync
|
|
430
|
+
writeHandoff({ thread_path: threadRel });
|
|
431
|
+
// Unrelated workspace litter that must stay machine-local.
|
|
432
|
+
fs.mkdirSync(path.join(hqRoot, "workspace", "locks"), { recursive: true });
|
|
433
|
+
fs.writeFileSync(path.join(hqRoot, "workspace", "locks", "x.lock"), "1");
|
|
434
|
+
fs.writeFileSync(path.join(hqRoot, "workspace", "threads", "INDEX.md"), "#");
|
|
435
|
+
|
|
436
|
+
const out = rel(computePersonalVaultPaths(hqRoot));
|
|
437
|
+
expect(out).toContain(path.join(THREADS, "handoff.json"));
|
|
438
|
+
expect(out).toContain(path.join(THREADS, "T-active.json"));
|
|
439
|
+
expect(out).not.toContain(path.join(THREADS, "T-old.json"));
|
|
440
|
+
expect(out).not.toContain(path.join(THREADS, "INDEX.md"));
|
|
441
|
+
expect(out.some((p) => p.startsWith(path.join("workspace", "locks")))).toBe(
|
|
442
|
+
false,
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("handoff.json present but thread_path absent → only the pointer", () => {
|
|
447
|
+
writeHandoff({ message: "no pointer field" });
|
|
448
|
+
expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
|
|
449
|
+
path.join(THREADS, "handoff.json"),
|
|
450
|
+
]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("handoff.json points to a non-existent thread file → only the pointer", () => {
|
|
454
|
+
writeHandoff({ thread_path: "workspace/threads/T-ghost.json" });
|
|
455
|
+
expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
|
|
456
|
+
path.join(THREADS, "handoff.json"),
|
|
457
|
+
]);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("no handoff.json at all → empty (nothing to carry)", () => {
|
|
461
|
+
fs.mkdirSync(path.join(hqRoot, THREADS), { recursive: true });
|
|
462
|
+
expect(computeContinuityPointerPaths(hqRoot)).toEqual([]);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("malformed handoff.json → fail-soft to pointer only (no throw)", () => {
|
|
466
|
+
writeHandoff(null, "{ this is : not json ]");
|
|
467
|
+
expect(() => computeContinuityPointerPaths(hqRoot)).not.toThrow();
|
|
468
|
+
expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
|
|
469
|
+
path.join(THREADS, "handoff.json"),
|
|
470
|
+
]);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ── Security: thread_path must never escape workspace/threads/ ──────────
|
|
474
|
+
it("rejects an absolute thread_path (only the pointer is included)", () => {
|
|
475
|
+
// Put a real file at the absolute target so the ONLY thing rejecting it
|
|
476
|
+
// is the containment guard, not a missing-file fallthrough.
|
|
477
|
+
const evil = path.join(hqRoot, "secret.txt");
|
|
478
|
+
fs.writeFileSync(evil, "top secret");
|
|
479
|
+
writeHandoff({ thread_path: evil });
|
|
480
|
+
expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
|
|
481
|
+
path.join(THREADS, "handoff.json"),
|
|
482
|
+
]);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("rejects a traversal thread_path (../../.env) — no smuggling out of threads/", () => {
|
|
486
|
+
fs.writeFileSync(path.join(hqRoot, ".env"), "SECRET=1");
|
|
487
|
+
writeHandoff({ thread_path: "workspace/threads/../../.env" });
|
|
488
|
+
const out = rel(computeContinuityPointerPaths(hqRoot));
|
|
489
|
+
expect(out).toEqual([path.join(THREADS, "handoff.json")]);
|
|
490
|
+
expect(out.some((p) => p.endsWith(".env"))).toBe(false);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("rejects a thread_path under workspace/ but outside threads/", () => {
|
|
494
|
+
fs.mkdirSync(path.join(hqRoot, "workspace", "reports"), {
|
|
495
|
+
recursive: true,
|
|
496
|
+
});
|
|
497
|
+
fs.writeFileSync(
|
|
498
|
+
path.join(hqRoot, "workspace", "reports", "secret.md"),
|
|
499
|
+
"x",
|
|
500
|
+
);
|
|
501
|
+
writeHandoff({ thread_path: "workspace/reports/secret.md" });
|
|
502
|
+
expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
|
|
503
|
+
path.join(THREADS, "handoff.json"),
|
|
504
|
+
]);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("rejects a symlink that escapes threads/ (realpath containment)", () => {
|
|
508
|
+
// A thread_path that names an in-threads symlink whose target is OUTSIDE
|
|
509
|
+
// threads/ must be rejected by the realpath re-check.
|
|
510
|
+
fs.writeFileSync(path.join(hqRoot, "outside.txt"), "secret");
|
|
511
|
+
fs.mkdirSync(path.join(hqRoot, THREADS), { recursive: true });
|
|
512
|
+
const link = path.join(hqRoot, THREADS, "escape.json");
|
|
513
|
+
try {
|
|
514
|
+
fs.symlinkSync(path.join(hqRoot, "outside.txt"), link);
|
|
515
|
+
} catch {
|
|
516
|
+
// Platform without symlink support — skip the assertion gracefully.
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
writeHandoff({ thread_path: "workspace/threads/escape.json" });
|
|
520
|
+
expect(rel(computeContinuityPointerPaths(hqRoot))).toEqual([
|
|
521
|
+
path.join(THREADS, "handoff.json"),
|
|
522
|
+
]);
|
|
523
|
+
});
|
|
524
|
+
});
|
package/src/personal-vault.ts
CHANGED
|
@@ -125,7 +125,92 @@ export function computePersonalVaultPaths(
|
|
|
125
125
|
const companySubdirs = opts.includeLocalCompanies === true
|
|
126
126
|
? computePersonalCompanySubdirs(hqRoot, opts.teamSyncedSlugs)
|
|
127
127
|
: [];
|
|
128
|
-
|
|
128
|
+
const continuity = computeContinuityPointerPaths(hqRoot);
|
|
129
|
+
return [...topLevel, ...manifest, ...companySubdirs, ...continuity];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Fixed relative path (forward-slash, hq-root-relative) of the session
|
|
134
|
+
* continuity pointer. The continuity pointer is the ONE file under the
|
|
135
|
+
* otherwise machine-local `workspace/` that must travel across machines:
|
|
136
|
+
* `/handoff` writes it on machine A so a fresh session on machine B can
|
|
137
|
+
* resume via `/startwork`. See {@link computeContinuityPointerPaths}.
|
|
138
|
+
*/
|
|
139
|
+
export const CONTINUITY_POINTER_REL = "workspace/threads/handoff.json";
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compute the absolute paths of the session-continuity pointer carve-out:
|
|
143
|
+
* `workspace/threads/handoff.json` plus the single thread file it points to.
|
|
144
|
+
*
|
|
145
|
+
* `workspace/` is in {@link PERSONAL_VAULT_EXCLUDED_TOP_LEVEL} — it is
|
|
146
|
+
* machine-local by design (session scratch, locks, reports, the full thread
|
|
147
|
+
* history). The continuity pointer is the one exception: without it a
|
|
148
|
+
* `/handoff` on one machine never reaches a second machine, so the session
|
|
149
|
+
* pointer doesn't follow the user even though the durable output already
|
|
150
|
+
* syncs. This carve-out pierces the exclusion for EXACTLY two files and
|
|
151
|
+
* nothing else, mirroring the `companies/manifest.yaml` special-case above
|
|
152
|
+
* (a single file pushed back in despite its parent dir being excluded).
|
|
153
|
+
*
|
|
154
|
+
* The "active thread file" is not a fixed name — it is resolved from
|
|
155
|
+
* `handoff.json.thread_path` (the pointer the finalize script writes). We
|
|
156
|
+
* read + parse `handoff.json`, then include `thread_path` ONLY when it is a
|
|
157
|
+
* relative path that resolves to an existing regular file strictly within
|
|
158
|
+
* `<hqRoot>/workspace/threads/`. This containment check is a hard security
|
|
159
|
+
* boundary: a malformed or tampered `handoff.json` must never be able to
|
|
160
|
+
* smuggle an arbitrary file (e.g. `../../.env`, an absolute path, or a
|
|
161
|
+
* symlink escaping the threads dir) into the personal vault.
|
|
162
|
+
*
|
|
163
|
+
* Fail-soft throughout: a missing/unreadable/malformed `handoff.json`, or an
|
|
164
|
+
* out-of-bounds / missing `thread_path`, silently degrades to "include
|
|
165
|
+
* whatever is valid" (handoff.json alone, or `[]`). Callers tolerate empty
|
|
166
|
+
* arrays — same contract as the manifest special-case.
|
|
167
|
+
*/
|
|
168
|
+
export function computeContinuityPointerPaths(hqRoot: string): string[] {
|
|
169
|
+
const out: string[] = [];
|
|
170
|
+
const threadsDir = path.join(hqRoot, "workspace", "threads");
|
|
171
|
+
const handoffPath = path.join(threadsDir, "handoff.json");
|
|
172
|
+
|
|
173
|
+
let raw: string;
|
|
174
|
+
try {
|
|
175
|
+
if (!fs.statSync(handoffPath).isFile()) return out;
|
|
176
|
+
raw = fs.readFileSync(handoffPath, "utf8");
|
|
177
|
+
} catch {
|
|
178
|
+
// No pointer on this machine yet (or unreadable) — nothing to carry.
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
out.push(handoffPath);
|
|
182
|
+
|
|
183
|
+
// Resolve the active thread file from the pointer's `thread_path`. Any
|
|
184
|
+
// failure leaves the pointer itself in `out` and skips the thread body —
|
|
185
|
+
// the next handoff (or a peer's push) re-converges it.
|
|
186
|
+
let threadPath: unknown;
|
|
187
|
+
try {
|
|
188
|
+
threadPath = (JSON.parse(raw) as { thread_path?: unknown })?.thread_path;
|
|
189
|
+
} catch {
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
if (typeof threadPath !== "string" || threadPath.length === 0) return out;
|
|
193
|
+
|
|
194
|
+
// Containment: the resolved file must live strictly inside the threads dir.
|
|
195
|
+
// Reject absolute paths, traversal, and symlink escapes. thread_path is
|
|
196
|
+
// hq-root-relative as written by handoff-finalize.sh, so resolve against
|
|
197
|
+
// hqRoot and re-check the realpath is still under the threads dir.
|
|
198
|
+
if (path.isAbsolute(threadPath)) return out;
|
|
199
|
+
const resolvedThreads = path.resolve(threadsDir);
|
|
200
|
+
const candidate = path.resolve(hqRoot, threadPath);
|
|
201
|
+
const withinThreads = (p: string): boolean =>
|
|
202
|
+
p === resolvedThreads || p.startsWith(resolvedThreads + path.sep);
|
|
203
|
+
if (!withinThreads(candidate)) return out;
|
|
204
|
+
try {
|
|
205
|
+
const real = fs.realpathSync(candidate);
|
|
206
|
+
if (!withinThreads(real)) return out;
|
|
207
|
+
if (!fs.statSync(real).isFile()) return out;
|
|
208
|
+
} catch {
|
|
209
|
+
// Pointer references a thread file that doesn't exist here — skip it.
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
out.push(candidate);
|
|
213
|
+
return out;
|
|
129
214
|
}
|
|
130
215
|
|
|
131
216
|
/**
|
package/src/watcher.test.ts
CHANGED
|
@@ -263,6 +263,47 @@ describe("US-002: createWatchPathFilter — personal-vault exclusions", () => {
|
|
|
263
263
|
expect(personal(path.join(ROOT, "repos/private/foo/file.ts"))).toBe(false);
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
+
// ── DEV-1778 continuity-pointer carve-out ───────────────────────────────
|
|
267
|
+
it("DOES emit for workspace/threads/handoff.json (the continuity pointer)", () => {
|
|
268
|
+
expect(personal(path.join(ROOT, "workspace/threads/handoff.json"))).toBe(
|
|
269
|
+
true,
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("still does NOT emit for other workspace/ files (carve-out is pointer-only)", () => {
|
|
274
|
+
// The pointer is the ONLY re-include — sibling threads, the journal, and
|
|
275
|
+
// unrelated workspace litter stay machine-local.
|
|
276
|
+
expect(personal(path.join(ROOT, "workspace/threads/T-old.json"))).toBe(
|
|
277
|
+
false,
|
|
278
|
+
);
|
|
279
|
+
expect(personal(path.join(ROOT, "workspace/threads/INDEX.md"))).toBe(false);
|
|
280
|
+
expect(personal(path.join(ROOT, "workspace/locks/x.lock"))).toBe(false);
|
|
281
|
+
// The pointer queried as a directory is not an emit target either.
|
|
282
|
+
expect(
|
|
283
|
+
personal(path.join(ROOT, "workspace/threads/handoff.json"), true),
|
|
284
|
+
).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("allows chokidar to DESCEND through the pointer's ancestor dirs (dir probe)", () => {
|
|
288
|
+
// Without this the Linux chokidar backend prunes `workspace/` before ever
|
|
289
|
+
// reaching the leaf. Ancestor dirs return true ONLY when queried as dirs.
|
|
290
|
+
expect(personal(path.join(ROOT, "workspace"), true)).toBe(true);
|
|
291
|
+
expect(personal(path.join(ROOT, "workspace/threads"), true)).toBe(true);
|
|
292
|
+
// A non-ancestor dir under workspace/ is still pruned.
|
|
293
|
+
expect(personal(path.join(ROOT, "workspace/locks"), true)).toBe(false);
|
|
294
|
+
// Ancestor dirs queried as FILES are not emit targets.
|
|
295
|
+
expect(personal(path.join(ROOT, "workspace"), false)).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("does not apply the carve-out in non-personal mode", () => {
|
|
299
|
+
// In non-personal mode the excluded-top-level layer never runs, so the
|
|
300
|
+
// carve-out is irrelevant; handoff.json passes via the ordinary ignore
|
|
301
|
+
// stack like any other tracked file (no special-casing needed).
|
|
302
|
+
expect(
|
|
303
|
+
nonPersonal(path.join(ROOT, "workspace/threads/handoff.json")),
|
|
304
|
+
).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
266
307
|
it("DOES emit for an included top-level personal path in personalMode", () => {
|
|
267
308
|
expect(personal(path.join(ROOT, "personal/notes.md"))).toBe(true);
|
|
268
309
|
expect(personal(path.join(ROOT, "core/policies/x.md"))).toBe(true);
|
package/src/watcher.ts
CHANGED
|
@@ -19,7 +19,10 @@ import { readJournal, writeJournal, hashFile, updateEntry } from "./journal.js";
|
|
|
19
19
|
import { uploadFile, deleteRemoteFile, toPosixKey } from "./s3.js";
|
|
20
20
|
import type { UploadAuthor } from "./s3.js";
|
|
21
21
|
import { isPersonalVaultExcluded } from "./personal-vault-exclusions.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
CONTINUITY_POINTER_REL,
|
|
24
|
+
PERSONAL_VAULT_EXCLUDED_TOP_LEVEL,
|
|
25
|
+
} from "./personal-vault.js";
|
|
23
26
|
import type { PushEvent } from "./sync/push-event.js";
|
|
24
27
|
import type { PushTransport } from "./sync/push-transport.js";
|
|
25
28
|
import type { EventDrivenPushFlagProvider } from "./sync/feature-flags.js";
|
|
@@ -498,6 +501,26 @@ export function createWatchPathFilter(
|
|
|
498
501
|
if (!ignoreFilter(absolutePath, isDir)) return false;
|
|
499
502
|
|
|
500
503
|
if (personalMode) {
|
|
504
|
+
// Continuity-pointer carve-out: the session pointer lives under the
|
|
505
|
+
// otherwise-excluded `workspace/`, so it must re-include BEFORE the
|
|
506
|
+
// top-level bucket rejection below. Two cases:
|
|
507
|
+
// (a) the pointer FILE itself → emit. Waking on the pointer is
|
|
508
|
+
// sufficient: `/handoff` rewrites handoff.json AFTER its
|
|
509
|
+
// (immutable) thread file, and the resulting personal push
|
|
510
|
+
// re-enumerates via computePersonalVaultPaths, which sweeps the
|
|
511
|
+
// pointer + its active thread file together.
|
|
512
|
+
// (b) an ANCESTOR DIR of the pointer (`workspace`, `workspace/threads`)
|
|
513
|
+
// queried as a directory → allow descent. The Linux chokidar
|
|
514
|
+
// backend prunes a subtree the instant its dir probe says "ignore",
|
|
515
|
+
// so without this it would prune `workspace/` before ever reaching
|
|
516
|
+
// the leaf (the same ancestor-descent subtlety `.hqinclude` handles
|
|
517
|
+
// via its ancestor matcher). The native recursive backend ignores
|
|
518
|
+
// descent decisions and re-checks each path at event time, so this
|
|
519
|
+
// only matters for chokidar but is harmless everywhere.
|
|
520
|
+
// See computeContinuityPointerPaths for the matching push-side carve-out.
|
|
521
|
+
if (rel === CONTINUITY_POINTER_REL && !isDir) return true;
|
|
522
|
+
if (isDir && CONTINUITY_POINTER_REL.startsWith(rel + "/")) return true;
|
|
523
|
+
|
|
501
524
|
// Layer 3: excluded top-level buckets (.git/companies/repos/workspace).
|
|
502
525
|
const topLevel = rel.split("/")[0];
|
|
503
526
|
if (excludedTopLevel.has(topLevel)) return false;
|