@indigoai-us/hq-cloud 6.14.12 → 6.14.14
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/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +22 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +6 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +3 -1
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +10 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +42 -3
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +1 -0
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +3 -0
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +17 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +83 -33
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +193 -5
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +23 -60
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.d.ts +2 -2
- package/dist/cli/reindex.test.js +60 -109
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/rescue.reindex.test.js +3 -2
- package/dist/cli/rescue.reindex.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +12 -1
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +14 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +9 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +8 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +44 -12
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +440 -76
- package/dist/cognito-auth.js.map +1 -1
- package/dist/cognito-auth.test.js +174 -0
- package/dist/cognito-auth.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/machine-auth.test.js +683 -5
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/vault-client.d.ts +2 -1
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +45 -9
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +77 -7
- package/dist/vault-client.test.js.map +1 -1
- package/package.json +2 -1
- package/src/bin/sync-runner-company.ts +5 -0
- package/src/bin/sync-runner-planning.ts +2 -1
- package/src/bin/sync-runner-watch-loop.ts +51 -4
- package/src/bin/sync-runner-watch-routes.ts +2 -0
- package/src/bin/sync-runner.test.ts +244 -4
- package/src/bin/sync-runner.ts +99 -32
- package/src/cli/reindex.test.ts +63 -120
- package/src/cli/reindex.ts +23 -62
- package/src/cli/rescue.reindex.test.ts +3 -2
- package/src/cli/share.test.ts +20 -0
- package/src/cli/share.ts +7 -1
- package/src/cli/sync.test.ts +14 -0
- package/src/cli/sync.ts +5 -1
- package/src/cognito-auth.test.ts +227 -0
- package/src/cognito-auth.ts +533 -71
- package/src/index.ts +5 -1
- package/src/machine-auth.test.ts +808 -5
- package/src/vault-client.test.ts +100 -11
- package/src/vault-client.ts +55 -10
- package/test/share-sync.integration.test.ts +12 -9
package/src/cli/reindex.test.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The logic is implemented natively in TypeScript in ./reindex.ts; these tests
|
|
6
6
|
* exercise reindex() directly against a temp HQ tree, asserting the observable
|
|
7
|
-
* filesystem outcomes (skill wrappers, personal-overlay
|
|
8
|
-
* re-deriving the implementation internals.
|
|
7
|
+
* filesystem outcomes (skill wrappers, retired personal-overlay mirror cleanup)
|
|
8
|
+
* rather than re-deriving the implementation internals.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
@@ -112,26 +112,29 @@ describe("reindex", () => {
|
|
|
112
112
|
);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
it("
|
|
115
|
+
it("does NOT mirror the personal overlay into core/<type>/ (read directly now)", () => {
|
|
116
116
|
fs.mkdirSync(path.join(root, "personal/policies"), { recursive: true });
|
|
117
117
|
fs.writeFileSync(path.join(root, "personal/policies/myrule.md"), "rule\n");
|
|
118
118
|
|
|
119
119
|
const { status } = reindex({ repoRoot: root });
|
|
120
120
|
expect(status).toBe(0);
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
// personal is the sole read source — reindex must NOT create a mirror
|
|
123
|
+
// symlink in core/policies. The HQ tooling reads personal/policies directly,
|
|
124
|
+
// so a mirror would only double-surface every entry.
|
|
125
|
+
expect(fs.existsSync(path.join(root, "core/policies/myrule.md"))).toBe(false);
|
|
125
126
|
});
|
|
126
127
|
|
|
127
|
-
it("collapses per-file
|
|
128
|
-
// A REAL
|
|
129
|
-
// no-op that used to spam one stderr line per entry and flood the
|
|
130
|
-
// breadcrumb ring, evicting the real per-company sync error.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
it("collapses per-file wrapper-skip spam into ONE summary line (HQ-SYNC-2)", () => {
|
|
129
|
+
// A REAL file sits where a managed skill-wrapper symlink would go — the
|
|
130
|
+
// legitimate no-op that used to spam one stderr line per entry and flood the
|
|
131
|
+
// Sentry breadcrumb ring, evicting the real per-company sync error.
|
|
132
|
+
writeSkill("core/skills/demo");
|
|
133
|
+
reindex({ repoRoot: root });
|
|
134
|
+
// Replace the managed per-file symlink with a real (non-symlink) file.
|
|
135
|
+
const slot = path.join(root, ".claude/skills/core:demo/SKILL.md");
|
|
136
|
+
fs.rmSync(slot);
|
|
137
|
+
fs.writeFileSync(slot, "REAL file, not a link\n");
|
|
135
138
|
|
|
136
139
|
const writes: string[] = [];
|
|
137
140
|
const spy = vi
|
|
@@ -150,7 +153,7 @@ describe("reindex", () => {
|
|
|
150
153
|
|
|
151
154
|
expect(status).toBe(0);
|
|
152
155
|
// The pre-existing real file is left untouched (never overwritten).
|
|
153
|
-
expect(fs.lstatSync(
|
|
156
|
+
expect(fs.lstatSync(slot).isSymbolicLink()).toBe(false);
|
|
154
157
|
// No per-file spam…
|
|
155
158
|
expect(stderr).not.toContain("already exists and is not a symlink; skipping");
|
|
156
159
|
// …just ONE collapsed summary naming the count.
|
|
@@ -158,11 +161,12 @@ describe("reindex", () => {
|
|
|
158
161
|
expect(stderr).toContain("HQ_REINDEX_VERBOSE=1");
|
|
159
162
|
});
|
|
160
163
|
|
|
161
|
-
it("keeps per-file
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
fs.
|
|
164
|
+
it("keeps per-file wrapper-skip detail under HQ_REINDEX_VERBOSE=1", () => {
|
|
165
|
+
writeSkill("core/skills/demo");
|
|
166
|
+
reindex({ repoRoot: root });
|
|
167
|
+
const slot = path.join(root, ".claude/skills/core:demo/SKILL.md");
|
|
168
|
+
fs.rmSync(slot);
|
|
169
|
+
fs.writeFileSync(slot, "REAL\n");
|
|
166
170
|
|
|
167
171
|
const writes: string[] = [];
|
|
168
172
|
const spy = vi
|
|
@@ -180,7 +184,7 @@ describe("reindex", () => {
|
|
|
180
184
|
}
|
|
181
185
|
const stderr = writes.join("");
|
|
182
186
|
expect(stderr).toContain(
|
|
183
|
-
"core/
|
|
187
|
+
".claude/skills/core:demo/SKILL.md already exists and is not a symlink; skipping",
|
|
184
188
|
);
|
|
185
189
|
});
|
|
186
190
|
|
|
@@ -203,75 +207,55 @@ describe("reindex", () => {
|
|
|
203
207
|
).toBe("../../../core/skills/demo/SKILL.md");
|
|
204
208
|
});
|
|
205
209
|
|
|
206
|
-
// ──
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// personal
|
|
210
|
+
// ── Migration cleanup: retired personal-overlay mirror symlinks ──────────
|
|
211
|
+
// reindex no longer CREATES core/<type>/<entry> -> ../../personal/<type>/<entry>
|
|
212
|
+
// mirror symlinks — personal is read directly. A managed mirror symlink a
|
|
213
|
+
// PRIOR release left behind must be pruned on the next reindex, WHETHER OR NOT
|
|
214
|
+
// its personal source still exists, so it can never double-surface alongside
|
|
215
|
+
// the direct read.
|
|
216
|
+
|
|
217
|
+
// Forge a legacy managed mirror symlink exactly as the old reindex would have.
|
|
218
|
+
function legacyMirror(type: string, entry: string): string {
|
|
219
|
+
const coreDir = path.join(root, "core", type);
|
|
220
|
+
fs.mkdirSync(coreDir, { recursive: true });
|
|
221
|
+
const link = path.join(coreDir, entry);
|
|
222
|
+
fs.symlinkSync(`../../personal/${type}/${entry}`, link);
|
|
223
|
+
return link;
|
|
224
|
+
}
|
|
211
225
|
|
|
212
|
-
it("prunes
|
|
226
|
+
it("prunes a legacy mirror symlink even when its personal source still exists", () => {
|
|
213
227
|
fs.mkdirSync(path.join(root, "personal/policies"), { recursive: true });
|
|
214
228
|
fs.writeFileSync(path.join(root, "personal/policies/myrule.md"), "rule\n");
|
|
215
|
-
|
|
216
|
-
const link = path.join(root, "core/policies/myrule.md");
|
|
229
|
+
const link = legacyMirror("policies", "myrule.md");
|
|
217
230
|
expect(fs.lstatSync(link).isSymbolicLink()).toBe(true);
|
|
218
231
|
|
|
219
|
-
// Retire the personal source, then reindex again.
|
|
220
|
-
fs.rmSync(path.join(root, "personal/policies/myrule.md"));
|
|
221
232
|
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
222
233
|
|
|
223
|
-
//
|
|
234
|
+
// Pruned — personal/policies/myrule.md is read directly now, no mirror.
|
|
224
235
|
expect(fs.existsSync(link)).toBe(false);
|
|
225
236
|
expect(() => fs.lstatSync(link)).toThrow();
|
|
237
|
+
// The personal source itself is untouched.
|
|
238
|
+
expect(fs.existsSync(path.join(root, "personal/policies/myrule.md"))).toBe(true);
|
|
226
239
|
});
|
|
227
240
|
|
|
228
|
-
it("prunes
|
|
229
|
-
|
|
230
|
-
fs.
|
|
231
|
-
fs.writeFileSync(path.join(root, "personal/policies/b.md"), "b\n");
|
|
232
|
-
reindex({ repoRoot: root });
|
|
233
|
-
expect(fs.lstatSync(path.join(root, "core/policies/a.md")).isSymbolicLink()).toBe(true);
|
|
234
|
-
expect(fs.lstatSync(path.join(root, "core/policies/b.md")).isSymbolicLink()).toBe(true);
|
|
241
|
+
it("prunes a legacy mirror symlink whose personal source was deleted", () => {
|
|
242
|
+
const link = legacyMirror("policies", "gone.md"); // dangling: no source
|
|
243
|
+
expect(fs.lstatSync(link).isSymbolicLink()).toBe(true);
|
|
235
244
|
|
|
236
|
-
// Nuke the whole personal/policies dir (the creation loop's isDir guard would
|
|
237
|
-
// skip the type entirely — the prune must still run and clean both links).
|
|
238
|
-
fs.rmSync(path.join(root, "personal/policies"), { recursive: true, force: true });
|
|
239
245
|
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
240
|
-
|
|
241
|
-
expect(fs.existsSync(path.join(root, "core/policies/a.md"))).toBe(false);
|
|
242
|
-
expect(fs.existsSync(path.join(root, "core/policies/b.md"))).toBe(false);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it("keeps a LIVE personal-overlay mirror symlink (only orphans are pruned)", () => {
|
|
246
|
-
fs.mkdirSync(path.join(root, "personal/policies"), { recursive: true });
|
|
247
|
-
fs.writeFileSync(path.join(root, "personal/policies/keep.md"), "keep\n");
|
|
248
|
-
fs.writeFileSync(path.join(root, "personal/policies/drop.md"), "drop\n");
|
|
249
|
-
reindex({ repoRoot: root });
|
|
250
|
-
|
|
251
|
-
// Retire only one source; the other stays live.
|
|
252
|
-
fs.rmSync(path.join(root, "personal/policies/drop.md"));
|
|
253
|
-
reindex({ repoRoot: root });
|
|
254
|
-
|
|
255
|
-
const keep = path.join(root, "core/policies/keep.md");
|
|
256
|
-
expect(fs.lstatSync(keep).isSymbolicLink()).toBe(true);
|
|
257
|
-
expect(fs.readlinkSync(keep)).toBe("../../personal/policies/keep.md");
|
|
258
|
-
expect(fs.readFileSync(keep, "utf-8")).toBe("keep\n");
|
|
259
|
-
expect(fs.existsSync(path.join(root, "core/policies/drop.md"))).toBe(false);
|
|
246
|
+
expect(fs.existsSync(link)).toBe(false);
|
|
260
247
|
});
|
|
261
248
|
|
|
262
|
-
it("prunes
|
|
263
|
-
// personal/knowledge/<kb>/ mirrors as a directory symlink; deleting the kb
|
|
264
|
-
// must prune the dangling directory symlink too (rmSync on a symlink, not
|
|
265
|
-
// recursive into a resolved dir).
|
|
249
|
+
it("prunes a legacy mirror DIRECTORY symlink without recursing into the source (DEV-1767 shape)", () => {
|
|
266
250
|
fs.mkdirSync(path.join(root, "personal/knowledge/my-kb"), { recursive: true });
|
|
267
251
|
fs.writeFileSync(path.join(root, "personal/knowledge/my-kb/note.md"), "n\n");
|
|
268
|
-
|
|
269
|
-
const link = path.join(root, "core/knowledge/my-kb");
|
|
252
|
+
const link = legacyMirror("knowledge", "my-kb"); // directory-shaped symlink
|
|
270
253
|
expect(fs.lstatSync(link).isSymbolicLink()).toBe(true);
|
|
271
254
|
|
|
272
|
-
fs.rmSync(path.join(root, "personal/knowledge/my-kb"), { recursive: true, force: true });
|
|
273
255
|
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
274
256
|
expect(fs.existsSync(link)).toBe(false);
|
|
257
|
+
// rmSync on the symlink must NOT recurse into the resolved directory.
|
|
258
|
+
expect(fs.existsSync(path.join(root, "personal/knowledge/my-kb/note.md"))).toBe(true);
|
|
275
259
|
});
|
|
276
260
|
|
|
277
261
|
it("leaves a real core file and a foreign symlink in core/<type>/ untouched (cross-check invariant)", () => {
|
|
@@ -369,24 +353,21 @@ describe("reindex", () => {
|
|
|
369
353
|
expect(fs.readlinkSync(agentsLink)).toBe("../../../core/skills/demo/agents");
|
|
370
354
|
});
|
|
371
355
|
|
|
372
|
-
it("
|
|
373
|
-
// personal/knowledge/<kb>/ is a directory of notes
|
|
374
|
-
//
|
|
356
|
+
it("does NOT mirror a personal/<type> DIRECTORY entry into core/ (read directly now)", () => {
|
|
357
|
+
// personal/knowledge/<kb>/ is a directory of notes. It used to mirror as a
|
|
358
|
+
// core/ directory symlink (the DEV-1767 shape rescue/update-hq choked on).
|
|
359
|
+
// The mirror is retired: qmd indexes personal/knowledge directly, so reindex
|
|
360
|
+
// must leave core/knowledge alone — nothing to choke on anymore.
|
|
375
361
|
fs.mkdirSync(path.join(root, "personal/knowledge/my-kb"), { recursive: true });
|
|
376
362
|
fs.writeFileSync(path.join(root, "personal/knowledge/my-kb/note.md"), "owned by personal\n");
|
|
377
363
|
|
|
378
364
|
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
379
365
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
expect(fs.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// Idempotent re-run: existing directory symlink recognised, left in place.
|
|
387
|
-
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
388
|
-
expect(fs.readlinkSync(link)).toBe("../../personal/knowledge/my-kb");
|
|
389
|
-
expect(fs.readFileSync(path.join(link, "note.md"), "utf-8")).toBe("owned by personal\n");
|
|
366
|
+
expect(fs.existsSync(path.join(root, "core/knowledge/my-kb"))).toBe(false);
|
|
367
|
+
// The personal source is untouched.
|
|
368
|
+
expect(fs.readFileSync(path.join(root, "personal/knowledge/my-kb/note.md"), "utf-8")).toBe(
|
|
369
|
+
"owned by personal\n",
|
|
370
|
+
);
|
|
390
371
|
});
|
|
391
372
|
|
|
392
373
|
// ── operation lock: reindex uses a SEPARATE scope from sync/rescue ──────
|
|
@@ -497,22 +478,6 @@ describe("reindex", () => {
|
|
|
497
478
|
}
|
|
498
479
|
});
|
|
499
480
|
|
|
500
|
-
it("does not abort reindex when a core/<type> mirror mkdir fails", () => {
|
|
501
|
-
writeSkill("core/skills/demo");
|
|
502
|
-
fs.mkdirSync(path.join(root, "personal/knowledge/my-kb"), { recursive: true });
|
|
503
|
-
fs.writeFileSync(path.join(root, "personal/knowledge/my-kb/note.md"), "n\n");
|
|
504
|
-
|
|
505
|
-
hoisted.failMkdirContaining = path.join("core", "knowledge");
|
|
506
|
-
try {
|
|
507
|
-
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
508
|
-
} finally {
|
|
509
|
-
hoisted.failMkdirContaining = "";
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// The mirror for the failed type is skipped; skill surfacing still happened.
|
|
513
|
-
expect(fs.existsSync(path.join(root, ".claude/skills/core:demo"))).toBe(true);
|
|
514
|
-
});
|
|
515
|
-
|
|
516
481
|
// ── HQ-CLI-4: symlink creation is privileged on Windows. Without Developer
|
|
517
482
|
// Mode, fs.symlinkSync throws EPERM. The HQ-B0 work guarded every mkdir
|
|
518
483
|
// site but left the symlink sites unguarded, so `hq reindex` still aborted
|
|
@@ -536,28 +501,6 @@ describe("reindex", () => {
|
|
|
536
501
|
).toBe(false);
|
|
537
502
|
});
|
|
538
503
|
|
|
539
|
-
it("does not abort reindex when a personal-mirror symlink fails with EPERM, and other work still runs", () => {
|
|
540
|
-
writeSkill("core/skills/demo");
|
|
541
|
-
fs.mkdirSync(path.join(root, "personal/knowledge/my-kb"), { recursive: true });
|
|
542
|
-
fs.writeFileSync(path.join(root, "personal/knowledge/my-kb/note.md"), "n\n");
|
|
543
|
-
|
|
544
|
-
// Fail ONLY the core/<type> personal-mirror symlinks; wrapper symlinks
|
|
545
|
-
// succeed, proving the skip is scoped and the reindex completes.
|
|
546
|
-
hoisted.failSymlinkContaining = path.join("core", "knowledge");
|
|
547
|
-
try {
|
|
548
|
-
expect(reindex({ repoRoot: root }).status).toBe(0);
|
|
549
|
-
} finally {
|
|
550
|
-
hoisted.failSymlinkContaining = "";
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// The personal mirror was skipped (EPERM)...
|
|
554
|
-
expect(fs.existsSync(path.join(root, "core/knowledge/my-kb"))).toBe(false);
|
|
555
|
-
// ...but unaffected work still happened: the skill wrapper + its symlink.
|
|
556
|
-
expect(
|
|
557
|
-
fs.lstatSync(path.join(root, ".claude/skills/core:demo/SKILL.md")).isSymbolicLink(),
|
|
558
|
-
).toBe(true);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
504
|
// --- multi-harness session-log capture into workspace/.session-logs/<harness>/
|
|
562
505
|
describe("session-log capture", () => {
|
|
563
506
|
let claudeDir: string; // CLAUDE_CONFIG_DIR
|
package/src/cli/reindex.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* hq reindex — surfaces namespaced skills as Claude Code skill wrappers under
|
|
3
|
-
* .claude/skills/<ns>:<skill>/ (one symlink per source file),
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* .claude/skills/<ns>:<skill>/ (one symlink per source file), prunes orphan
|
|
4
|
+
* wrappers + legacy command symlinks (plus a migration cleanup of retired
|
|
5
|
+
* personal/{knowledge,policies,workers,settings} mirror symlinks in core/<type>/
|
|
6
|
+
* — that overlay is now read directly from personal/, not mirrored), captures
|
|
7
|
+
* this HQ tree's coding-harness session logs (Claude Code / Codex / Grok) into
|
|
7
8
|
* workspace/.session-logs/<harness>/, and regenerates the workers registry.
|
|
8
9
|
*
|
|
9
10
|
* The logic historically lived in a bundled bash script (scripts/reindex.sh)
|
|
@@ -689,63 +690,24 @@ export function reindex(opts: ReindexOptions = {}): ReindexResult {
|
|
|
689
690
|
}
|
|
690
691
|
}
|
|
691
692
|
|
|
692
|
-
// --- Personal
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
for (const entry of globEntries(personalDir)) {
|
|
702
|
-
const entryPath = path.join(personalDir, entry);
|
|
703
|
-
if (!existsFollow(entryPath)) continue;
|
|
704
|
-
|
|
705
|
-
const linkPath = path.join(coreDir, entry);
|
|
706
|
-
const relativeTarget = `../../personal/${type}/${entry}`;
|
|
707
|
-
|
|
708
|
-
const lst = lstatOrNull(linkPath);
|
|
709
|
-
if (lst && lst.isSymbolicLink()) {
|
|
710
|
-
const current = readlinkOrNull(linkPath);
|
|
711
|
-
if (current === relativeTarget) continue;
|
|
712
|
-
skippedPointsElsewhere++;
|
|
713
|
-
if (verbose) {
|
|
714
|
-
warn(
|
|
715
|
-
`reindex: core/${type}/${entry} already points to '${current}' (expected '${relativeTarget}'); leaving alone`,
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
continue;
|
|
719
|
-
} else if (lst) {
|
|
720
|
-
skippedNonSymlink++;
|
|
721
|
-
if (verbose) {
|
|
722
|
-
warn(`reindex: core/${type}/${entry} already exists and is not a symlink; skipping`);
|
|
723
|
-
}
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
safeSymlink(relativeTarget, linkPath, `core/${type}/${entry}`);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// --- Cleanup pass C: drop orphan personal-overlay mirror symlinks ----------
|
|
732
|
-
// The mirror above creates core/<type>/<entry> -> ../../personal/<type>/<entry>
|
|
733
|
-
// for each LIVE personal entry, but historically never removed that symlink
|
|
734
|
-
// when its personal source was later deleted or renamed — leaving a dangling
|
|
735
|
-
// link in core/<type>/ (e.g. a retired personal policy that still surfaced via
|
|
736
|
-
// the policy-trigger hook until a human hand-deleted it). This pass prunes it.
|
|
693
|
+
// --- Personal-overlay mirroring: RETIRED + migration cleanup ---------------
|
|
694
|
+
// personal/{knowledge,policies,workers,settings} is now read DIRECTLY by the
|
|
695
|
+
// HQ tooling (the inject-policy-on-trigger hook reads personal/policies, the
|
|
696
|
+
// workers-registry generator walks personal/workers, qmd-reindex-after-sync
|
|
697
|
+
// registers a personal/knowledge collection, and repo-run-registry reads
|
|
698
|
+
// personal/settings/orchestrator.yaml). personal is the SOLE read source for
|
|
699
|
+
// the personal overlay, so reindex no longer SYMLINKS personal/<type>/<entry>
|
|
700
|
+
// into core/<type>/<entry> — a mirror would now double-surface every entry
|
|
701
|
+
// (read once directly, once via the link).
|
|
737
702
|
//
|
|
738
|
-
//
|
|
739
|
-
// core/<type>/<entry>
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
// precisely "the personal source was removed". This runs independently of the
|
|
747
|
-
// creation loop above (note: no `isDir(personalDir)` guard) so that deleting
|
|
748
|
-
// the ENTIRE personal/<type>/ directory still prunes every mirror it produced.
|
|
703
|
+
// Migration cleanup: prune any mirror symlink a PRIOR release created —
|
|
704
|
+
// core/<type>/<entry> whose target is EXACTLY `../../personal/<type>/<entry>`.
|
|
705
|
+
// That exact-target discriminator (policy wrapper-sync-orphan-cleanup-cross-
|
|
706
|
+
// check) means a real release-shipped core file (not a symlink) or a human-
|
|
707
|
+
// authored symlink pointing elsewhere is never touched. Unlike the old
|
|
708
|
+
// "Cleanup pass C", this prunes the link WHETHER OR NOT its personal source
|
|
709
|
+
// still exists: the mirror is retired, so every managed link is now stale.
|
|
710
|
+
// Kept for a few releases so existing trees self-heal on the next reindex.
|
|
749
711
|
for (const type of ["knowledge", "policies", "workers", "settings"]) {
|
|
750
712
|
const coreDir = path.join(root, "core", type);
|
|
751
713
|
for (const entry of globEntries(coreDir)) {
|
|
@@ -753,8 +715,7 @@ export function reindex(opts: ReindexOptions = {}): ReindexResult {
|
|
|
753
715
|
const lst = lstatOrNull(linkPath);
|
|
754
716
|
if (!lst || !lst.isSymbolicLink()) continue; // real file/dir → not ours
|
|
755
717
|
if (readlinkOrNull(linkPath) !== `../../personal/${type}/${entry}`) continue; // not a managed mirror link
|
|
756
|
-
|
|
757
|
-
// Managed mirror symlink whose personal source is gone → prune.
|
|
718
|
+
// Managed mirror symlink from a prior release → prune (mirror is retired).
|
|
758
719
|
try {
|
|
759
720
|
fs.rmSync(linkPath);
|
|
760
721
|
} catch {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wiring tests: `rescue()` runs `reindex()` after a successful, non-dry-run
|
|
3
|
-
* rescue so the generated skill wrappers /
|
|
4
|
-
*
|
|
3
|
+
* rescue so the generated skill wrappers / workers registry are refreshed once
|
|
4
|
+
* core/ has been re-laid-down (the personal overlay is read directly, not
|
|
5
|
+
* mirrored, so there are no personal mirror symlinks to refresh).
|
|
5
6
|
*
|
|
6
7
|
* The rescue algorithm (./rescue-core.js) is mocked so no real clone/overlay
|
|
7
8
|
* runs, and ./reindex.js is mocked to a spy so we assert the call without
|
package/src/cli/share.test.ts
CHANGED
|
@@ -41,6 +41,7 @@ import * as readline from "readline";
|
|
|
41
41
|
import { share, _testing as shareTesting } from "./share.js";
|
|
42
42
|
import { deleteRemoteFile, downloadFile, headRemoteFile, uploadFile, uploadSymlink } from "../s3.js";
|
|
43
43
|
import type { EntityContext } from "../types.js";
|
|
44
|
+
import { VaultAuthError } from "../vault-client.js";
|
|
44
45
|
|
|
45
46
|
const mockConfig: VaultServiceConfig = {
|
|
46
47
|
apiUrl: "https://vault-api.test",
|
|
@@ -207,6 +208,25 @@ describe("share", () => {
|
|
|
207
208
|
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md", undefined, expect.anything());
|
|
208
209
|
});
|
|
209
210
|
|
|
211
|
+
it("propagates a per-file vault 401 to the runner auth boundary", async () => {
|
|
212
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
213
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
214
|
+
const testFile = path.join(companyRoot, "private.md");
|
|
215
|
+
fs.writeFileSync(testFile, "private");
|
|
216
|
+
vi.mocked(uploadFile).mockRejectedValueOnce(
|
|
217
|
+
new VaultAuthError("upload unauthorized"),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
await expect(
|
|
221
|
+
share({
|
|
222
|
+
paths: [testFile],
|
|
223
|
+
company: "acme",
|
|
224
|
+
vaultConfig: mockConfig,
|
|
225
|
+
hqRoot: tmpDir,
|
|
226
|
+
}),
|
|
227
|
+
).rejects.toBeInstanceOf(VaultAuthError);
|
|
228
|
+
});
|
|
229
|
+
|
|
210
230
|
it("respects ignore rules", async () => {
|
|
211
231
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
212
232
|
fs.mkdirSync(path.join(companyRoot, ".git"), { recursive: true });
|
package/src/cli/share.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
} from "../lib/conflict-file.js";
|
|
68
68
|
import { appendConflictEntry } from "../lib/conflict-index.js";
|
|
69
69
|
import { isCloudAuthoritative } from "../lib/cloud-authoritative.js";
|
|
70
|
+
import { VaultAuthError } from "../vault-client.js";
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
73
|
* Push-side fresh-collision convergence probe.
|
|
@@ -110,7 +111,8 @@ async function remoteContentDiffers(
|
|
|
110
111
|
? hashSymlinkTarget(fs.readlinkSync(probePath))
|
|
111
112
|
: hashFile(probePath);
|
|
112
113
|
return remoteHash !== localHash;
|
|
113
|
-
} catch {
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (err instanceof VaultAuthError) throw err;
|
|
114
116
|
return true;
|
|
115
117
|
} finally {
|
|
116
118
|
try {
|
|
@@ -1388,6 +1390,7 @@ async function executeUploads(
|
|
|
1388
1390
|
try {
|
|
1389
1391
|
await performUpload(precondition);
|
|
1390
1392
|
} catch (err) {
|
|
1393
|
+
if (err instanceof VaultAuthError) throw err;
|
|
1391
1394
|
if (isPreconditionFailed(err)) {
|
|
1392
1395
|
conflictPaths.push(relativePath);
|
|
1393
1396
|
const resolution = await resolveConflictSerialized({
|
|
@@ -1410,6 +1413,7 @@ async function executeUploads(
|
|
|
1410
1413
|
try {
|
|
1411
1414
|
await performUpload(undefined);
|
|
1412
1415
|
} catch (retryErr) {
|
|
1416
|
+
if (retryErr instanceof VaultAuthError) throw retryErr;
|
|
1413
1417
|
run.emit({
|
|
1414
1418
|
type: "error",
|
|
1415
1419
|
path: relativePath,
|
|
@@ -1501,6 +1505,7 @@ async function writePushConflictMirror(
|
|
|
1501
1505
|
});
|
|
1502
1506
|
}
|
|
1503
1507
|
} catch (mirrorErr) {
|
|
1508
|
+
if (mirrorErr instanceof VaultAuthError) throw mirrorErr;
|
|
1504
1509
|
run.emit({
|
|
1505
1510
|
type: "error",
|
|
1506
1511
|
path: item.relativePath,
|
|
@@ -1562,6 +1567,7 @@ async function executeDeletes(
|
|
|
1562
1567
|
deleted: true,
|
|
1563
1568
|
});
|
|
1564
1569
|
} catch (err) {
|
|
1570
|
+
if (err instanceof VaultAuthError) throw err;
|
|
1565
1571
|
run.emit({
|
|
1566
1572
|
type: "error",
|
|
1567
1573
|
path: relativePath,
|
package/src/cli/sync.test.ts
CHANGED
|
@@ -58,6 +58,7 @@ import { share } from "./share.js";
|
|
|
58
58
|
import * as s3Module from "../s3.js";
|
|
59
59
|
import { reindex } from "./reindex.js";
|
|
60
60
|
import { readSyncProgress } from "../sync-progress.js";
|
|
61
|
+
import { VaultAuthError } from "../vault-client.js";
|
|
61
62
|
|
|
62
63
|
const mockConfig: VaultServiceConfig = {
|
|
63
64
|
apiUrl: "https://vault-api.test",
|
|
@@ -154,6 +155,19 @@ describe("sync", () => {
|
|
|
154
155
|
expect(s3Module.classifyVaultKey("docs/readme.md", "company")).toBeNull();
|
|
155
156
|
});
|
|
156
157
|
|
|
158
|
+
it("propagates a per-file vault 401 to the runner auth boundary", async () => {
|
|
159
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
|
|
160
|
+
{ key: "docs/private.md", size: 10, lastModified: new Date(), etag: '"private"' },
|
|
161
|
+
]);
|
|
162
|
+
vi.mocked(s3Module.downloadFile).mockRejectedValueOnce(
|
|
163
|
+
new VaultAuthError("download unauthorized"),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await expect(
|
|
167
|
+
sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir }),
|
|
168
|
+
).rejects.toBeInstanceOf(VaultAuthError);
|
|
169
|
+
});
|
|
170
|
+
|
|
157
171
|
it("runs reindex against hqRoot after a sync that downloaded files", async () => {
|
|
158
172
|
await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
|
|
159
173
|
// skipLock: the surrounding sync run already holds the per-root lock.
|
package/src/cli/sync.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
VaultServiceConfig,
|
|
13
13
|
SyncJournal,
|
|
14
14
|
} from "../types.js";
|
|
15
|
-
import { VaultClient, type SyncMode } from "../vault-client.js";
|
|
15
|
+
import { VaultAuthError, VaultClient, type SyncMode } from "../vault-client.js";
|
|
16
16
|
import {
|
|
17
17
|
emitCloudTelemetry,
|
|
18
18
|
type TelemetryClaims,
|
|
@@ -1311,6 +1311,7 @@ async function executeConflictItem(
|
|
|
1311
1311
|
: (downloaded.contentHash ?? hashFile(conflictAbs));
|
|
1312
1312
|
converged = remoteHash === item.localHash;
|
|
1313
1313
|
} catch (probeErr) {
|
|
1314
|
+
if (probeErr instanceof VaultAuthError) throw probeErr;
|
|
1314
1315
|
run.emit({
|
|
1315
1316
|
type: "error",
|
|
1316
1317
|
path: remoteFile.key,
|
|
@@ -1563,6 +1564,7 @@ async function downloadOne(
|
|
|
1563
1564
|
counters.bytesDownloaded += size;
|
|
1564
1565
|
counters.changedPaths.push(remoteFile.key);
|
|
1565
1566
|
} catch (err) {
|
|
1567
|
+
if (err instanceof VaultAuthError) throw err;
|
|
1566
1568
|
if (err instanceof LocalSnapshotChangedError) {
|
|
1567
1569
|
reportLocalSnapshotConflict(run, counters, remoteFile.key);
|
|
1568
1570
|
} else if (isAccessDenied(err)) {
|
|
@@ -1617,6 +1619,7 @@ async function emitAndReportNewFiles(
|
|
|
1617
1619
|
addedBy = head.metadata["created-by"];
|
|
1618
1620
|
}
|
|
1619
1621
|
} catch (headErr) {
|
|
1622
|
+
if (headErr instanceof VaultAuthError) throw headErr;
|
|
1620
1623
|
try {
|
|
1621
1624
|
console.error(
|
|
1622
1625
|
`[hq-sync] HeadObject failed for ${nf.path}: ${
|
|
@@ -1683,6 +1686,7 @@ async function verifyPlannedJournalTombstones(
|
|
|
1683
1686
|
const head = await headRemoteFile(run.ctx, tombstone.key);
|
|
1684
1687
|
return head === null ? tombstone : null;
|
|
1685
1688
|
} catch (err) {
|
|
1689
|
+
if (err instanceof VaultAuthError) throw err;
|
|
1686
1690
|
if (isAccessDenied(err)) return null;
|
|
1687
1691
|
run.emit({
|
|
1688
1692
|
type: "error",
|