@delorenj/pjangler 1.5.13 → 1.5.15

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.
@@ -177,6 +177,41 @@ try {
177
177
  }
178
178
  });
179
179
 
180
+ // src/utils/tree-diff.ts
181
+ import { createHash } from "node:crypto";
182
+ import { lstatSync, readFileSync, readdirSync, readlinkSync } from "node:fs";
183
+ import { join as join2, relative } from "node:path";
184
+ function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()) {
185
+ let stat;
186
+ try {
187
+ stat = lstatSync(current);
188
+ } catch (error) {
189
+ if (error.code === "ENOENT") return snapshot;
190
+ throw error;
191
+ }
192
+ const rel = relative(root, current) || ".";
193
+ if (rel === ".git" || rel.startsWith(`.git${process.platform === "win32" ? "\\" : "/"}`)) return snapshot;
194
+ if (stat.isSymbolicLink()) {
195
+ snapshot.set(rel, `link:${readlinkSync(current)}`);
196
+ } else if (stat.isFile()) {
197
+ snapshot.set(rel, `file:${createHash("sha256").update(readFileSync(current)).digest("hex")}:${stat.mode & 4095}`);
198
+ } else if (stat.isDirectory()) {
199
+ snapshot.set(rel, `dir:${stat.mode & 4095}`);
200
+ for (const name of readdirSync(current)) snapshotTree(root, join2(current, name), snapshot);
201
+ } else {
202
+ snapshot.set(rel, `other:${stat.mode}`);
203
+ }
204
+ return snapshot;
205
+ }
206
+ function changedTreePaths(root, before, after) {
207
+ return [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].filter((path) => before.get(path) !== after.get(path)).map((path) => join2(root, path)).sort();
208
+ }
209
+ var init_tree_diff = __esm({
210
+ "src/utils/tree-diff.ts"() {
211
+ "use strict";
212
+ }
213
+ });
214
+
180
215
  // src/notebook/types.ts
181
216
  function notebookCredentialMaterialPath(value) {
182
217
  const seen = /* @__PURE__ */ new WeakSet();
@@ -258,7 +293,7 @@ var init_types = __esm({
258
293
 
259
294
  // src/project/notebookSettingsValidation.ts
260
295
  import { isIP } from "node:net";
261
- import { isAbsolute as isAbsolute2 } from "node:path";
296
+ import { isAbsolute as isAbsolute3 } from "node:path";
262
297
  function isRecord(value) {
263
298
  return typeof value === "object" && value !== null && !Array.isArray(value);
264
299
  }
@@ -315,7 +350,7 @@ function validateGlobalNotebookConfig(value) {
315
350
  if (limits.lease_seconds * 1e3 <= limits.overall_timeout_ms) throw new Error("Project registry notebook lease_seconds must exceed one request timeout");
316
351
  if (isRecord(value.defaults) && value.defaults.overview_max_chars !== void 0 && Number(value.defaults.overview_max_chars) > limits.note_max_bytes) throw new Error("Project registry notebook overview_max_chars exceeds note_max_bytes");
317
352
  if (value.summarizer !== void 0) {
318
- if (!isRecord(value.summarizer) || typeof value.summarizer.executable !== "string" || !isAbsolute2(value.summarizer.executable) || value.summarizer.executable.includes("\0") || Buffer.byteLength(value.summarizer.executable, "utf8") > 1024) throw new Error("Project registry notebook.summarizer executable must be a bounded absolute path");
353
+ if (!isRecord(value.summarizer) || typeof value.summarizer.executable !== "string" || !isAbsolute3(value.summarizer.executable) || value.summarizer.executable.includes("\0") || Buffer.byteLength(value.summarizer.executable, "utf8") > 1024) throw new Error("Project registry notebook.summarizer executable must be a bounded absolute path");
319
354
  if (value.summarizer.args !== void 0 && (!Array.isArray(value.summarizer.args) || value.summarizer.args.length > 32 || value.summarizer.args.some((entry) => typeof entry !== "string" || entry.includes("\0") || Buffer.byteLength(entry, "utf8") > 1024))) throw new Error("Project registry notebook.summarizer args must be bounded strings");
320
355
  }
321
356
  }
@@ -334,7 +369,7 @@ var init_executionBinding = __esm({
334
369
  });
335
370
 
336
371
  // src/project/manifestIndex.ts
337
- import { basename as basename4, dirname as dirname6, join as join6, resolve as resolve6 } from "node:path";
372
+ import { basename as basename4, dirname as dirname6, join as join8, resolve as resolve7 } from "node:path";
338
373
  function normalizeProjectId2(value) {
339
374
  try {
340
375
  return normalizeProjectId(value);
@@ -380,53 +415,23 @@ var init_manifestIndex = __esm({
380
415
  }
381
416
  });
382
417
 
383
- // src/utils/tree-diff.ts
384
- import { createHash as createHash3 } from "node:crypto";
385
- import { existsSync as existsSync4, lstatSync as lstatSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, readlinkSync as readlinkSync4 } from "node:fs";
386
- import { join as join7, relative as relative5 } from "node:path";
387
- function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()) {
388
- if (!existsSync4(current)) return snapshot;
389
- const rel = relative5(root, current) || ".";
390
- if (rel === ".git" || rel.startsWith(`.git${process.platform === "win32" ? "\\" : "/"}`)) return snapshot;
391
- const stat = lstatSync5(current);
392
- if (stat.isSymbolicLink()) {
393
- snapshot.set(rel, `link:${readlinkSync4(current)}`);
394
- } else if (stat.isFile()) {
395
- snapshot.set(rel, `file:${createHash3("sha256").update(readFileSync4(current)).digest("hex")}:${stat.mode & 511}`);
396
- } else if (stat.isDirectory()) {
397
- snapshot.set(rel, `dir:${stat.mode & 511}`);
398
- for (const name of readdirSync4(current)) snapshotTree(root, join7(current, name), snapshot);
399
- } else {
400
- snapshot.set(rel, `other:${stat.mode}`);
401
- }
402
- return snapshot;
403
- }
404
- function changedTreePaths(root, before, after) {
405
- return [.../* @__PURE__ */ new Set([...before.keys(), ...after.keys()])].filter((path) => path !== "." && before.get(path) !== after.get(path)).map((path) => join7(root, path)).sort();
406
- }
407
- var init_tree_diff = __esm({
408
- "src/utils/tree-diff.ts"() {
409
- "use strict";
410
- }
411
- });
412
-
413
418
  // src/lifecycle/preflight.ts
414
419
  import { createHash as createHash4 } from "node:crypto";
415
- import { accessSync, constants, existsSync as existsSync5, lstatSync as lstatSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, realpathSync as realpathSync4, statSync as statSync2 } from "node:fs";
420
+ import { accessSync, constants, existsSync as existsSync5, lstatSync as lstatSync7, readFileSync as readFileSync6, readdirSync as readdirSync5, realpathSync as realpathSync4, statSync as statSync2 } from "node:fs";
416
421
  import { tmpdir, userInfo } from "node:os";
417
- import { basename as basename5, delimiter, dirname as dirname7, isAbsolute as isAbsolute3, join as join8, relative as relative6, resolve as resolve7 } from "node:path";
422
+ import { basename as basename5, delimiter, dirname as dirname7, isAbsolute as isAbsolute4, join as join9, relative as relative6, resolve as resolve8 } from "node:path";
418
423
  import YAML3 from "yaml";
419
424
  function containedBy(parent, candidate) {
420
- const rel = relative6(resolve7(parent), resolve7(candidate));
421
- return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
425
+ const rel = relative6(resolve8(parent), resolve8(candidate));
426
+ return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
422
427
  }
423
428
  function accountMiseShimsDir(home) {
424
- return resolve7(join8(home, ".local", "share", "mise", "shims"));
429
+ return resolve8(join9(home, ".local", "share", "mise", "shims"));
425
430
  }
426
431
  function inactiveMiseShim(candidate, shimsDir) {
427
432
  if (dirname7(candidate) !== shimsDir) return void 0;
428
433
  try {
429
- if (!lstatSync6(candidate).isSymbolicLink()) return void 0;
434
+ if (!lstatSync7(candidate).isSymbolicLink()) return void 0;
430
435
  const realPath = realpathSync4(candidate);
431
436
  if (basename5(realPath) !== "mise") return void 0;
432
437
  return { path: candidate, realPath, reason: "inactive-mise-shim" };
@@ -438,10 +443,10 @@ function resolveCopierOnPath(env2, shimsDir) {
438
443
  const skipped = [];
439
444
  for (const rawEntry of (env2.PATH ?? "").split(delimiter)) {
440
445
  const entry = rawEntry || process.cwd();
441
- const candidate = resolve7(entry, process.platform === "win32" ? "copier.exe" : "copier");
446
+ const candidate = resolve8(entry, process.platform === "win32" ? "copier.exe" : "copier");
442
447
  try {
443
448
  accessSync(candidate, constants.X_OK);
444
- const stat = lstatSync6(candidate);
449
+ const stat = lstatSync7(candidate);
445
450
  if (!stat.isFile() && !stat.isSymbolicLink()) continue;
446
451
  } catch {
447
452
  continue;
@@ -459,10 +464,10 @@ function describeSkipped(skipped) {
459
464
  return skipped.map((entry) => `${entry.path} -> ${entry.realPath}`).join(", ");
460
465
  }
461
466
  function sha256(path) {
462
- return createHash4("sha256").update(readFileSync5(path)).digest("base64url");
467
+ return createHash4("sha256").update(readFileSync6(path)).digest("base64url");
463
468
  }
464
469
  function fingerprint(path) {
465
- const absolute = resolve7(path);
470
+ const absolute = resolve8(path);
466
471
  const realPath = realpathSync4(absolute);
467
472
  const stat = statSync2(realPath);
468
473
  if (!stat.isFile()) throw new Error(`${absolute} is not a regular file`);
@@ -497,17 +502,17 @@ function sameFingerprint(expected) {
497
502
  function consoleScriptContract(path) {
498
503
  let text;
499
504
  try {
500
- text = readFileSync5(path, "utf8").slice(0, 32 * 1024);
505
+ text = readFileSync6(path, "utf8").slice(0, 32 * 1024);
501
506
  } catch (error) {
502
507
  return { ok: false, error: `cannot read Copier launcher: ${error instanceof Error ? error.message : String(error)}` };
503
508
  }
504
509
  const firstLine = text.split(/\r?\n/, 1)[0] ?? "";
505
510
  if (!firstLine.startsWith("#!")) return { ok: false, error: "Copier launcher has no executable shebang" };
506
511
  const shebang = firstLine.slice(2).trim().split(/\s+/);
507
- if (shebang.length !== 1 || !isAbsolute3(shebang[0] ?? "")) {
512
+ if (shebang.length !== 1 || !isAbsolute4(shebang[0] ?? "")) {
508
513
  return { ok: false, error: "Copier launcher must use one absolute Python interpreter" };
509
514
  }
510
- const interpreterPath = resolve7(shebang[0]);
515
+ const interpreterPath = resolve8(shebang[0]);
511
516
  const interpreter = basename5(interpreterPath);
512
517
  if (!/^python(?:\d+(?:\.\d+)*)?$/.test(interpreter)) {
513
518
  return { ok: false, error: "Copier launcher is not an absolute Python console script" };
@@ -519,16 +524,16 @@ function consoleScriptContract(path) {
519
524
  }
520
525
  function defaultUvToolRoots(home) {
521
526
  return [
522
- join8(home, ".local", "share", "uv", "tools", "copier"),
523
- join8(home, "Library", "Application Support", "uv", "tools", "copier")
524
- ].map((path) => resolve7(path));
527
+ join9(home, ".local", "share", "uv", "tools", "copier"),
528
+ join9(home, "Library", "Application Support", "uv", "tools", "copier")
529
+ ].map((path) => resolve8(path));
525
530
  }
526
531
  function locateUvSitePackages(toolRoot) {
527
- const lib = join8(toolRoot, "lib");
532
+ const lib = join9(toolRoot, "lib");
528
533
  const candidates = [];
529
534
  for (const entry of readdirSync5(lib, { withFileTypes: true })) {
530
535
  if (!entry.isDirectory() || !/^python\d+(?:\.\d+)*$/.test(entry.name)) continue;
531
- const sitePackages = join8(lib, entry.name, "site-packages");
536
+ const sitePackages = join9(lib, entry.name, "site-packages");
532
537
  if (existsSync5(sitePackages)) candidates.push(sitePackages);
533
538
  }
534
539
  if (candidates.length !== 1) throw new Error(`expected one UV Copier site-packages directory, found ${candidates.length}`);
@@ -545,15 +550,15 @@ function parseRecordLine(line) {
545
550
  };
546
551
  }
547
552
  function attestUvCopier(candidate, realCandidate, home) {
548
- const toolRoot = defaultUvToolRoots(home).find((root) => realCandidate === join8(root, "bin", "copier"));
553
+ const toolRoot = defaultUvToolRoots(home).find((root) => realCandidate === join9(root, "bin", "copier"));
549
554
  if (!toolRoot) {
550
555
  return { ok: false, error: `refusing untrusted PATH-shadowed Copier executable: ${candidate}` };
551
556
  }
552
557
  const supportedEntries = new Set([
553
- join8(home, ".local", "bin", "copier"),
554
- join8(toolRoot, "bin", "copier")
555
- ].map((path) => resolve7(path)));
556
- if (!supportedEntries.has(resolve7(candidate))) {
558
+ join9(home, ".local", "bin", "copier"),
559
+ join9(toolRoot, "bin", "copier")
560
+ ].map((path) => resolve8(path)));
561
+ if (!supportedEntries.has(resolve8(candidate))) {
557
562
  return { ok: false, error: `refusing non-canonical UV Copier entry point: ${candidate}` };
558
563
  }
559
564
  try {
@@ -561,43 +566,43 @@ function attestUvCopier(candidate, realCandidate, home) {
561
566
  if (!launcher.ok || !launcher.interpreter) {
562
567
  return { ...launcher, executable: realCandidate, realExecutable: realCandidate };
563
568
  }
564
- const expectedInterpreter = join8(toolRoot, "bin", basename5(launcher.interpreter));
565
- if (resolve7(launcher.interpreter) !== resolve7(expectedInterpreter)) {
569
+ const expectedInterpreter = join9(toolRoot, "bin", basename5(launcher.interpreter));
570
+ if (resolve8(launcher.interpreter) !== resolve8(expectedInterpreter)) {
566
571
  return { ok: false, error: "UV Copier launcher interpreter is outside the attested tool environment" };
567
572
  }
568
573
  const interpreterReal = realpathSync4(launcher.interpreter);
569
574
  const uvPythonRoots = [
570
- join8(home, ".local", "share", "uv", "python"),
571
- join8(home, "Library", "Application Support", "uv", "python")
575
+ join9(home, ".local", "share", "uv", "python"),
576
+ join9(home, "Library", "Application Support", "uv", "python")
572
577
  ];
573
578
  if (!uvPythonRoots.some((root) => containedBy(root, interpreterReal))) {
574
579
  return { ok: false, error: "UV Copier interpreter is not managed by the canonical UV Python installation" };
575
580
  }
576
- const receiptPath = join8(toolRoot, "uv-receipt.toml");
577
- const receipt = readFileSync5(receiptPath, "utf8");
581
+ const receiptPath = join9(toolRoot, "uv-receipt.toml");
582
+ const receipt = readFileSync6(receiptPath, "utf8");
578
583
  if (!/requirements\s*=\s*\[[\s\S]*?name\s*=\s*["']copier["']/.test(receipt) || !/entrypoints\s*=\s*\[[\s\S]*?name\s*=\s*["']copier["'][\s\S]*?from\s*=\s*["']copier["']/.test(receipt)) {
579
584
  return { ok: false, error: "UV tool receipt does not bind the copier entry point to the Copier package" };
580
585
  }
581
586
  const sitePackages = locateUvSitePackages(toolRoot);
582
- const distInfos = readdirSync5(sitePackages, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^copier-[^-]+\.dist-info$/i.test(entry.name)).map((entry) => join8(sitePackages, entry.name));
587
+ const distInfos = readdirSync5(sitePackages, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^copier-[^-]+\.dist-info$/i.test(entry.name)).map((entry) => join9(sitePackages, entry.name));
583
588
  if (distInfos.length !== 1) {
584
589
  return { ok: false, error: `expected one installed Copier distribution, found ${distInfos.length}` };
585
590
  }
586
591
  const distInfo = realpathSync4(distInfos[0]);
587
- const metadataPath = join8(distInfo, "METADATA");
588
- const entryPointsPath = join8(distInfo, "entry_points.txt");
589
- const recordPath = join8(distInfo, "RECORD");
590
- const metadata = readFileSync5(metadataPath, "utf8");
592
+ const metadataPath = join9(distInfo, "METADATA");
593
+ const entryPointsPath = join9(distInfo, "entry_points.txt");
594
+ const recordPath = join9(distInfo, "RECORD");
595
+ const metadata = readFileSync6(metadataPath, "utf8");
591
596
  const name = metadata.match(/^Name:\s*(.+)$/mi)?.[1]?.trim();
592
597
  const version = metadata.match(/^Version:\s*(.+)$/mi)?.[1]?.trim();
593
598
  if (name?.toLowerCase() !== "copier" || !version || !/^9(?:\.|$)/.test(version)) {
594
599
  return { ok: false, error: "installed distribution is not a Copier 9 package" };
595
600
  }
596
- const entryPoints = readFileSync5(entryPointsPath, "utf8");
601
+ const entryPoints = readFileSync6(entryPointsPath, "utf8");
597
602
  if (!/^copier\s*=\s*copier\.__main__:CopierApp\.run\s*$/m.test(entryPoints)) {
598
603
  return { ok: false, error: "installed Copier distribution has an unexpected console entry point" };
599
604
  }
600
- const recordEntries = readFileSync5(recordPath, "utf8").split(/\r?\n/).map(parseRecordLine).filter((entry) => Boolean(entry));
605
+ const recordEntries = readFileSync6(recordPath, "utf8").split(/\r?\n/).map(parseRecordLine).filter((entry) => Boolean(entry));
601
606
  const selected = recordEntries.filter((entry) => {
602
607
  const normalized = entry.relativePath.replaceAll("\\", "/");
603
608
  return normalized.startsWith("copier/") || normalized === `${basename5(distInfo)}/METADATA` || normalized === `${basename5(distInfo)}/entry_points.txt` || normalized === "../../../bin/copier";
@@ -607,7 +612,7 @@ function attestUvCopier(candidate, realCandidate, home) {
607
612
  }
608
613
  const attestedFiles = /* @__PURE__ */ new Map();
609
614
  for (const entry of selected) {
610
- const path = resolve7(sitePackages, entry.relativePath);
615
+ const path = resolve8(sitePackages, entry.relativePath);
611
616
  const allowed = path === realCandidate || containedBy(sitePackages, path);
612
617
  if (!allowed) return { ok: false, error: `Copier RECORD path escapes the tool environment: ${entry.relativePath}` };
613
618
  const actualDigest = sha256(path);
@@ -615,14 +620,14 @@ function attestUvCopier(candidate, realCandidate, home) {
615
620
  if (actualDigest !== entry.digest || actualSize !== entry.size) {
616
621
  return { ok: false, error: `Copier RECORD integrity mismatch: ${entry.relativePath}` };
617
622
  }
618
- attestedFiles.set(resolve7(path), fingerprint(path));
623
+ attestedFiles.set(resolve8(path), fingerprint(path));
619
624
  }
620
- for (const path of [realCandidate, join8(toolRoot, "pyvenv.cfg"), receiptPath, metadataPath, entryPointsPath, recordPath]) {
621
- attestedFiles.set(resolve7(path), fingerprint(path));
625
+ for (const path of [realCandidate, join9(toolRoot, "pyvenv.cfg"), receiptPath, metadataPath, entryPointsPath, recordPath]) {
626
+ attestedFiles.set(resolve8(path), fingerprint(path));
622
627
  }
623
628
  const identity = {
624
629
  executable: realCandidate,
625
- resolvedFrom: resolve7(candidate),
630
+ resolvedFrom: resolve8(candidate),
626
631
  layout: "uv-tool",
627
632
  version,
628
633
  toolRoot,
@@ -641,7 +646,7 @@ function attestUvCopier(candidate, realCandidate, home) {
641
646
  }
642
647
  }
643
648
  function verifyTrustedCopierIdentity(identity) {
644
- if (!isAbsolute3(identity.executable) || resolve7(identity.executable) !== resolve7(identity.files.find((file) => file.path === identity.executable)?.path ?? "")) {
649
+ if (!isAbsolute4(identity.executable) || resolve8(identity.executable) !== resolve8(identity.files.find((file) => file.path === identity.executable)?.path ?? "")) {
645
650
  return { ok: false, error: "trusted Copier identity has no canonical absolute launcher" };
646
651
  }
647
652
  const interpreter = sameFingerprint(identity.interpreter);
@@ -654,9 +659,9 @@ function verifyTrustedCopierIdentity(identity) {
654
659
  }
655
660
  function preflightTrustedCopier(options) {
656
661
  const env2 = options.env ?? process.env;
657
- const home = resolve7(options.homeDir ?? userInfo().homedir);
658
- const temporary = resolve7(options.temporaryDir ?? tmpdir());
659
- const target = resolve7(options.targetDir);
662
+ const home = resolve8(options.homeDir ?? userInfo().homedir);
663
+ const temporary = resolve8(options.temporaryDir ?? tmpdir());
664
+ const target = resolve8(options.targetDir);
660
665
  const { candidate, skipped } = resolveCopierOnPath(env2, accountMiseShimsDir(home));
661
666
  for (const shim of skipped) {
662
667
  for (const [label, root] of [["target", target], ["temporary", temporary]]) {
@@ -692,18 +697,18 @@ function regularContainedFile(root, path, label) {
692
697
  const rootReal = realpathSync4(root);
693
698
  const fileReal = realpathSync4(path);
694
699
  if (!containedBy(rootReal, fileReal)) return { ok: false, error: `${label} escapes its vendored template root` };
695
- if (!lstatSync6(path).isFile()) return { ok: false, error: `${label} is not a regular file` };
700
+ if (!lstatSync7(path).isFile()) return { ok: false, error: `${label} is not a regular file` };
696
701
  return { ok: true };
697
702
  } catch (error) {
698
703
  return { ok: false, error: `${label} is unavailable: ${error instanceof Error ? error.message : String(error)}` };
699
704
  }
700
705
  }
701
706
  function parseCopierConfig(templateRoot, label) {
702
- const configPath = join8(templateRoot, "copier.yml");
707
+ const configPath = join9(templateRoot, "copier.yml");
703
708
  const file = regularContainedFile(templateRoot, configPath, `${label} copier.yml`);
704
709
  if (!file.ok) return { result: file };
705
710
  try {
706
- const parsed = YAML3.parse(readFileSync5(configPath, "utf8"));
711
+ const parsed = YAML3.parse(readFileSync6(configPath, "utf8"));
707
712
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
708
713
  return { result: { ok: false, error: `${label} copier.yml must contain a mapping` } };
709
714
  }
@@ -721,13 +726,13 @@ function parseCopierConfig(templateRoot, label) {
721
726
  }
722
727
  function requireFiles(templateRoot, files, label) {
723
728
  for (const rel of files) {
724
- const result2 = regularContainedFile(templateRoot, join8(templateRoot, rel), `${label} ${rel}`);
729
+ const result2 = regularContainedFile(templateRoot, join9(templateRoot, rel), `${label} ${rel}`);
725
730
  if (!result2.ok) return result2;
726
731
  }
727
732
  return { ok: true };
728
733
  }
729
734
  function preflightCommonProjectTemplate(pjanglerRoot) {
730
- const templateRoot = join8(resolve7(pjanglerRoot), "templates", "commonproject");
735
+ const templateRoot = join9(resolve8(pjanglerRoot), "templates", "commonproject");
731
736
  const parsed = parseCopierConfig(templateRoot, "CommonProject template");
732
737
  if (!parsed.result.ok) return parsed.result;
733
738
  const files = requireFiles(templateRoot, [
@@ -738,7 +743,7 @@ function preflightCommonProjectTemplate(pjanglerRoot) {
738
743
  "template/mise.toml.jinja"
739
744
  ], "CommonProject template");
740
745
  if (!files.ok) return files;
741
- const projectJson = readFileSync5(join8(templateRoot, "template", ".project.json.jinja"), "utf8");
746
+ const projectJson = readFileSync6(join9(templateRoot, "template", ".project.json.jinja"), "utf8");
742
747
  for (const key of ["project_name", "repo_path", "ticket_provider", "agents"]) {
743
748
  if (!projectJson.includes(`"${key}"`)) return { ok: false, error: `CommonProject projection is missing ${key}` };
744
749
  }
@@ -763,12 +768,12 @@ var init_preflight = __esm({
763
768
  });
764
769
 
765
770
  // src/project/boardUrl.ts
766
- import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
767
- import { dirname as dirname8, isAbsolute as isAbsolute4, join as join9, resolve as resolve8 } from "node:path";
771
+ import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync3 } from "node:fs";
772
+ import { dirname as dirname8, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "node:path";
768
773
  function findProjectRoot(from) {
769
- let dir = resolve8(from);
774
+ let dir = resolve9(from);
770
775
  for (; ; ) {
771
- if (existsSync6(join9(dir, ".project.json"))) return dir;
776
+ if (existsSync6(join10(dir, ".project.json"))) return dir;
772
777
  const parent = dirname8(dir);
773
778
  if (parent === dir) return void 0;
774
779
  dir = parent;
@@ -781,8 +786,8 @@ var init_boardUrl = __esm({
781
786
  });
782
787
 
783
788
  // src/utils/version.ts
784
- import { readFileSync as readFileSync7 } from "node:fs";
785
- import { dirname as dirname9, join as join10 } from "node:path";
789
+ import { readFileSync as readFileSync8 } from "node:fs";
790
+ import { dirname as dirname9, join as join11 } from "node:path";
786
791
  import { fileURLToPath } from "node:url";
787
792
  var PJANGLER_VERSION;
788
793
  var init_version = __esm({
@@ -793,7 +798,7 @@ var init_version = __esm({
793
798
  let dir = dirname9(fileURLToPath(import.meta.url));
794
799
  for (let i = 0; i < 4; i++) {
795
800
  try {
796
- const raw = readFileSync7(join10(dir, "package.json"), "utf8");
801
+ const raw = readFileSync8(join11(dir, "package.json"), "utf8");
797
802
  return JSON.parse(raw).version ?? "0.0.0";
798
803
  } catch {
799
804
  const parent = dirname9(dir);
@@ -822,10 +827,10 @@ var init_boardQuery = __esm({
822
827
  });
823
828
 
824
829
  // src/project/index.ts
825
- import { spawnSync as spawnSync4 } from "node:child_process";
826
- import { chmodSync as chmodSync2, closeSync, copyFileSync as copyFileSync2, existsSync as existsSync7, fchmodSync, fsyncSync, lstatSync as lstatSync7, mkdirSync as mkdirSync4, mkdtempSync, openSync, readFileSync as readFileSync8, realpathSync as realpathSync5, renameSync as renameSync3, rmSync as rmSync3, statSync as statSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
830
+ import { spawnSync as spawnSync5 } from "node:child_process";
831
+ import { chmodSync as chmodSync2, closeSync, copyFileSync as copyFileSync2, existsSync as existsSync7, fchmodSync, fsyncSync, lstatSync as lstatSync8, mkdirSync as mkdirSync4, mkdtempSync, openSync, readFileSync as readFileSync9, realpathSync as realpathSync5, renameSync as renameSync3, rmSync as rmSync3, statSync as statSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
827
832
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
828
- import { basename as basename6, delimiter as delimiter2, dirname as dirname10, isAbsolute as isAbsolute5, join as join11, relative as relative7, resolve as resolve9, sep as sep2, win32 } from "node:path";
833
+ import { basename as basename6, delimiter as delimiter2, dirname as dirname10, isAbsolute as isAbsolute6, join as join12, relative as relative7, resolve as resolve10, sep as sep2, win32 } from "node:path";
829
834
  import { fileURLToPath as fileURLToPath2 } from "node:url";
830
835
  import YAML4 from "yaml";
831
836
  function normalizeProjectType(value, label = "Project type") {
@@ -835,7 +840,7 @@ function normalizeProjectType(value, label = "Project type") {
835
840
  }
836
841
  function looksLikeObsidianPlugin(dir) {
837
842
  try {
838
- const manifest = JSON.parse(readFileSync8(join11(dir, "manifest.json"), "utf8"));
843
+ const manifest = JSON.parse(readFileSync9(join12(dir, "manifest.json"), "utf8"));
839
844
  return isRecord2(manifest) && typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
840
845
  } catch {
841
846
  return false;
@@ -843,10 +848,10 @@ function looksLikeObsidianPlugin(dir) {
843
848
  }
844
849
  function assertTemplateOffersProjectType(pjanglerRoot, projectType) {
845
850
  if (projectType === "base") return;
846
- const copierYml = join11(pjanglerRoot, "templates", "commonproject", "copier.yml");
851
+ const copierYml = join12(pjanglerRoot, "templates", "commonproject", "copier.yml");
847
852
  let choices;
848
853
  try {
849
- const parsed = YAML4.parse(readFileSync8(copierYml, "utf8"));
854
+ const parsed = YAML4.parse(readFileSync9(copierYml, "utf8"));
850
855
  choices = isRecord2(parsed) && isRecord2(parsed.project_type) ? parsed.project_type.choices : void 0;
851
856
  } catch {
852
857
  choices = void 0;
@@ -857,7 +862,7 @@ function assertTemplateOffersProjectType(pjanglerRoot, projectType) {
857
862
  }
858
863
  }
859
864
  function operatorDisplayName(cwd) {
860
- const result2 = spawnSync4("git", ["config", "--get", "user.name"], { cwd: existsSync7(cwd) ? cwd : void 0, encoding: "utf8", timeout: 5e3 });
865
+ const result2 = spawnSync5("git", ["config", "--get", "user.name"], { cwd: existsSync7(cwd) ? cwd : void 0, encoding: "utf8", timeout: 5e3 });
861
866
  return result2.status === 0 ? result2.stdout.trim() : "";
862
867
  }
863
868
  function providerAssignsIdentifiers(type) {
@@ -869,9 +874,9 @@ function carriedAutomation(automation) {
869
874
  return Object.keys(rest).length ? { automation: rest } : {};
870
875
  }
871
876
  function synchronizeCopierIdentity(manifestPath, manifest) {
872
- const answersPath = join11(dirname10(manifestPath), ".copier-answers.yml");
877
+ const answersPath = join12(dirname10(manifestPath), ".copier-answers.yml");
873
878
  if (!existsSync7(answersPath)) return [];
874
- const current = readFileSync8(answersPath, "utf8");
879
+ const current = readFileSync9(answersPath, "utf8");
875
880
  const document = YAML4.parseDocument(current);
876
881
  if (document.errors.length) return [];
877
882
  const name = String(document.get("project_name") ?? "");
@@ -888,12 +893,12 @@ function projectRegistryPath(env2 = process.env) {
888
893
  return resolveRegistryLocation(void 0, env2);
889
894
  }
890
895
  function createSafeRecord(entries = []) {
891
- const record2 = /* @__PURE__ */ Object.create(null);
892
- for (const [key, value] of entries) record2[key] = value;
893
- return record2;
896
+ const record3 = /* @__PURE__ */ Object.create(null);
897
+ for (const [key, value] of entries) record3[key] = value;
898
+ return record3;
894
899
  }
895
- function getOwnRecordValue(record2, key) {
896
- return Object.hasOwn(record2, key) ? record2[key] : void 0;
900
+ function getOwnRecordValue(record3, key) {
901
+ return Object.hasOwn(record3, key) ? record3[key] : void 0;
897
902
  }
898
903
  function emptyProjectRegistry() {
899
904
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: createSafeRecord() };
@@ -905,7 +910,7 @@ function loadProjectRegistry(path = projectRegistryPath()) {
905
910
  return registry2;
906
911
  }
907
912
  if (!existsSync7(path)) return emptyProjectRegistry();
908
- const raw = YAML4.parse(readFileSync8(path, "utf8"));
913
+ const raw = YAML4.parse(readFileSync9(path, "utf8"));
909
914
  if (raw == null) return emptyProjectRegistry();
910
915
  if (!isRecord2(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
911
916
  const registry = raw;
@@ -995,10 +1000,10 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
995
1000
  let text;
996
1001
  let mode = 420;
997
1002
  if (existsSync7(path)) {
998
- const stat = lstatSync7(path);
1003
+ const stat = lstatSync8(path);
999
1004
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Project registry must be a regular file: ${path}`);
1000
1005
  mode = stat.mode & 511;
1001
- const current = readFileSync8(path, "utf8");
1006
+ const current = readFileSync9(path, "utf8");
1002
1007
  const document = YAML4.parseDocument(current);
1003
1008
  if (document.errors.length) throw new Error(`Project registry YAML is invalid: ${path}`);
1004
1009
  setYamlLeaf(document, ["schema_version"], registry.schema_version);
@@ -1065,7 +1070,7 @@ function validateProjectRegistry(registry) {
1065
1070
  validateProjectRecord(project, slug);
1066
1071
  if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
1067
1072
  slugs.add(project.slug);
1068
- const repoKey = resolve9(project.repo_path);
1073
+ const repoKey = resolve10(project.repo_path);
1069
1074
  const existingRepoSlug = repoPaths.get(repoKey);
1070
1075
  if (existingRepoSlug && existingRepoSlug !== slug) {
1071
1076
  throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
@@ -1142,18 +1147,18 @@ function ticketProviderKeyVars(provider, workspace) {
1142
1147
  return ticketProviderKeyGroups(provider, workspace).flat();
1143
1148
  }
1144
1149
  function ticketProviderSecretsPath(env2 = process.env) {
1145
- const base = env2.XDG_CONFIG_HOME || join11(env2.HOME || homedir3(), ".config");
1146
- return join11(base, "zshyzsh", "secrets.zsh");
1150
+ const base = env2.XDG_CONFIG_HOME || join12(env2.HOME || homedir3(), ".config");
1151
+ return join12(base, "zshyzsh", "secrets.zsh");
1147
1152
  }
1148
1153
  function ticketProviderFleetEnvPath(env2 = process.env) {
1149
- return env2.HERMES_FLEET_ENV?.trim() || join11(env2.HOME || homedir3(), ".hermes", "fleet.env");
1154
+ return env2.HERMES_FLEET_ENV?.trim() || join12(env2.HOME || homedir3(), ".hermes", "fleet.env");
1150
1155
  }
1151
1156
  function readShellAssignments(path, keys) {
1152
1157
  const found = {};
1153
1158
  if (!existsSync7(path)) return found;
1154
1159
  let text;
1155
1160
  try {
1156
- text = readFileSync8(path, "utf8");
1161
+ text = readFileSync9(path, "utf8");
1157
1162
  } catch {
1158
1163
  return found;
1159
1164
  }
@@ -1189,7 +1194,7 @@ function resolveTicketProviderCredentials(input) {
1189
1194
  }
1190
1195
  }
1191
1196
  const candidates = [];
1192
- if (input.repoPath) candidates.push(join11(input.repoPath, ".env"));
1197
+ if (input.repoPath) candidates.push(join12(input.repoPath, ".env"));
1193
1198
  candidates.push(ticketProviderFleetEnvPath(env2));
1194
1199
  candidates.push(ticketProviderSecretsPath(env2));
1195
1200
  for (const candidate of candidates) {
@@ -1207,19 +1212,19 @@ function resolveTicketProviderAdapter(provider, env2 = process.env) {
1207
1212
  const file = `${provider}.sh`;
1208
1213
  const candidates = [];
1209
1214
  const override = env2[TICKET_PROVIDER_ADAPTERS_ENV];
1210
- if (override) candidates.push(join11(override, file));
1211
- const relativeRoot = join11("krebs", "adapters", "tp");
1215
+ if (override) candidates.push(join12(override, file));
1216
+ const relativeRoot = join12("krebs", "adapters", "tp");
1212
1217
  try {
1213
1218
  let dir = dirname10(fileURLToPath2(import.meta.url));
1214
1219
  for (let depth = 0; depth < 8; depth++) {
1215
- candidates.push(join11(dir, relativeRoot, file));
1220
+ candidates.push(join12(dir, relativeRoot, file));
1216
1221
  const parent = dirname10(dir);
1217
1222
  if (parent === dir) break;
1218
1223
  dir = parent;
1219
1224
  }
1220
1225
  } catch {
1221
1226
  }
1222
- candidates.push(join11(homedir3(), "code", "33GOD", relativeRoot, file));
1227
+ candidates.push(join12(homedir3(), "code", "33GOD", relativeRoot, file));
1223
1228
  return candidates.find((candidate) => existsSync7(candidate));
1224
1229
  }
1225
1230
  function provisionTicketProviderBoard(action, env2 = process.env) {
@@ -1237,7 +1242,7 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
1237
1242
  ok: true,
1238
1243
  skipped: true,
1239
1244
  logs: [
1240
- `ticket-provider: ${names} not set; skipping ${provider} board creation (state stays "planned"). Set it in the environment, ${join11(action.repoPath, ".env")}, ${ticketProviderFleetEnvPath(env2)}, or ${ticketProviderSecretsPath(env2)} \u2014 or pass --board-id to link an existing board.`
1245
+ `ticket-provider: ${names} not set; skipping ${provider} board creation (state stays "planned"). Set it in the environment, ${join12(action.repoPath, ".env")}, ${ticketProviderFleetEnvPath(env2)}, or ${ticketProviderSecretsPath(env2)} \u2014 or pass --board-id to link an existing board.`
1241
1246
  ]
1242
1247
  };
1243
1248
  }
@@ -1251,12 +1256,12 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
1251
1256
  };
1252
1257
  }
1253
1258
  const redact = (text) => Object.values(values).reduce((acc, secret) => secret ? acc.split(secret).join("***") : acc, text);
1254
- const staging = mkdtempSync(join11(tmpdir2(), "pjangler-tp-"));
1259
+ const staging = mkdtempSync(join12(tmpdir2(), "pjangler-tp-"));
1255
1260
  try {
1256
- const providersDir = join11(staging, ".tp", "adapters");
1261
+ const providersDir = join12(staging, ".tp", "adapters");
1257
1262
  mkdirSync4(providersDir, { recursive: true });
1258
1263
  writeFileSync4(
1259
- join11(staging, ".project.json"),
1264
+ join12(staging, ".project.json"),
1260
1265
  `${JSON.stringify(
1261
1266
  {
1262
1267
  project_name: action.boardName,
@@ -1275,11 +1280,11 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
1275
1280
  `,
1276
1281
  "utf8"
1277
1282
  );
1278
- const staged = join11(providersDir, `${provider}.sh`);
1283
+ const staged = join12(providersDir, `${provider}.sh`);
1279
1284
  copyFileSync2(adapter, staged);
1280
1285
  const childEnv = { ...env2, ...values, TICKET_PROVIDER: provider };
1281
1286
  if (provider === "plane" && action.workspace) childEnv.PLANE_WORKSPACE = action.workspace;
1282
- const result2 = spawnSync4("sh", [staged, "create_board", action.boardName, action.identifier, action.description], {
1287
+ const result2 = spawnSync5("sh", [staged, "create_board", action.boardName, action.identifier, action.description], {
1283
1288
  cwd: existsSync7(action.repoPath) ? action.repoPath : staging,
1284
1289
  encoding: "utf8",
1285
1290
  env: childEnv
@@ -1350,7 +1355,7 @@ function slugifyProjectName(value) {
1350
1355
  }
1351
1356
  function validateSafePathSegment(value, label) {
1352
1357
  const normalized = value.trim();
1353
- const unsafe = !normalized || normalized !== value || normalized === "." || normalized === ".." || isAbsolute5(normalized) || win32.isAbsolute(normalized) || normalized.includes("/") || normalized.includes("\\") || !SAFE_PATH_SEGMENT.test(normalized);
1358
+ const unsafe = !normalized || normalized !== value || normalized === "." || normalized === ".." || isAbsolute6(normalized) || win32.isAbsolute(normalized) || normalized.includes("/") || normalized.includes("\\") || !SAFE_PATH_SEGMENT.test(normalized);
1354
1359
  if (unsafe) {
1355
1360
  throw new Error(
1356
1361
  `${label} must be a non-empty safe single path segment using letters, numbers, dots, underscores, or hyphens (no dot segments, absolute paths, separators, or traversal)`
@@ -1359,24 +1364,24 @@ function validateSafePathSegment(value, label) {
1359
1364
  return normalized;
1360
1365
  }
1361
1366
  function prospectiveRealPath(path) {
1362
- let cursor = resolve9(path);
1367
+ let cursor = resolve10(path);
1363
1368
  const suffix = [];
1364
1369
  while (!existsSync7(cursor)) {
1365
1370
  const parent = dirname10(cursor);
1366
- if (parent === cursor) return resolve9(path);
1371
+ if (parent === cursor) return resolve10(path);
1367
1372
  suffix.unshift(basename6(cursor));
1368
1373
  cursor = parent;
1369
1374
  }
1370
- return resolve9(realpathSync5(cursor), ...suffix);
1375
+ return resolve10(realpathSync5(cursor), ...suffix);
1371
1376
  }
1372
1377
  function resolveContainedPath(parentDir, candidate, label) {
1373
1378
  const physicalParent = prospectiveRealPath(parentDir);
1374
1379
  const physicalCandidate = prospectiveRealPath(candidate);
1375
1380
  const fromParent = relative7(physicalParent, physicalCandidate);
1376
- if (!fromParent || fromParent === ".." || fromParent.startsWith(`..${sep2}`) || isAbsolute5(fromParent)) {
1377
- throw new Error(`${label} must remain contained beneath parent directory ${resolve9(parentDir)}`);
1381
+ if (!fromParent || fromParent === ".." || fromParent.startsWith(`..${sep2}`) || isAbsolute6(fromParent)) {
1382
+ throw new Error(`${label} must remain contained beneath parent directory ${resolve10(parentDir)}`);
1378
1383
  }
1379
- return resolve9(candidate);
1384
+ return resolve10(candidate);
1380
1385
  }
1381
1386
  function proposeProjectIdentifier(value) {
1382
1387
  const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
@@ -1391,7 +1396,7 @@ function resolveAgentHooksLayer2(input, env2 = process.env) {
1391
1396
  const override = env2.PJ_AGENT_HOOKS_LAYER;
1392
1397
  if (override === "0" || override === "false") return false;
1393
1398
  if (override === "1" || override === "true") return true;
1394
- return !existsSync7(join11(homedir3(), ".agents", "hooks"));
1399
+ return !existsSync7(join12(homedir3(), ".agents", "hooks"));
1395
1400
  }
1396
1401
  function jsonStable(value) {
1397
1402
  return JSON.stringify(value, (_key, item) => isRecord2(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item);
@@ -1405,14 +1410,14 @@ function projectRecordEquivalent(a, b) {
1405
1410
  function defaultProjectTargetDir(name, cwd = process.cwd()) {
1406
1411
  const compactName = name.replace(/[^A-Za-z0-9._-]/g, "");
1407
1412
  const safeName = SAFE_PATH_SEGMENT.test(compactName) ? compactName : slugifyProjectName(name);
1408
- return resolve9(dirname10(resolve9(cwd)), validateSafePathSegment(safeName, "Generated project directory"));
1413
+ return resolve10(dirname10(resolve10(cwd)), validateSafePathSegment(safeName, "Generated project directory"));
1409
1414
  }
1410
1415
  function sourceSkillRoots(env2 = process.env) {
1411
1416
  const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter2).map((root) => root.trim()).filter(Boolean);
1412
1417
  const seen = /* @__PURE__ */ new Set();
1413
1418
  const roots = [];
1414
1419
  for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
1415
- const normalized = resolve9(expandHome(root));
1420
+ const normalized = resolve10(expandHome(root));
1416
1421
  if (seen.has(normalized)) continue;
1417
1422
  seen.add(normalized);
1418
1423
  roots.push(normalized);
@@ -1422,12 +1427,12 @@ function sourceSkillRoots(env2 = process.env) {
1422
1427
  function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1423
1428
  if (!sourceSkill) return void 0;
1424
1429
  const expanded = expandHome(sourceSkill);
1425
- const direct = resolve9(expanded);
1430
+ const direct = resolve10(expanded);
1426
1431
  if (existsSync7(direct)) return direct;
1427
1432
  const name = basename6(sourceSkill);
1428
1433
  const roots = sourceSkillRoots(env2);
1429
1434
  for (const root of roots) {
1430
- const candidate = join11(root, name);
1435
+ const candidate = join12(root, name);
1431
1436
  if (existsSync7(candidate)) return candidate;
1432
1437
  }
1433
1438
  const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
@@ -1436,9 +1441,9 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1436
1441
  }
1437
1442
  function planProjectInit(input) {
1438
1443
  if (!input.name.trim()) throw new Error("Project name is required");
1439
- const targetDir = resolve9(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
1440
- const localManifestPath = join11(targetDir, ".project.json");
1441
- const localManifestContent = existsSync7(localManifestPath) ? readFileSync8(localManifestPath, "utf8") : void 0;
1444
+ const targetDir = resolve10(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
1445
+ const localManifestPath = join12(targetDir, ".project.json");
1446
+ const localManifestContent = existsSync7(localManifestPath) ? readFileSync9(localManifestPath, "utf8") : void 0;
1442
1447
  const localManifest = localManifestContent === void 0 ? void 0 : JSON.parse(localManifestContent);
1443
1448
  const localId = localManifest?.project_id ?? localManifest?.project_slug;
1444
1449
  const slug = normalizeProjectId(input.projectSlug ?? localId ?? slugifyProjectName(input.name));
@@ -1466,7 +1471,7 @@ function planProjectInit(input) {
1466
1471
  const recordedTypeValue = existing?.template?.commonproject?.project_type;
1467
1472
  const recordedType = recordedTypeValue === void 0 ? void 0 : normalizeProjectType(recordedTypeValue, `Recorded project type of ${slug}`);
1468
1473
  const requestedType = input.projectType === void 0 ? void 0 : normalizeProjectType(input.projectType);
1469
- const freshRender = scaffold && localManifest === void 0 && !existsSync7(join11(targetDir, ".copier-answers.yml"));
1474
+ const freshRender = scaffold && localManifest === void 0 && !existsSync7(join12(targetDir, ".copier-answers.yml"));
1470
1475
  if (requestedType && !freshRender && requestedType !== (recordedType ?? "base")) {
1471
1476
  const adoptingPlugin = !scaffold && recordedType === void 0 && requestedType === "obsidian-plugin" && looksLikeObsidianPlugin(targetDir);
1472
1477
  if (!adoptingPlugin) {
@@ -1532,7 +1537,7 @@ function planProjectInit(input) {
1532
1537
  updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
1533
1538
  };
1534
1539
  validateNoDuplicateProject(registry, project, overwrite);
1535
- const pjanglerRoot = resolve9(input.pjanglerRoot ?? resolvePjanglerRoot());
1540
+ const pjanglerRoot = resolve10(input.pjanglerRoot ?? resolvePjanglerRoot());
1536
1541
  const manifest = projectManifestFromRegistryProject(project);
1537
1542
  const apply = input.apply ?? false;
1538
1543
  const live = input.live ?? false;
@@ -1564,7 +1569,7 @@ function planProjectInit(input) {
1564
1569
  }));
1565
1570
  }
1566
1571
  actions.push(
1567
- { kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest, expectedContent: localManifestContent },
1572
+ { kind: "project.write-manifest", path: join12(targetDir, ".project.json"), manifest, expectedContent: localManifestContent },
1568
1573
  {
1569
1574
  kind: "ticket-provider.create-or-link",
1570
1575
  enabled: boardEnabled,
@@ -1619,12 +1624,12 @@ function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__
1619
1624
  plan.manifest.ticket_provider = manifestProvider;
1620
1625
  action.boardId = boardId;
1621
1626
  action.state = manifestProvider.state;
1622
- const manifestPath = join11(action.repoPath, ".project.json");
1627
+ const manifestPath = join12(action.repoPath, ".project.json");
1623
1628
  let next;
1624
1629
  if (existsSync7(manifestPath)) {
1625
1630
  let existing = {};
1626
1631
  try {
1627
- const parsed = JSON.parse(readFileSync8(manifestPath, "utf8"));
1632
+ const parsed = JSON.parse(readFileSync9(manifestPath, "utf8"));
1628
1633
  if (isRecord2(parsed)) existing = parsed;
1629
1634
  } catch {
1630
1635
  existing = {};
@@ -1637,7 +1642,7 @@ function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__
1637
1642
  }
1638
1643
  const text = `${JSON.stringify(next, null, 2)}
1639
1644
  `;
1640
- if (!existsSync7(manifestPath) || readFileSync8(manifestPath, "utf8") !== text) {
1645
+ if (!existsSync7(manifestPath) || readFileSync9(manifestPath, "utf8") !== text) {
1641
1646
  writeFileSync4(manifestPath, text, "utf8");
1642
1647
  return [manifestPath];
1643
1648
  }
@@ -1677,7 +1682,7 @@ async function executeProjectInitPlan(plan, options = {}) {
1677
1682
  copierEnv.PYTHONSAFEPATH = "1";
1678
1683
  }
1679
1684
  copierEnv.MISE_YES = "1";
1680
- const result2 = spawnSync4(copierExecutable, action.command.slice(1), {
1685
+ const result2 = spawnSync5(copierExecutable, action.command.slice(1), {
1681
1686
  encoding: "utf8",
1682
1687
  cwd: action.cwd,
1683
1688
  env: copierEnv
@@ -1698,14 +1703,14 @@ async function executeProjectInitPlan(plan, options = {}) {
1698
1703
  break;
1699
1704
  }
1700
1705
  const projectType = action.data.project_type ?? "base";
1701
- const missing = (PROJECT_TYPE_SENTINELS[projectType] ?? []).filter((rel) => !existsSync7(join11(action.targetDir, rel)));
1706
+ const missing = (PROJECT_TYPE_SENTINELS[projectType] ?? []).filter((rel) => !existsSync7(join12(action.targetDir, rel)));
1702
1707
  if (missing.length) {
1703
1708
  errors.push(`copier rendered no ${projectType} project: ${missing.join(", ")} missing from ${action.targetDir}`);
1704
1709
  break;
1705
1710
  }
1706
1711
  if (projectType !== "base") logs.push(`commonproject: rendered as ${projectType}`);
1707
1712
  } else if (action.kind === "project.write-manifest") {
1708
- if (action.expectedContent !== void 0 && (!existsSync7(action.path) || readFileSync8(action.path, "utf8") !== action.expectedContent)) {
1713
+ if (action.expectedContent !== void 0 && (!existsSync7(action.path) || readFileSync9(action.path, "utf8") !== action.expectedContent)) {
1709
1714
  errors.push(`Manifest changed after planning: ${action.path}; re-plan before applying`);
1710
1715
  break;
1711
1716
  }
@@ -1713,7 +1718,7 @@ async function executeProjectInitPlan(plan, options = {}) {
1713
1718
  let value = action.manifest;
1714
1719
  if (existsSync7(action.path)) {
1715
1720
  try {
1716
- const currentValue = JSON.parse(readFileSync8(action.path, "utf8"));
1721
+ const currentValue = JSON.parse(readFileSync9(action.path, "utf8"));
1717
1722
  if (isRecord2(currentValue)) {
1718
1723
  const currentNotebook = isRecord2(currentValue.notebook) ? currentValue.notebook : {};
1719
1724
  const desiredNotebook = isRecord2(value.notebook) ? value.notebook : {};
@@ -1741,7 +1746,7 @@ async function executeProjectInitPlan(plan, options = {}) {
1741
1746
  value.project_id = normalizeProjectId(value.project_id ?? action.manifest.project_id);
1742
1747
  const next = `${JSON.stringify(value, null, 2)}
1743
1748
  `;
1744
- const current = existsSync7(action.path) ? readFileSync8(action.path, "utf8") : void 0;
1749
+ const current = existsSync7(action.path) ? readFileSync9(action.path, "utf8") : void 0;
1745
1750
  if (current !== next) {
1746
1751
  writeFileSync4(action.path, next, "utf8");
1747
1752
  changedFiles.push(action.path);
@@ -1824,7 +1829,7 @@ function getProject(registry, slug) {
1824
1829
  return project;
1825
1830
  }
1826
1831
  function buildCommonProjectCopierAction(input) {
1827
- const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
1832
+ const templateDir = join12(input.pjanglerRoot, "templates", "commonproject");
1828
1833
  const data = {
1829
1834
  project_name: input.projectName,
1830
1835
  project_description: input.projectDescription ?? "",
@@ -1855,19 +1860,19 @@ function buildCommonProjectCopierAction(input) {
1855
1860
  function resolvePjanglerRoot() {
1856
1861
  let dir = dirname10(fileURLToPath2(import.meta.url));
1857
1862
  while (dir !== dirname10(dir)) {
1858
- if (existsSync7(join11(dir, "package.json")) && existsSync7(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
1863
+ if (existsSync7(join12(dir, "package.json")) && existsSync7(join12(dir, "templates", "commonproject", "copier.yml"))) return dir;
1859
1864
  dir = dirname10(dir);
1860
1865
  }
1861
- return resolve9(process.cwd());
1866
+ return resolve10(process.cwd());
1862
1867
  }
1863
1868
  function validateNoDuplicateProject(registry, project, overwrite) {
1864
1869
  const existingSameSlug = getOwnRecordValue(registry.projects, project.slug);
1865
- if (existingSameSlug && !overwrite && resolve9(existingSameSlug.repo_path) !== resolve9(project.repo_path)) {
1870
+ if (existingSameSlug && !overwrite && resolve10(existingSameSlug.repo_path) !== resolve10(project.repo_path)) {
1866
1871
  throw new Error(`Project slug already exists in registry: ${project.slug}`);
1867
1872
  }
1868
1873
  for (const [slug, existing] of Object.entries(registry.projects)) {
1869
1874
  if (slug === project.slug) continue;
1870
- if (resolve9(existing.repo_path) === resolve9(project.repo_path)) {
1875
+ if (resolve10(existing.repo_path) === resolve10(project.repo_path)) {
1871
1876
  throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
1872
1877
  }
1873
1878
  if (ticketProviderScope(existing.ticket_provider) !== ticketProviderScope(project.ticket_provider)) continue;
@@ -1932,7 +1937,7 @@ function validateProjectRecord(project, key) {
1932
1937
  }
1933
1938
  function expandHome(path) {
1934
1939
  if (path === "~") return homedir3();
1935
- if (path.startsWith("~/")) return join11(homedir3(), path.slice(2));
1940
+ if (path.startsWith("~/")) return join12(homedir3(), path.slice(2));
1936
1941
  return path;
1937
1942
  }
1938
1943
  function isRecord2(value) {
@@ -1959,7 +1964,7 @@ var init_project = __esm({
1959
1964
  "obsidian-plugin": ["typescript", "javascript"]
1960
1965
  };
1961
1966
  PROJECT_TYPE_SENTINELS = {
1962
- "obsidian-plugin": ["manifest.json", join11("src", "main.ts")]
1967
+ "obsidian-plugin": ["manifest.json", join12("src", "main.ts")]
1963
1968
  };
1964
1969
  BOARD_URL_DEPRECATION_WARNING = "boardUrl is deprecated and ignored; board URLs are derived at runtime and are never persisted.";
1965
1970
  PROJECT_IDENTIFIER_SOURCES = ["provider", "proposed"];
@@ -1968,8 +1973,8 @@ var init_project = __esm({
1968
1973
  IDENTIFIER_REPAIR_COMMAND = "pj project identity --all --apply";
1969
1974
  DEFAULT_SOURCE_SKILL_ROOTS = [
1970
1975
  "/home/delorenj/code/skillex/all-skills",
1971
- join11(homedir3(), ".agents", "skills"),
1972
- join11(homedir3(), ".codex", "skills")
1976
+ join12(homedir3(), ".agents", "skills"),
1977
+ join12(homedir3(), ".codex", "skills")
1973
1978
  ];
1974
1979
  PROJECT_REGISTRY_OWNED_KEYS = [
1975
1980
  "name",
@@ -2001,21 +2006,21 @@ var init_project = __esm({
2001
2006
  import { randomUUID } from "node:crypto";
2002
2007
  import { isDeepStrictEqual } from "node:util";
2003
2008
  import { isIP as isIP2 } from "node:net";
2004
- import { closeSync as closeSync2, existsSync as existsSync8, fchmodSync as fchmodSync2, fsyncSync as fsyncSync2, lstatSync as lstatSync8, openSync as openSync2, readFileSync as readFileSync9, realpathSync as realpathSync6, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
2005
- import { dirname as dirname11, resolve as resolve10 } from "node:path";
2009
+ import { closeSync as closeSync2, existsSync as existsSync8, fchmodSync as fchmodSync2, fsyncSync as fsyncSync2, lstatSync as lstatSync9, openSync as openSync2, readFileSync as readFileSync10, realpathSync as realpathSync6, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
2010
+ import { dirname as dirname11, resolve as resolve11 } from "node:path";
2006
2011
  function isRecord3(value) {
2007
2012
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2008
2013
  }
2009
2014
  function realOrResolved(path) {
2010
- const absolute = resolve10(path);
2015
+ const absolute = resolve11(path);
2011
2016
  return existsSync8(absolute) ? realpathSync6(absolute) : absolute;
2012
2017
  }
2013
2018
  function readManifest(repoPath) {
2014
- const path = resolve10(repoPath, ".project.json");
2019
+ const path = resolve11(repoPath, ".project.json");
2015
2020
  if (!existsSync8(path)) return null;
2016
2021
  let parsed;
2017
2022
  try {
2018
- parsed = JSON.parse(readFileSync9(path, "utf8"));
2023
+ parsed = JSON.parse(readFileSync10(path, "utf8"));
2019
2024
  } catch {
2020
2025
  throw new NotebookError("INVALID_INPUT", `${path} is not valid JSON`);
2021
2026
  }
@@ -2255,9 +2260,9 @@ function registryProjectBySlug(registry, slug) {
2255
2260
  }
2256
2261
  function persistProjectNotebookBinding(resolved, binding, policy) {
2257
2262
  const changed = [];
2258
- const manifestPath = resolve10(resolved.project.repo_path, ".project.json");
2263
+ const manifestPath = resolve11(resolved.project.repo_path, ".project.json");
2259
2264
  if (isRegistryServiceLocation(resolved.registry_path)) {
2260
- const before = existsSync8(manifestPath) ? readFileSync9(manifestPath, "utf8") : null;
2265
+ const before = existsSync8(manifestPath) ? readFileSync10(manifestPath, "utf8") : null;
2261
2266
  const declaredPolicy = resolved.manifest?.notebook?.policy ?? {};
2262
2267
  const nextProject = {
2263
2268
  ...resolved.project,
@@ -2269,10 +2274,10 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2269
2274
  resolved.registry = loadProjectRegistry(resolved.registry_path);
2270
2275
  resolved.project = registryProjectBySlug(resolved.registry, nextProject.slug);
2271
2276
  resolved.manifest = readManifest(nextProject.repo_path);
2272
- const after = existsSync8(manifestPath) ? readFileSync9(manifestPath, "utf8") : null;
2277
+ const after = existsSync8(manifestPath) ? readFileSync10(manifestPath, "utf8") : null;
2273
2278
  return before === after ? [] : [manifestPath];
2274
2279
  }
2275
- const manifestRaw = existsSync8(manifestPath) ? JSON.parse(readFileSync9(manifestPath, "utf8")) : {};
2280
+ const manifestRaw = existsSync8(manifestPath) ? JSON.parse(readFileSync10(manifestPath, "utf8")) : {};
2276
2281
  if (!isRecord3(manifestRaw)) throw new NotebookError("INVALID_INPUT", `${manifestPath} must contain a JSON object`);
2277
2282
  validateManifestNotebookSurface(manifestRaw.notebook);
2278
2283
  const manifestNotebook2 = isRecord3(manifestRaw.notebook) ? manifestRaw.notebook : {};
@@ -2305,14 +2310,14 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2305
2310
  };
2306
2311
  const manifestText = `${JSON.stringify(manifestNext, null, 2)}
2307
2312
  `;
2308
- if (!existsSync8(manifestPath) || readFileSync9(manifestPath, "utf8") !== manifestText) {
2313
+ if (!existsSync8(manifestPath) || readFileSync10(manifestPath, "utf8") !== manifestText) {
2309
2314
  let mode = 420;
2310
2315
  if (existsSync8(manifestPath)) {
2311
- const current2 = lstatSync8(manifestPath);
2316
+ const current2 = lstatSync9(manifestPath);
2312
2317
  if (!current2.isFile() || current2.isSymbolicLink()) throw new NotebookError("CONFLICT", `${manifestPath} must be a regular non-symlink file`);
2313
2318
  mode = current2.mode & 511;
2314
2319
  }
2315
- const temp = resolve10(dirname11(manifestPath), `.${process.pid}.${randomUUID()}.project.json.tmp`);
2320
+ const temp = resolve11(dirname11(manifestPath), `.${process.pid}.${randomUUID()}.project.json.tmp`);
2316
2321
  const fd = openSync2(temp, "wx", mode);
2317
2322
  try {
2318
2323
  writeFileSync5(fd, manifestText, "utf8");
@@ -2336,9 +2341,9 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2336
2341
  const current = resolved.project.notebook;
2337
2342
  resolved.project.notebook = { ...isRecord3(current) ? current : { state: "planned" }, ...binding };
2338
2343
  const fileRegistry = !isRegistryServiceLocation(resolved.registry_path);
2339
- const registryBefore = fileRegistry && existsSync8(resolved.registry_path) ? readFileSync9(resolved.registry_path, "utf8") : null;
2344
+ const registryBefore = fileRegistry && existsSync8(resolved.registry_path) ? readFileSync10(resolved.registry_path, "utf8") : null;
2340
2345
  saveProjectRegistry(resolved.registry, resolved.registry_path);
2341
- const registryAfter = fileRegistry && existsSync8(resolved.registry_path) ? readFileSync9(resolved.registry_path, "utf8") : null;
2346
+ const registryAfter = fileRegistry && existsSync8(resolved.registry_path) ? readFileSync10(resolved.registry_path, "utf8") : null;
2342
2347
  if (registryBefore !== registryAfter) changed.push(resolved.registry_path);
2343
2348
  resolved.manifest = manifestNext;
2344
2349
  return changed;
@@ -2380,8 +2385,8 @@ function canonicalJson2(value) {
2380
2385
  return encoded;
2381
2386
  }
2382
2387
  if (Array.isArray(value)) return `[${value.map((item) => item === void 0 ? "null" : canonicalJson2(item)).join(",")}]`;
2383
- const record2 = value;
2384
- return `{${Object.keys(record2).filter((key) => record2[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(record2[key])}`).join(",")}}`;
2388
+ const record3 = value;
2389
+ return `{${Object.keys(record3).filter((key) => record3[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(record3[key])}`).join(",")}}`;
2385
2390
  }
2386
2391
  function sha256Hex(value) {
2387
2392
  return createHash5("sha256").update(value).digest("hex");
@@ -2649,7 +2654,7 @@ import {
2649
2654
  fchmodSync as fchmodSync3,
2650
2655
  fstatSync,
2651
2656
  fsyncSync as fsyncSync3,
2652
- lstatSync as lstatSync9,
2657
+ lstatSync as lstatSync10,
2653
2658
  mkdirSync as mkdirSync5,
2654
2659
  openSync as openSync3,
2655
2660
  readSync,
@@ -2659,26 +2664,26 @@ import {
2659
2664
  writeFileSync as writeFileSync6
2660
2665
  } from "node:fs";
2661
2666
  import { homedir as homedir4 } from "node:os";
2662
- import { basename as basename7, dirname as dirname12, join as join12, parse, relative as relative8, resolve as resolve11, sep as sep3 } from "node:path";
2667
+ import { basename as basename7, dirname as dirname12, join as join13, parse, relative as relative8, resolve as resolve12, sep as sep3 } from "node:path";
2663
2668
  function notebookStateRoot(env2 = process.env) {
2664
- const base = env2.XDG_STATE_HOME || join12(env2.HOME || homedir4(), ".local", "state");
2665
- return resolve11(base, "pjangler", "notebook", NOTEBOOK_STATE_VERSION);
2669
+ const base = env2.XDG_STATE_HOME || join13(env2.HOME || homedir4(), ".local", "state");
2670
+ return resolve12(base, "pjangler", "notebook", NOTEBOOK_STATE_VERSION);
2666
2671
  }
2667
2672
  function assertDigest(value, label) {
2668
2673
  if (!SESSION_KEY_RE.test(value)) throw new NotebookError("INVALID_INPUT", `Invalid ${label}`);
2669
2674
  }
2670
2675
  function projectStateDir(root, projectSlug) {
2671
2676
  if (!/^[a-z0-9][a-z0-9._-]{0,127}$/iu.test(projectSlug)) throw new NotebookError("INVALID_INPUT", "Invalid project slug for Notebook state");
2672
- return join12(resolve11(root), "projects", sha256Hex(`pjangler-project-state-v1\0${projectSlug}`));
2677
+ return join13(resolve12(root), "projects", sha256Hex(`pjangler-project-state-v1\0${projectSlug}`));
2673
2678
  }
2674
2679
  function assertContained(root, candidate) {
2675
- const rel = relative8(resolve11(root), resolve11(candidate));
2680
+ const rel = relative8(resolve12(root), resolve12(candidate));
2676
2681
  if (!rel || !rel.startsWith(`..${sep3}`) && rel !== ".." && !rel.startsWith(sep3)) return;
2677
2682
  throw new NotebookError("INTERNAL_ERROR", "Notebook state path escaped its root");
2678
2683
  }
2679
2684
  function openPinnedDirectory(path, root, create) {
2680
- const absolute = resolve11(path);
2681
- const absoluteRoot = resolve11(root);
2685
+ const absolute = resolve12(path);
2686
+ const absoluteRoot = resolve12(root);
2682
2687
  assertContained(absoluteRoot, absolute);
2683
2688
  if (!existsSync9("/proc/self/fd")) throw new NotebookError("INTERNAL_ERROR", "Descriptor-pinned Notebook state requires procfs");
2684
2689
  const parsed = parse(absolute);
@@ -2687,7 +2692,7 @@ function openPinnedDirectory(path, root, create) {
2687
2692
  try {
2688
2693
  for (const part of absolute.slice(parsed.root.length).split(sep3).filter(Boolean)) {
2689
2694
  const child = `/proc/self/fd/${fd}/${part}`;
2690
- cursor = join12(cursor, part);
2695
+ cursor = join13(cursor, part);
2691
2696
  let childFd;
2692
2697
  try {
2693
2698
  childFd = openSync3(child, DIRECTORY_OPEN_FLAGS);
@@ -2724,8 +2729,8 @@ function assertOwned(stat) {
2724
2729
  }
2725
2730
  }
2726
2731
  function ensureDirectory(path, root) {
2727
- const absoluteRoot = resolve11(root);
2728
- const absolute = resolve11(path);
2732
+ const absoluteRoot = resolve12(root);
2733
+ const absolute = resolve12(path);
2729
2734
  assertContained(absoluteRoot, absolute);
2730
2735
  const fd = openPinnedDirectory(absolute, absoluteRoot, true);
2731
2736
  try {
@@ -2739,23 +2744,23 @@ function ensureDirectory(path, root) {
2739
2744
  }
2740
2745
  }
2741
2746
  function notebookStatePaths(root, projectSlug) {
2742
- const absoluteRoot = resolve11(root);
2747
+ const absoluteRoot = resolve12(root);
2743
2748
  const project = projectStateDir(absoluteRoot, projectSlug);
2744
2749
  return {
2745
2750
  root: absoluteRoot,
2746
2751
  project,
2747
- baselines: join12(project, "baselines"),
2748
- claims: join12(project, "claims"),
2749
- receipts: join12(project, "receipts"),
2750
- refusals: join12(project, "refusals"),
2751
- journals: join12(project, "journals"),
2752
- locks: join12(project, "locks")
2752
+ baselines: join13(project, "baselines"),
2753
+ claims: join13(project, "claims"),
2754
+ receipts: join13(project, "receipts"),
2755
+ refusals: join13(project, "refusals"),
2756
+ journals: join13(project, "journals"),
2757
+ locks: join13(project, "locks")
2753
2758
  };
2754
2759
  }
2755
2760
  function ensureNotebookState(root, projectSlug) {
2756
2761
  const paths = notebookStatePaths(root, projectSlug);
2757
2762
  const { root: absoluteRoot, project } = paths;
2758
- for (const path of [absoluteRoot, join12(absoluteRoot, "projects"), project, paths.baselines, paths.claims, paths.receipts, paths.refusals, paths.journals, paths.locks]) {
2763
+ for (const path of [absoluteRoot, join13(absoluteRoot, "projects"), project, paths.baselines, paths.claims, paths.receipts, paths.refusals, paths.journals, paths.locks]) {
2759
2764
  ensureDirectory(path, absoluteRoot);
2760
2765
  }
2761
2766
  return paths;
@@ -2823,7 +2828,7 @@ function renameStateFile(source, target, root) {
2823
2828
  if (!stat.isFile() || (stat.mode & 511) !== 384) throw new NotebookError("CONFLICT", "Notebook state rename source has an integrity finding");
2824
2829
  assertOwned(stat);
2825
2830
  try {
2826
- lstatSync9(targetPath);
2831
+ lstatSync10(targetPath);
2827
2832
  throw new NotebookError("CONFLICT", "Notebook state rename target already exists");
2828
2833
  } catch (error) {
2829
2834
  if (error.code !== "ENOENT") throw error;
@@ -2868,7 +2873,7 @@ function atomicWriteJson(path, value, root, afterParentPinned) {
2868
2873
  closeSync3(fd);
2869
2874
  }
2870
2875
  try {
2871
- const current = lstatSync9(target);
2876
+ const current = lstatSync10(target);
2872
2877
  if (!existingIdentity || !current.isFile() || current.isSymbolicLink() || current.dev !== existingIdentity.dev || current.ino !== existingIdentity.ino) {
2873
2878
  throw new NotebookError("CONFLICT", "Notebook state target changed during atomic update; preserving both entries for audit");
2874
2879
  }
@@ -3103,13 +3108,13 @@ function baselineReceiptByteCeiling(limits) {
3103
3108
  function readSessionBaseline(root, projectSlug, sessionKey, limits) {
3104
3109
  assertDigest(sessionKey, "session key");
3105
3110
  const paths = ensureNotebookState(root, projectSlug);
3106
- return readBaseline(join12(paths.baselines, `${sessionKey}.json`), baselineReceiptByteCeiling(limits));
3111
+ return readBaseline(join13(paths.baselines, `${sessionKey}.json`), baselineReceiptByteCeiling(limits));
3107
3112
  }
3108
3113
  function acquireLock(paths, maxWaitMs) {
3109
- const lock = join12(paths.locks, "admission.lock");
3114
+ const lock = join13(paths.locks, "admission.lock");
3110
3115
  const deadline = Date.now() + Math.max(1, maxWaitMs);
3111
3116
  const token = randomUUID3();
3112
- const record2 = () => ({
3117
+ const record3 = () => ({
3113
3118
  schema_version: NOTEBOOK_SCHEMA_VERSION,
3114
3119
  token,
3115
3120
  pid: process.pid,
@@ -3117,7 +3122,7 @@ function acquireLock(paths, maxWaitMs) {
3117
3122
  expires_at: new Date(Date.now() + Math.max(5e3, maxWaitMs * 20)).toISOString()
3118
3123
  });
3119
3124
  while (true) {
3120
- if (exclusiveWrite(lock, jsonLine(record2()), paths.root)) {
3125
+ if (exclusiveWrite(lock, jsonLine(record3()), paths.root)) {
3121
3126
  return () => {
3122
3127
  const current = safeReadJson(lock, 8192);
3123
3128
  if (current.value && typeof current.value === "object" && !Array.isArray(current.value) && current.value.token === token) {
@@ -3136,7 +3141,7 @@ function acquireLock(paths, maxWaitMs) {
3136
3141
  throw new NotebookError("CONFLICT", "Notebook state lock has an integrity finding; preserve it and run pj notebook audit --json");
3137
3142
  }
3138
3143
  if (Date.now() >= expiresAt) {
3139
- const recovery = join12(paths.locks, `recovery-${heldToken}.lock`);
3144
+ const recovery = join13(paths.locks, `recovery-${heldToken}.lock`);
3140
3145
  const recoveryRecord = jsonLine({ schema_version: NOTEBOOK_SCHEMA_VERSION, stale_token: heldToken, recovery_token: token, expires_at: new Date(Date.now() + 5e3).toISOString() });
3141
3146
  if (exclusiveWrite(recovery, recoveryRecord, paths.root)) {
3142
3147
  try {
@@ -3144,7 +3149,7 @@ function acquireLock(paths, maxWaitMs) {
3144
3149
  const currentToken = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? verify.value.token : null;
3145
3150
  const currentExpiry = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? Date.parse(String(verify.value.expires_at ?? "")) : Number.NaN;
3146
3151
  if (currentToken === heldToken && Number.isFinite(currentExpiry) && Date.now() >= currentExpiry) {
3147
- const recovered = join12(paths.locks, `.recovered-${heldToken}-${randomUUID3()}.json`);
3152
+ const recovered = join13(paths.locks, `.recovered-${heldToken}-${randomUUID3()}.json`);
3148
3153
  renameStateFile(lock, recovered, paths.root);
3149
3154
  unlinkStateFile(recovered, paths.root);
3150
3155
  }
@@ -3224,7 +3229,7 @@ function scanAuxiliaryState(paths, limits) {
3224
3229
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
3225
3230
  continue;
3226
3231
  }
3227
- const read = safeReadJson(join12(specification.dir, entry.name), specification.maxBytes);
3232
+ const read = safeReadJson(join13(specification.dir, entry.name), specification.maxBytes);
3228
3233
  if (read.reason || read.value === void 0) {
3229
3234
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
3230
3235
  continue;
@@ -3249,7 +3254,7 @@ function scanAuxiliaryState(paths, limits) {
3249
3254
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
3250
3255
  continue;
3251
3256
  }
3252
- const read = safeReadJson(join12(paths.journals, entry.name), limits.receipt_max_bytes);
3257
+ const read = safeReadJson(join13(paths.journals, entry.name), limits.receipt_max_bytes);
3253
3258
  if (read.reason || read.value === void 0) {
3254
3259
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
3255
3260
  continue;
@@ -3278,7 +3283,7 @@ function scanAuxiliaryState(paths, limits) {
3278
3283
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
3279
3284
  continue;
3280
3285
  }
3281
- const read = safeReadJson(join12(paths.locks, entry.name), 8192);
3286
+ const read = safeReadJson(join13(paths.locks, entry.name), 8192);
3282
3287
  if (read.reason || read.value === void 0) {
3283
3288
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
3284
3289
  continue;
@@ -3317,7 +3322,7 @@ function scanReceipts(paths, limits) {
3317
3322
  }
3318
3323
  for (const entry of entries) {
3319
3324
  const entryId = safeEntryId("receipts", entry.name);
3320
- const path = join12(paths.receipts, entry.name);
3325
+ const path = join13(paths.receipts, entry.name);
3321
3326
  if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9]{64}\.json$/u.test(entry.name)) {
3322
3327
  addIntegrity({ entry_id: entryId, reason: "non-regular" });
3323
3328
  continue;
@@ -3347,7 +3352,7 @@ function scanBaselines(paths, nowMs, limits, referenced) {
3347
3352
  if (!existsSync9(paths.baselines)) return { current, stale };
3348
3353
  for (const entry of readStateDirectory(paths.baselines, paths.root)) {
3349
3354
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3350
- const baseline = readBaseline(join12(paths.baselines, entry.name), baselineReceiptByteCeiling(limits));
3355
+ const baseline = readBaseline(join13(paths.baselines, entry.name), baselineReceiptByteCeiling(limits));
3351
3356
  if (!baseline || referenced.has(baseline.session_key)) continue;
3352
3357
  const expires = Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3;
3353
3358
  if (nowMs >= expires) stale += 1;
@@ -3361,7 +3366,7 @@ function scanRefusals(paths, nowMs, limits, scan) {
3361
3366
  const entries = readStateDirectory(paths.refusals, paths.root).sort((a, b) => a.name.localeCompare(b.name, "en")).slice(0, limits.refusal_max_entries);
3362
3367
  for (const entry of entries) {
3363
3368
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3364
- const read = safeReadJson(join12(paths.refusals, entry.name), limits.receipt_max_bytes);
3369
+ const read = safeReadJson(join13(paths.refusals, entry.name), limits.receipt_max_bytes);
3365
3370
  const marker = read.value === void 0 ? null : parseRefusal(read.value);
3366
3371
  if (!marker || `${marker.session_key}.json` !== entry.name) continue;
3367
3372
  if (nowMs >= Date.parse(marker.baseline_created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
@@ -3444,7 +3449,7 @@ function listCaptureReceipts(root, projectSlug, limits, state) {
3444
3449
  function readCaptureReceipt(root, projectSlug, receiptId, limits) {
3445
3450
  if (!RECEIPT_ID_RE.test(receiptId)) throw new NotebookError("INVALID_INPUT", "Invalid receipt ID");
3446
3451
  const paths = notebookStatePaths(root, projectSlug);
3447
- const path = join12(paths.receipts, `${receiptId}.json`);
3452
+ const path = join13(paths.receipts, `${receiptId}.json`);
3448
3453
  if (!existsSync9(path)) throw new NotebookError("NOT_FOUND", `Capture receipt not found: ${receiptId}`);
3449
3454
  const read = safeReadJson(path, limits.receipt_max_bytes);
3450
3455
  const receipt = read.value === void 0 ? null : parseReceipt(read.value);
@@ -3457,7 +3462,7 @@ function writeReceipt(paths, receipt, limit) {
3457
3462
  const bytes = Buffer.byteLength(text, "utf8");
3458
3463
  if (bytes > limit) throw new NotebookError("CONFLICT", "Receipt transition exceeds its per-receipt ceiling");
3459
3464
  if (receipt.serialized_bytes === bytes) {
3460
- atomicWriteJson(join12(paths.receipts, `${receipt.receipt_id}.json`), receipt, paths.root);
3465
+ atomicWriteJson(join13(paths.receipts, `${receipt.receipt_id}.json`), receipt, paths.root);
3461
3466
  return receipt;
3462
3467
  }
3463
3468
  receipt.serialized_bytes = bytes;
@@ -3596,7 +3601,7 @@ function pruneNotebookState(root, projectSlug, limits, now = /* @__PURE__ */ new
3596
3601
  const successCutoff = now.getTime() - limits.receipt_succeeded_retention_days * 864e5;
3597
3602
  for (const receipt of scan.receipts) {
3598
3603
  if (receipt.state !== "succeeded" || Date.parse(receipt.updated_at) > successCutoff) continue;
3599
- const path = join12(paths.receipts, `${receipt.receipt_id}.json`);
3604
+ const path = join13(paths.receipts, `${receipt.receipt_id}.json`);
3600
3605
  unlinkStateFile(path, paths.root);
3601
3606
  removed.push(safeEntryId("receipts", basename7(path)));
3602
3607
  }
@@ -3605,15 +3610,15 @@ function pruneNotebookState(root, projectSlug, limits, now = /* @__PURE__ */ new
3605
3610
  const referenced = /* @__PURE__ */ new Set([...remaining.referencedSessions, ...remainingAuxiliary.referencedSessions]);
3606
3611
  for (const entry of readStateDirectory(paths.baselines, paths.root)) {
3607
3612
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3608
- const path = join12(paths.baselines, entry.name);
3613
+ const path = join13(paths.baselines, entry.name);
3609
3614
  const baseline = readBaseline(path, baselineReceiptByteCeiling(limits));
3610
3615
  if (!baseline || referenced.has(baseline.session_key)) continue;
3611
3616
  if (now.getTime() < Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
3612
3617
  unlinkStateFile(path, paths.root);
3613
3618
  removed.push(safeEntryId("baselines", entry.name));
3614
- const claim = join12(paths.claims, `${baseline.session_key}.overview`);
3619
+ const claim = join13(paths.claims, `${baseline.session_key}.overview`);
3615
3620
  if (unlinkStateFile(claim, paths.root, true)) removed.push(safeEntryId("claims", basename7(claim)));
3616
- const refusal = join12(paths.refusals, `${baseline.session_key}.json`);
3621
+ const refusal = join13(paths.refusals, `${baseline.session_key}.json`);
3617
3622
  if (unlinkStateFile(refusal, paths.root, true)) removed.push(safeEntryId("refusals", basename7(refusal)));
3618
3623
  }
3619
3624
  fsyncDirectory2(paths.project);
@@ -3998,11 +4003,11 @@ var init_open_notebook_client = __esm({
3998
4003
  });
3999
4004
 
4000
4005
  // src/notebook/git-evidence.ts
4001
- import { spawnSync as spawnSync6 } from "node:child_process";
4002
- import { closeSync as closeSync4, constants as constants3, fstatSync as fstatSync2, lstatSync as lstatSync10, openSync as openSync4, readSync as readSync2, realpathSync as realpathSync7 } from "node:fs";
4003
- import { extname, join as join14, relative as relative9, resolve as resolve12, sep as sep4 } from "node:path";
4006
+ import { spawnSync as spawnSync7 } from "node:child_process";
4007
+ import { closeSync as closeSync4, constants as constants3, fstatSync as fstatSync2, lstatSync as lstatSync11, openSync as openSync4, readSync as readSync2, realpathSync as realpathSync7 } from "node:fs";
4008
+ import { extname, join as join15, relative as relative9, resolve as resolve13, sep as sep4 } from "node:path";
4004
4009
  function git(repo, args, maxBuffer = 4 * 1024 * 1024, timeout = 5e3) {
4005
- const result2 = spawnSync6("git", args, { cwd: repo, encoding: "utf8", maxBuffer, timeout, shell: false });
4010
+ const result2 = spawnSync7("git", args, { cwd: repo, encoding: "utf8", maxBuffer, timeout, shell: false });
4006
4011
  return { ok: result2.status === 0, stdout: result2.stdout ?? "" };
4007
4012
  }
4008
4013
  function statusPaths(value) {
@@ -4069,7 +4074,7 @@ function validateCommittedGitRef(repoPath, gitRef) {
4069
4074
  function safeRelative(repoPath, relativePath2) {
4070
4075
  if (!relativePath2 || relativePath2.includes("\0") || relativePath2.startsWith("/") || relativePath2.split(/[\\/]/u).includes("..")) return null;
4071
4076
  const root = realpathSync7(repoPath);
4072
- const candidate = resolve12(root, relativePath2);
4077
+ const candidate = resolve13(root, relativePath2);
4073
4078
  const rel = relative9(root, candidate);
4074
4079
  if (rel === ".." || rel.startsWith(`..${sep4}`) || rel.startsWith(sep4)) return null;
4075
4080
  return { root, candidate };
@@ -4078,9 +4083,9 @@ function hasSymlinkComponent(root, candidate) {
4078
4083
  const rel = relative9(root, candidate);
4079
4084
  let cursor = root;
4080
4085
  for (const part of rel.split(sep4).filter(Boolean)) {
4081
- cursor = join14(cursor, part);
4086
+ cursor = join15(cursor, part);
4082
4087
  try {
4083
- if (lstatSync10(cursor).isSymbolicLink()) return true;
4088
+ if (lstatSync11(cursor).isSymbolicLink()) return true;
4084
4089
  } catch {
4085
4090
  return true;
4086
4091
  }
@@ -4147,8 +4152,8 @@ function matchesSimpleGlob(path, glob) {
4147
4152
  if (glob.startsWith("**/*.")) return extname(path).toLowerCase() === `.${glob.slice(5).toLowerCase()}`;
4148
4153
  return path === glob;
4149
4154
  }
4150
- function addExclusion(record2, reason) {
4151
- record2[reason] = (record2[reason] ?? 0) + 1;
4155
+ function addExclusion(record3, reason) {
4156
+ record3[reason] = (record3[reason] ?? 0) + 1;
4152
4157
  }
4153
4158
  function selectEligibleDocuments(config, baseline, manualBaselineRef) {
4154
4159
  const repo = config.repo_path;
@@ -4221,10 +4226,10 @@ var init_git_evidence = __esm({
4221
4226
 
4222
4227
  // src/notebook/remote-mutation-journal.ts
4223
4228
  import { randomUUID as randomUUID4 } from "node:crypto";
4224
- import { join as join15 } from "node:path";
4229
+ import { join as join16 } from "node:path";
4225
4230
  function journalPath(root, projectSlug, operationId) {
4226
4231
  if (!/^[a-f0-9-]{16,64}$/iu.test(operationId)) throw new NotebookError("INVALID_INPUT", "Invalid remote mutation operation ID");
4227
- return join15(ensureNotebookState(root, projectSlug).journals, `${operationId}.json`);
4232
+ return join16(ensureNotebookState(root, projectSlug).journals, `${operationId}.json`);
4228
4233
  }
4229
4234
  function remoteMutationJournalPath(root, projectSlug, operationId) {
4230
4235
  return journalPath(root, projectSlug, operationId);
@@ -4244,7 +4249,7 @@ function listRemoteMutationJournals(root, projectSlug) {
4244
4249
  if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9-]{16,64}\.json$/iu.test(entry.name)) {
4245
4250
  throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
4246
4251
  }
4247
- const read = readNotebookStateJson(join15(paths.journals, entry.name), paths.root, 65536);
4252
+ const read = readNotebookStateJson(join16(paths.journals, entry.name), paths.root, 65536);
4248
4253
  const journal = read.value === void 0 ? null : parseRemoteMutationJournal(read.value);
4249
4254
  if (!journal || `${journal.operation_id}.json` !== entry.name) {
4250
4255
  throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
@@ -4599,15 +4604,15 @@ var init_reconcile = __esm({
4599
4604
  // src/notebook/entropy.ts
4600
4605
  import { existsSync as existsSync12 } from "node:fs";
4601
4606
  import { createHash as createHash9 } from "node:crypto";
4602
- import { resolve as resolve15 } from "node:path";
4603
- import { spawnSync as spawnSync9 } from "node:child_process";
4607
+ import { resolve as resolve16 } from "node:path";
4608
+ import { spawnSync as spawnSync10 } from "node:child_process";
4604
4609
  function listAllTrackedDocuments(config) {
4605
4610
  const repo = config.repo_path;
4606
4611
  const timeout = config.limits.overall_timeout_ms;
4607
- const trackedResult = spawnSync9("git", ["ls-files", "-z"], { cwd: repo, encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout, shell: false });
4612
+ const trackedResult = spawnSync10("git", ["ls-files", "-z"], { cwd: repo, encoding: "utf8", maxBuffer: 4 * 1024 * 1024, timeout, shell: false });
4608
4613
  if (trackedResult.status !== 0) return [];
4609
4614
  const tracked = trackedResult.stdout.split("\0").filter(Boolean);
4610
- const headRev = spawnSync9("git", ["rev-parse", "HEAD"], { cwd: repo, encoding: "utf8", timeout, shell: false }).stdout?.trim() ?? "HEAD";
4615
+ const headRev = spawnSync10("git", ["rev-parse", "HEAD"], { cwd: repo, encoding: "utf8", timeout, shell: false }).stdout?.trim() ?? "HEAD";
4611
4616
  const documents = [];
4612
4617
  for (const path of tracked) {
4613
4618
  if (!config.policy.documentation_globs.some((glob) => matchesSimpleGlob(path, glob))) continue;
@@ -4634,7 +4639,7 @@ async function pruneZombieNotes(input) {
4634
4639
  const parsed = parseNoteEnvelope(note.content);
4635
4640
  if (!parsed || parsed.envelope.kind !== "document" || parsed.envelope.project_slug !== input.projectSlug) continue;
4636
4641
  const sourcePath = parsed.envelope.source_path;
4637
- if (!sourcePath || !existsSync12(resolve15(input.repoPath, sourcePath))) {
4642
+ if (!sourcePath || !existsSync12(resolve16(input.repoPath, sourcePath))) {
4638
4643
  if (typeof input.client.deleteOwnedNote === "function") {
4639
4644
  await input.client.deleteOwnedNote(input.notebookId, note.id);
4640
4645
  } else if (typeof input.client.deleteNote === "function") {
@@ -4794,8 +4799,8 @@ var init_entropy = __esm({
4794
4799
  });
4795
4800
 
4796
4801
  // src/notebook/summarizer.ts
4797
- import { spawnSync as spawnSync10 } from "node:child_process";
4798
- import { isAbsolute as isAbsolute7 } from "node:path";
4802
+ import { spawnSync as spawnSync11 } from "node:child_process";
4803
+ import { isAbsolute as isAbsolute8 } from "node:path";
4799
4804
  function evidenceItems(evidence) {
4800
4805
  const items = [];
4801
4806
  const eligible = new Set(evidence.documents.map((item) => item.path));
@@ -4868,11 +4873,11 @@ function words(value) {
4868
4873
  }
4869
4874
  function validateClaims(value, items, noteMaxBytes) {
4870
4875
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
4871
- const record2 = value;
4872
- if (record2.schema_version !== 1 || !Array.isArray(record2.claims) || record2.claims.length < 1 || record2.claims.length > 50 || Object.keys(record2).some((key) => key !== "schema_version" && key !== "claims")) return null;
4876
+ const record3 = value;
4877
+ if (record3.schema_version !== 1 || !Array.isArray(record3.claims) || record3.claims.length < 1 || record3.claims.length > 50 || Object.keys(record3).some((key) => key !== "schema_version" && key !== "claims")) return null;
4873
4878
  const byId = new Map(items.map((item) => [item.evidence_id, item]));
4874
4879
  const rendered = ["## Session outcome"];
4875
- for (const claimValue of record2.claims) {
4880
+ for (const claimValue of record3.claims) {
4876
4881
  if (!claimValue || typeof claimValue !== "object" || Array.isArray(claimValue)) return null;
4877
4882
  const claim = claimValue;
4878
4883
  if (Object.keys(claim).some((key) => key !== "text" && key !== "evidence_ids") || typeof claim.text !== "string" || !claim.text.trim() || Buffer.byteLength(claim.text, "utf8") > 1024 || !Array.isArray(claim.evidence_ids) || claim.evidence_ids.length < 1 || claim.evidence_ids.length > 10 || claim.evidence_ids.some((id) => typeof id !== "string" || !byId.has(id))) return null;
@@ -4893,7 +4898,7 @@ function summarizeCapture(config, evidence) {
4893
4898
  const items = evidenceItems(evidence);
4894
4899
  const payload = JSON.stringify({ schema_version: 1, evidence: items });
4895
4900
  if (Buffer.byteLength(payload, "utf8") > config.limits.request_max_bytes) return { schema_version: 1, mode: "deterministic-fallback", summary: fallbackSummary };
4896
- const result2 = spawnSync10(config.summarizer.executable, config.summarizer.args, {
4901
+ const result2 = spawnSync11(config.summarizer.executable, config.summarizer.args, {
4897
4902
  cwd: config.repo_path,
4898
4903
  input: Buffer.from(payload, "utf8"),
4899
4904
  timeout: config.limits.overall_timeout_ms,
@@ -4911,7 +4916,7 @@ function summarizeCapture(config, evidence) {
4911
4916
  }
4912
4917
  }
4913
4918
  function assertSummarizerConfig(config) {
4914
- if (config.summarizer && (!isAbsolute7(config.summarizer.executable) || config.summarizer.executable.includes("\0") || config.summarizer.args.length > 32 || config.summarizer.args.some((arg) => arg.includes("\0") || Buffer.byteLength(arg, "utf8") > 1024))) {
4919
+ if (config.summarizer && (!isAbsolute8(config.summarizer.executable) || config.summarizer.executable.includes("\0") || config.summarizer.args.length > 32 || config.summarizer.args.some((arg) => arg.includes("\0") || Buffer.byteLength(arg, "utf8") > 1024))) {
4915
4920
  throw new NotebookError("NOT_CONFIGURED", "Configured Notebook summarizer command is invalid");
4916
4921
  }
4917
4922
  }
@@ -5221,7 +5226,7 @@ var init_capture = __esm({
5221
5226
 
5222
5227
  // src/mcp-server.ts
5223
5228
  import { existsSync as existsSync18, statSync as statSync7 } from "node:fs";
5224
- import { dirname as dirname17, join as join26, resolve as resolve21 } from "node:path";
5229
+ import { dirname as dirname17, join as join27, resolve as resolve22 } from "node:path";
5225
5230
  import { fileURLToPath as fileURLToPath6 } from "node:url";
5226
5231
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5227
5232
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -5332,12 +5337,12 @@ var Recipe = class {
5332
5337
  };
5333
5338
  }
5334
5339
  /** Initialize missing/drifted state using only this recipe's owned checks. */
5335
- async initializeOwnedChecks(ctx) {
5340
+ async initializeOwnedChecks(ctx, checks = this.checks) {
5336
5341
  const phases = [];
5337
5342
  const logs = [];
5338
5343
  const errors = [];
5339
5344
  const changedFiles = [];
5340
- for (const check of this.checks) {
5345
+ for (const check of checks) {
5341
5346
  const finding2 = await check.audit(ctx);
5342
5347
  if (finding2.status === "pass" || finding2.status === "skip") {
5343
5348
  phases.push({ id: check.id, status: finding2.status === "skip" ? "skipped" : "unchanged", changedFiles: [], message: finding2.summary });
@@ -5419,10 +5424,233 @@ var Recipe = class {
5419
5424
 
5420
5425
  // src/parity/rules.ts
5421
5426
  init_registryClient();
5427
+
5428
+ // src/bmad/companion.ts
5429
+ init_tree_diff();
5430
+ import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2 } from "node:fs";
5431
+ import { spawnSync as spawnSync2 } from "node:child_process";
5432
+ import { isAbsolute, join as join3, resolve as resolve3 } from "node:path";
5433
+ import { pathToFileURL } from "node:url";
5434
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
5435
+ function optionalObject(parent, key, label) {
5436
+ if (!Object.hasOwn(parent, key)) return {};
5437
+ const value = parent[key];
5438
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object when present`);
5439
+ return value;
5440
+ }
5441
+ function select(projectRoot) {
5442
+ const path = join3(projectRoot, ".project.json");
5443
+ const manifest = existsSync2(path) ? record(JSON.parse(readFileSync2(path, "utf8"))) : {};
5444
+ const bmad = optionalObject(manifest, "bmad", "bmad");
5445
+ const config = optionalObject(bmad, "companion", "bmad.companion");
5446
+ const adapter = optionalObject(config, "adapter", "bmad.companion.adapter");
5447
+ const options = optionalObject(adapter, "options", "bmad.companion.adapter.options");
5448
+ if (config.enabled === false) return { enabled: false, reason: "g33 explicitly disabled in .project.json", options: {} };
5449
+ if (config.enabled !== void 0 && config.enabled !== true) throw new Error("bmad.companion.enabled must be boolean");
5450
+ const binding = record(manifest.ticket_provider);
5451
+ const registered = typeof manifest.project_id === "string" && /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(manifest.project_id) && manifest.project_id.length <= 128;
5452
+ const eligible = registered && binding.type === "plane" && binding.state === "linked" && typeof binding.workspace === "string" && binding.workspace.trim().toLowerCase() === "33god" && typeof binding.board_id === "string" && binding.board_id.trim() !== "";
5453
+ if (!eligible && config.enabled !== true) return { enabled: false, reason: "g33 defaults off without a registered linked 33god project or explicit opt-in", options: {} };
5454
+ const module = adapter.module ?? (process.env.PJ_G33_ADAPTER_MODULE?.trim() || void 0);
5455
+ const exportName = adapter.exportName ?? (process.env.PJ_G33_ADAPTER_EXPORT?.trim() || void 0);
5456
+ if (module !== void 0 && (typeof module !== "string" || !module.trim())) throw new Error("g33 adapter.module must be a non-empty package specifier or source path");
5457
+ if (exportName !== void 0 && (typeof exportName !== "string" || !exportName.trim())) throw new Error("g33 adapter.exportName must name the supported callable export");
5458
+ return {
5459
+ enabled: true,
5460
+ reason: eligible ? "Registered linked 33god project" : "Explicit g33 opt-in",
5461
+ module,
5462
+ exportName,
5463
+ options
5464
+ };
5465
+ }
5466
+ function g33CompanionEnabled(projectRoot) {
5467
+ return select(projectRoot).enabled;
5468
+ }
5469
+ function resolveAdapter(projectRoot, module) {
5470
+ if (module.startsWith(".") || isAbsolute(module)) {
5471
+ const path = resolve3(projectRoot, module);
5472
+ if (!existsSync2(path)) throw new Error(`Configured g33 source is unavailable: ${module}`);
5473
+ return pathToFileURL(path).href;
5474
+ }
5475
+ if (module.includes(":") || module.startsWith("/")) throw new Error("Use a local source path or installed package specifier for g33");
5476
+ const resolved = spawnSync2(process.execPath, [
5477
+ "--experimental-import-meta-resolve",
5478
+ "--input-type=module",
5479
+ "--eval",
5480
+ "process.stdout.write(import.meta.resolve(process.argv[1], process.argv[2]))",
5481
+ "--",
5482
+ module,
5483
+ pathToFileURL(join3(projectRoot, ".project.json")).href
5484
+ ], { encoding: "utf8", timeout: 1e4 });
5485
+ if (resolved.status === 0 && resolved.stdout.trim()) return resolved.stdout.trim();
5486
+ const projectError = resolved.stderr.trim() || resolved.error?.message || `resolver exit ${resolved.status}`;
5487
+ try {
5488
+ return import.meta.resolve(module);
5489
+ } catch (error) {
5490
+ throw new Error(`Project ESM resolution failed: ${projectError}
5491
+ PJangler ESM resolution failed: ${error instanceof Error ? error.message : String(error)}`);
5492
+ }
5493
+ }
5494
+ function reply(value, operation) {
5495
+ const data = record(value);
5496
+ const statuses = operation === "observe" ? ["installed", "missing", "unavailable", "conflict", "error"] : operation === "plan" ? ["planned", "unchanged", "unavailable", "conflict", "error"] : ["changed", "unchanged", "unavailable", "conflict", "error"];
5497
+ if (data.schemaVersion !== 1 || !statuses.includes(String(data.status)) || typeof data.summary !== "string" || ![data.details, data.evidence].every((field2) => Array.isArray(field2) && field2.every((item) => typeof item === "string"))) {
5498
+ throw new Error(`Configured g33 adapter returned an invalid v1 ${operation} result`);
5499
+ }
5500
+ if (data.status === "installed" && !data.evidence.some((item) => item.trim())) {
5501
+ throw new Error("g33 installed observation requires owner content/mapping evidence; metadata alone is insufficient");
5502
+ }
5503
+ return {
5504
+ schemaVersion: 1,
5505
+ status: data.status,
5506
+ summary: data.summary,
5507
+ details: [...data.details],
5508
+ evidence: [...data.evidence]
5509
+ };
5510
+ }
5511
+ async function runG33Companion(projectRoot, dryRun, reason = "recipe") {
5512
+ projectRoot = resolve3(projectRoot);
5513
+ const result2 = { schemaVersion: 1, moduleId: "g33", optional: true, status: "unavailable", summary: "g33 companion unavailable", details: [], evidence: [], changedFiles: [] };
5514
+ let before;
5515
+ try {
5516
+ const selection = select(projectRoot);
5517
+ if (!selection.enabled) return { ...result2, status: "disabled", summary: selection.reason };
5518
+ result2.details.push(selection.reason);
5519
+ if (!selection.module || !selection.exportName) return {
5520
+ ...result2,
5521
+ summary: "Optional g33 installer is not configured",
5522
+ details: [...result2.details, "Configure bmad.companion.adapter.module and exportName (or PJ_G33_ADAPTER_MODULE/PJ_G33_ADAPTER_EXPORT) to the owner's supported callable adapter. No g33 installation was attempted."]
5523
+ };
5524
+ result2.adapter = { module: selection.module, exportName: selection.exportName };
5525
+ let specifier;
5526
+ try {
5527
+ specifier = resolveAdapter(projectRoot, selection.module);
5528
+ } catch (error) {
5529
+ return { ...result2, status: "unavailable", summary: "Configured g33 adapter is unavailable", details: [...result2.details, error instanceof Error ? error.message : String(error)] };
5530
+ }
5531
+ before = snapshotTree(projectRoot);
5532
+ const exported = await import(specifier);
5533
+ if (changedTreePaths(projectRoot, before, snapshotTree(projectRoot)).length) {
5534
+ throw new Error("g33 adapter violated the no-write import contract");
5535
+ }
5536
+ const callable = exported[selection.exportName];
5537
+ if (typeof callable !== "function") throw new Error(`Configured g33 export ${selection.exportName} is not callable`);
5538
+ const invoke = async (operation) => {
5539
+ const value = reply(await callable({ schemaVersion: 1, moduleId: "g33", operation, projectRoot, reason, options: selection.options }), operation);
5540
+ if (operation !== "apply" && changedTreePaths(projectRoot, before, snapshotTree(projectRoot)).length) {
5541
+ throw new Error(`g33 adapter violated the no-write ${operation} contract`);
5542
+ }
5543
+ return value;
5544
+ };
5545
+ const observed = await invoke("observe");
5546
+ if (["unavailable", "conflict", "error"].includes(observed.status)) return { ...result2, ...observed };
5547
+ const plan = await invoke("plan");
5548
+ if (["unavailable", "conflict", "error"].includes(plan.status)) return { ...result2, ...plan };
5549
+ if (plan.status === "unchanged" && observed.status !== "installed") throw new Error("g33 plan claims convergence but owner observation does not prove installation");
5550
+ if (dryRun) return { ...result2, ...plan, evidence: observed.evidence };
5551
+ if (plan.status === "unchanged" && observed.status === "installed") return { ...result2, ...observed, status: "unchanged" };
5552
+ const applied = await invoke("apply");
5553
+ result2.changedFiles = changedTreePaths(projectRoot, before, snapshotTree(projectRoot));
5554
+ if (["unavailable", "conflict", "error"].includes(applied.status)) return { ...result2, ...applied };
5555
+ before = snapshotTree(projectRoot);
5556
+ const verified = await invoke("observe");
5557
+ if (verified.status !== "installed") return {
5558
+ ...result2,
5559
+ status: "error",
5560
+ summary: "g33 apply did not establish an installed owner observation",
5561
+ details: [verified.summary, ...verified.details],
5562
+ evidence: verified.evidence
5563
+ };
5564
+ return { ...result2, ...verified, status: result2.changedFiles.length ? "changed" : "unchanged" };
5565
+ } catch (error) {
5566
+ if (before) {
5567
+ try {
5568
+ result2.changedFiles = [.../* @__PURE__ */ new Set([...result2.changedFiles, ...changedTreePaths(projectRoot, before, snapshotTree(projectRoot))])].sort();
5569
+ } catch (snapshotError) {
5570
+ result2.details.push(`Could not inventory partial g33 writes: ${String(snapshotError)}`);
5571
+ }
5572
+ }
5573
+ return { ...result2, status: "error", summary: "Optional g33 companion could not be reconciled", details: [...result2.details, error instanceof Error ? error.message : String(error)] };
5574
+ }
5575
+ }
5576
+ function localEntryPresent(path) {
5577
+ try {
5578
+ lstatSync2(path);
5579
+ return true;
5580
+ } catch (error) {
5581
+ if (error.code === "ENOENT") return false;
5582
+ throw error;
5583
+ }
5584
+ }
5585
+ function g33BmadUpdateBoundary(projectRoot, retainedG33Declaration = false) {
5586
+ const result2 = {
5587
+ schemaVersion: 1,
5588
+ moduleId: "g33",
5589
+ optional: true,
5590
+ status: "conflict",
5591
+ summary: "BMAD update withheld pending the g33 owner pre-update preservation contract",
5592
+ details: [
5593
+ "No installer, retired-state eviction, or companion import/apply was attempted. Existing native BMAD bytes, customizations, and module mappings are retained.",
5594
+ "The owner must provide a supported pre-update capture/protection and post-update restore/verification handoff, including failure recovery. The configured v1 callable alone does not authorize a destructive BMAD update."
5595
+ ],
5596
+ evidence: [],
5597
+ changedFiles: []
5598
+ };
5599
+ try {
5600
+ const retainedG33 = retainedG33Declaration || localEntryPresent(join3(projectRoot, "_bmad", "g33"));
5601
+ const installedBmad = retainedG33 || localEntryPresent(join3(projectRoot, "_bmad", "_config", "manifest.yaml")) || localEntryPresent(join3(projectRoot, "_bmad", "core"));
5602
+ if (!installedBmad) return void 0;
5603
+ const selection = select(projectRoot);
5604
+ if (!selection.enabled && !retainedG33) return void 0;
5605
+ if (selection.module && selection.exportName) result2.adapter = { module: selection.module, exportName: selection.exportName };
5606
+ result2.details.unshift(retainedG33 ? "Retained g33 tree or module declaration requires preservation, including after opt-out." : selection.reason);
5607
+ return result2;
5608
+ } catch (error) {
5609
+ return {
5610
+ ...result2,
5611
+ status: "error",
5612
+ summary: "BMAD update withheld: companion configuration or preservation inspection failed",
5613
+ details: [...result2.details, error instanceof Error ? error.message : String(error)]
5614
+ };
5615
+ }
5616
+ }
5617
+ function companionDetails(result2) {
5618
+ return [`g33 (${result2.status}): ${result2.summary}`, ...result2.details, ...result2.evidence];
5619
+ }
5620
+ function createG33CompanionCheck() {
5621
+ return {
5622
+ id: "bmad.g33-companion",
5623
+ title: "Optional g33 companion",
5624
+ audit: async (ctx) => {
5625
+ const result2 = await runG33Companion(ctx.repoRoot, true, "audit");
5626
+ return {
5627
+ id: "bmad.g33-companion",
5628
+ title: "Optional g33 companion",
5629
+ status: result2.status === "disabled" ? "skip" : result2.status === "unchanged" && result2.evidence.length ? "pass" : "warn",
5630
+ summary: result2.summary,
5631
+ details: companionDetails(result2),
5632
+ fixable: result2.status === "planned"
5633
+ };
5634
+ },
5635
+ migrate: async (ctx, finding2) => {
5636
+ const result2 = await runG33Companion(ctx.repoRoot, ctx.dryRun, "repair");
5637
+ return {
5638
+ id: finding2.id,
5639
+ title: finding2.title,
5640
+ status: result2.status === "changed" || result2.status === "planned" ? "applied" : result2.status === "unchanged" ? "noop" : result2.status === "disabled" ? "skipped" : "partial",
5641
+ summary: result2.summary,
5642
+ details: companionDetails(result2),
5643
+ changedFiles: result2.changedFiles
5644
+ };
5645
+ }
5646
+ };
5647
+ }
5648
+
5649
+ // src/parity/rules.ts
5422
5650
  init_style();
5423
- import { existsSync as existsSync3, lstatSync as lstatSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readlinkSync as readlinkSync3, readdirSync as readdirSync3, realpathSync as realpathSync3, renameSync as renameSync2, symlinkSync as symlinkSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3, chmodSync, copyFileSync, rmSync as rmSync2 } from "node:fs";
5424
- import { basename as basename3, dirname as dirname5, join as join5, relative as relative4, resolve as resolve5 } from "node:path";
5425
- import { spawnSync as spawnSync3 } from "node:child_process";
5651
+ import { existsSync as existsSync4, lstatSync as lstatSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readlinkSync as readlinkSync4, readdirSync as readdirSync4, realpathSync as realpathSync3, renameSync as renameSync2, symlinkSync as symlinkSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3, chmodSync, copyFileSync, rmSync as rmSync2 } from "node:fs";
5652
+ import { basename as basename3, dirname as dirname5, join as join7, relative as relative5, resolve as resolve6 } from "node:path";
5653
+ import { spawnSync as spawnSync4 } from "node:child_process";
5426
5654
  import YAML2 from "yaml";
5427
5655
  import { parse as parseToml2 } from "smol-toml";
5428
5656
 
@@ -5441,8 +5669,8 @@ var SUPPORTED_BMAD_TOOLS = SUPPORTED_CLIS.map((cli) => cli.bmadTool);
5441
5669
  var SUPPORTED_CLI_ROOTS = SUPPORTED_CLIS.map((cli) => cli.projectRoot);
5442
5670
 
5443
5671
  // src/parity/skills.ts
5444
- import { lstatSync as lstatSync3, readlinkSync as readlinkSync2, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
5445
- import { basename as basename2, dirname as dirname4, join as join4, relative as relative3, resolve as resolve4 } from "node:path";
5672
+ import { lstatSync as lstatSync5, readlinkSync as readlinkSync3, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
5673
+ import { basename as basename2, dirname as dirname4, join as join6, relative as relative4, resolve as resolve5 } from "node:path";
5446
5674
  import {
5447
5675
  initScope,
5448
5676
  inspectStatus,
@@ -5453,15 +5681,15 @@ import {
5453
5681
  } from "@delorenj/skillex";
5454
5682
 
5455
5683
  // src/parity/skill-roots.ts
5456
- import { spawnSync as spawnSync2 } from "node:child_process";
5457
- import { createHash as createHash2 } from "node:crypto";
5684
+ import { spawnSync as spawnSync3 } from "node:child_process";
5685
+ import { createHash as createHash3 } from "node:crypto";
5458
5686
  import {
5459
5687
  appendFileSync,
5460
- lstatSync as lstatSync2,
5688
+ lstatSync as lstatSync4,
5461
5689
  mkdirSync as mkdirSync2,
5462
- readFileSync as readFileSync2,
5463
- readdirSync as readdirSync2,
5464
- readlinkSync,
5690
+ readFileSync as readFileSync4,
5691
+ readdirSync as readdirSync3,
5692
+ readlinkSync as readlinkSync2,
5465
5693
  realpathSync,
5466
5694
  renameSync,
5467
5695
  rmSync,
@@ -5471,15 +5699,15 @@ import {
5471
5699
  unlinkSync,
5472
5700
  writeFileSync as writeFileSync2
5473
5701
  } from "node:fs";
5474
- import { basename, dirname as dirname3, isAbsolute, join as join3, relative as relative2, resolve as resolve3, sep } from "node:path";
5702
+ import { basename, dirname as dirname3, isAbsolute as isAbsolute2, join as join5, relative as relative3, resolve as resolve4, sep } from "node:path";
5475
5703
 
5476
5704
  // src/parity/bmad-attestation.ts
5477
- import { createHash } from "node:crypto";
5478
- import { existsSync as existsSync2, lstatSync, readFileSync, readdirSync } from "node:fs";
5479
- import { dirname as dirname2, join as join2, relative } from "node:path";
5705
+ import { createHash as createHash2 } from "node:crypto";
5706
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
5707
+ import { dirname as dirname2, join as join4, relative as relative2 } from "node:path";
5480
5708
  import YAML from "yaml";
5481
5709
  function safeReadText(path) {
5482
- return existsSync2(path) ? readFileSync(path, "utf8").replace(/\r\n/g, "\n") : null;
5710
+ return existsSync3(path) ? readFileSync3(path, "utf8").replace(/\r\n/g, "\n") : null;
5483
5711
  }
5484
5712
  function parseCsvRows(text) {
5485
5713
  const rows = [];
@@ -5523,7 +5751,7 @@ function csvObjects(text) {
5523
5751
  return rows.filter((row) => row.some(Boolean)).map((row) => Object.fromEntries(headers.map((header, index) => [header, row[index] ?? ""])));
5524
5752
  }
5525
5753
  function installedBmadTools(repoRoot) {
5526
- const raw = safeReadText(join2(repoRoot, "_bmad", "_config", "manifest.yaml"));
5754
+ const raw = safeReadText(join4(repoRoot, "_bmad", "_config", "manifest.yaml"));
5527
5755
  if (!raw) return /* @__PURE__ */ new Set();
5528
5756
  try {
5529
5757
  const parsed = YAML.parse(raw);
@@ -5533,8 +5761,8 @@ function installedBmadTools(repoRoot) {
5533
5761
  }
5534
5762
  }
5535
5763
  function bmadCliProjectionInventory(repoRoot) {
5536
- const filesText = safeReadText(join2(repoRoot, "_bmad", "_config", "files-manifest.csv"));
5537
- const skillsText = safeReadText(join2(repoRoot, "_bmad", "_config", "skill-manifest.csv"));
5764
+ const filesText = safeReadText(join4(repoRoot, "_bmad", "_config", "files-manifest.csv"));
5765
+ const skillsText = safeReadText(join4(repoRoot, "_bmad", "_config", "skill-manifest.csv"));
5538
5766
  if (!filesText || !skillsText) return { files: /* @__PURE__ */ new Map(), error: "BMAD files/skill manifests are missing" };
5539
5767
  const fileHashes = /* @__PURE__ */ new Map();
5540
5768
  for (const row of csvObjects(filesText)) {
@@ -5550,23 +5778,23 @@ function bmadCliProjectionInventory(repoRoot) {
5550
5778
  const sourceRoot = dirname2(skillPath);
5551
5779
  for (const [sourcePath, hash] of fileHashes) {
5552
5780
  if (sourcePath !== `${sourceRoot}/SKILL.md` && !sourcePath.startsWith(`${sourceRoot}/`)) continue;
5553
- const suffix = relative(sourceRoot, sourcePath);
5781
+ const suffix = relative2(sourceRoot, sourcePath);
5554
5782
  if (!suffix || suffix.startsWith("..")) continue;
5555
- projected.set(join2("skills", canonicalId, suffix), hash);
5783
+ projected.set(join4("skills", canonicalId, suffix), hash);
5556
5784
  }
5557
5785
  }
5558
5786
  return projected.size ? { files: projected } : { files: projected, error: "BMAD manifests contain no projected skill inventory" };
5559
5787
  }
5560
5788
  function inventoryFilesUnder(root, current = root) {
5561
- if (!existsSync2(current)) return { files: [], unsafe: [] };
5562
- const stat = lstatSync(current);
5563
- const rel = relative(root, current) || ".";
5789
+ if (!existsSync3(current)) return { files: [], unsafe: [] };
5790
+ const stat = lstatSync3(current);
5791
+ const rel = relative2(root, current) || ".";
5564
5792
  if (stat.isSymbolicLink()) return { files: [], unsafe: [rel] };
5565
- if (stat.isFile()) return { files: [relative(root, current)], unsafe: [] };
5793
+ if (stat.isFile()) return { files: [relative2(root, current)], unsafe: [] };
5566
5794
  if (!stat.isDirectory()) return { files: [], unsafe: [rel] };
5567
5795
  const result2 = { files: [], unsafe: [] };
5568
- for (const name of readdirSync(current)) {
5569
- const child = inventoryFilesUnder(root, join2(current, name));
5796
+ for (const name of readdirSync2(current)) {
5797
+ const child = inventoryFilesUnder(root, join4(current, name));
5570
5798
  result2.files.push(...child.files);
5571
5799
  result2.unsafe.push(...child.unsafe);
5572
5800
  }
@@ -5578,7 +5806,7 @@ function attestBmadInstallerFiles(inventory, base, files, label) {
5578
5806
  for (const rel of files) {
5579
5807
  const expectedHash = inventory.files.get(rel);
5580
5808
  if (!expectedHash) return { safe: false, reason: `${label}/${rel} is outside the BMAD generated inventory` };
5581
- const actualHash = createHash("sha256").update(readFileSync(join2(base, rel))).digest("hex");
5809
+ const actualHash = createHash2("sha256").update(readFileSync3(join4(base, rel))).digest("hex");
5582
5810
  if (actualHash !== expectedHash) return { safe: false, reason: `${label}/${rel} was locally modified after generation` };
5583
5811
  }
5584
5812
  return { safe: true, reason: `${files.length} file(s) match BMAD installer inventory and hashes` };
@@ -5590,7 +5818,7 @@ var SUPPORTED_SKILLS_ALIASES = SUPPORTED_CLIS.map((cli) => cli.skillsRoot);
5590
5818
  var SKILL_ROOTS_QUARANTINE = ".pjangler-quarantine";
5591
5819
  function lstatOrUndefined(path) {
5592
5820
  try {
5593
- return lstatSync2(path);
5821
+ return lstatSync4(path);
5594
5822
  } catch (error) {
5595
5823
  const code = error.code;
5596
5824
  if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP" || code === "ENAMETOOLONG") return void 0;
@@ -5605,17 +5833,17 @@ function realpathOrUndefined(path) {
5605
5833
  }
5606
5834
  }
5607
5835
  function within(path, root) {
5608
- const part = relative2(root, path);
5609
- return part === "" || !part.startsWith(`..${sep}`) && part !== ".." && !isAbsolute(part);
5836
+ const part = relative3(root, path);
5837
+ return part === "" || !part.startsWith(`..${sep}`) && part !== ".." && !isAbsolute2(part);
5610
5838
  }
5611
5839
  function gitToplevel(directory) {
5612
- const result2 = spawnSync2("git", ["-C", directory, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
5840
+ const result2 = spawnSync3("git", ["-C", directory, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
5613
5841
  if (result2.status !== 0) return void 0;
5614
5842
  const top = result2.stdout.trim();
5615
5843
  return top ? realpathOrUndefined(top) ?? top : void 0;
5616
5844
  }
5617
5845
  function repoOwnsPath(repoRoot, path) {
5618
- const repoReal = realpathOrUndefined(repoRoot) ?? resolve3(repoRoot);
5846
+ const repoReal = realpathOrUndefined(repoRoot) ?? resolve4(repoRoot);
5619
5847
  const real = realpathOrUndefined(path);
5620
5848
  if (!real || !within(real, repoReal)) return false;
5621
5849
  let directory = real;
@@ -5627,7 +5855,7 @@ function repoOwnsPath(repoRoot, path) {
5627
5855
  return gitToplevel(directory) === gitToplevel(repoReal);
5628
5856
  }
5629
5857
  function entryOf(path, stat) {
5630
- if (stat.isSymbolicLink()) return { kind: "link", text: readlinkSync(path), stat, phys: path };
5858
+ if (stat.isSymbolicLink()) return { kind: "link", text: readlinkSync2(path), stat, phys: path };
5631
5859
  if (stat.isDirectory()) return { kind: "dir", stat, phys: path };
5632
5860
  if (stat.isFile()) return { kind: "file", stat, phys: path };
5633
5861
  return { kind: "special", stat, phys: path };
@@ -5638,16 +5866,16 @@ var diskLayout = {
5638
5866
  return stat ? entryOf(path, stat) : void 0;
5639
5867
  },
5640
5868
  readdir(path) {
5641
- return readdirSync2(path);
5869
+ return readdirSync3(path);
5642
5870
  }
5643
5871
  };
5644
5872
  function finalLayout(spec) {
5645
5873
  const underIncoming = (path) => {
5646
5874
  if (!within(path, spec.root) || path === spec.root) return void 0;
5647
- const [name, ...rest] = relative2(spec.root, path).split(sep);
5875
+ const [name, ...rest] = relative3(spec.root, path).split(sep);
5648
5876
  const occupant = spec.incoming.get(name);
5649
5877
  if (!occupant) return void 0;
5650
- if (occupant.kind === "move") return { mapped: join3(occupant.from, ...rest) };
5878
+ if (occupant.kind === "move") return { mapped: join5(occupant.from, ...rest) };
5651
5879
  return rest.length ? void 0 : { link: occupant.text };
5652
5880
  };
5653
5881
  return {
@@ -5662,8 +5890,8 @@ function finalLayout(spec) {
5662
5890
  },
5663
5891
  readdir(path) {
5664
5892
  const mapping = underIncoming(path);
5665
- if (mapping?.mapped) return readdirSync2(mapping.mapped);
5666
- const names = new Set(lstatOrUndefined(path)?.isDirectory() ? readdirSync2(path) : []);
5893
+ if (mapping?.mapped) return readdirSync3(mapping.mapped);
5894
+ const names = new Set(lstatOrUndefined(path)?.isDirectory() ? readdirSync3(path) : []);
5667
5895
  if (path === spec.root) for (const name of spec.incoming.keys()) names.add(name);
5668
5896
  for (const created of [...spec.links.keys(), ...spec.dirs]) if (dirname3(created) === path) names.add(basename(created));
5669
5897
  return [...names].sort();
@@ -5703,7 +5931,7 @@ function walkNode(layout, path, root) {
5703
5931
  if (!entry) return { t: "special" };
5704
5932
  if (entry.kind === "link") {
5705
5933
  const text = entry.text ?? "";
5706
- return { t: "link", text, escapes: !isAbsolute(text) && !within(resolve3(dirname3(path), text), root) };
5934
+ return { t: "link", text, escapes: !isAbsolute2(text) && !within(resolve4(dirname3(path), text), root) };
5707
5935
  }
5708
5936
  if (entry.kind === "file") {
5709
5937
  const stat = entry.stat;
@@ -5711,7 +5939,7 @@ function walkNode(layout, path, root) {
5711
5939
  }
5712
5940
  if (entry.kind === "dir") {
5713
5941
  const children = /* @__PURE__ */ new Map();
5714
- for (const name of layout.readdir(path).sort()) children.set(name, walkNode(layout, join3(path, name), root));
5942
+ for (const name of layout.readdir(path).sort()) children.set(name, walkNode(layout, join5(path, name), root));
5715
5943
  return { t: "dir", children };
5716
5944
  }
5717
5945
  return { t: "special" };
@@ -5731,7 +5959,7 @@ var Hasher = class {
5731
5959
  if (this.cache.has(key)) return this.cache.get(key);
5732
5960
  const path = before ? this.translate(node.phys) : node.phys;
5733
5961
  const stat = lstatOrUndefined(path);
5734
- const digest = stat && stat.ino === node.ino && stat.dev === node.dev ? createHash2("sha256").update(readFileSync2(path)).digest("hex") : void 0;
5962
+ const digest = stat && stat.ino === node.ino && stat.dev === node.dev ? createHash3("sha256").update(readFileSync4(path)).digest("hex") : void 0;
5735
5963
  this.cache.set(key, digest);
5736
5964
  return digest;
5737
5965
  }
@@ -5809,34 +6037,34 @@ function shellQuote(value) {
5809
6037
  function describeQuarantine(quarantine, label) {
5810
6038
  let runs;
5811
6039
  try {
5812
- runs = readdirSync2(quarantine).sort();
6040
+ runs = readdirSync3(quarantine).sort();
5813
6041
  } catch {
5814
6042
  return [`${label(quarantine)} exists but cannot be read; inspect it before converting any CLI skills root`];
5815
6043
  }
5816
6044
  if (!runs.length) return [`${label(quarantine)} is an empty leftover of an earlier conversion; remove it: rmdir ${shellQuote(quarantine)}`];
5817
6045
  return runs.map((run) => {
5818
- const path = join3(quarantine, run);
6046
+ const path = join5(quarantine, run);
5819
6047
  let progress;
5820
6048
  try {
5821
- progress = readFileSync2(join3(path, "progress.log"), "utf8");
6049
+ progress = readFileSync4(join5(path, "progress.log"), "utf8");
5822
6050
  } catch {
5823
6051
  }
5824
6052
  if (progress !== void 0 && /^committed$/m.test(progress)) {
5825
6053
  return `an earlier conversion completed and was verified, but its quarantine of proven duplicates remains at ${label(path)}; remove it: rm -rf ${shellQuote(path)}`;
5826
6054
  }
5827
6055
  if (progress !== void 0 && /^rolled-back$/m.test(progress)) {
5828
- return `an earlier conversion was rolled back, but ${label(join3(path, "items"))} still holds entries nothing planned to put there; inspect them, then remove ${shellQuote(path)}`;
6056
+ return `an earlier conversion was rolled back, but ${label(join5(path, "items"))} still holds entries nothing planned to put there; inspect them, then remove ${shellQuote(path)}`;
5829
6057
  }
5830
- if (progress === void 0 || !lstatOrUndefined(join3(path, "restore.sh"))) {
6058
+ if (progress === void 0 || !lstatOrUndefined(join5(path, "restore.sh"))) {
5831
6059
  return `an interrupted conversion left ${label(path)} before its first step (no journal), so nothing was changed; remove it: rm -rf ${shellQuote(path)}`;
5832
6060
  }
5833
- return `an interrupted CLI skills-root conversion left ${label(path)}; restore the repository exactly with: sh ${shellQuote(join3(path, "restore.sh"))} (it reverses every completed step listed in journal.json, then removes the quarantine)`;
6061
+ return `an interrupted CLI skills-root conversion left ${label(path)}; restore the repository exactly with: sh ${shellQuote(join5(path, "restore.sh"))} (it reverses every completed step listed in journal.json, then removes the quarantine)`;
5834
6062
  });
5835
6063
  }
5836
6064
  function indexRows(repoReal, rel) {
5837
- const probe = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], { cwd: repoReal, encoding: "utf8" });
6065
+ const probe = spawnSync3("git", ["rev-parse", "--is-inside-work-tree"], { cwd: repoReal, encoding: "utf8" });
5838
6066
  if (probe.status !== 0 || probe.stdout.trim() !== "true") return { rows: [], worktree: false };
5839
- const listed = spawnSync2("git", ["ls-files", "-s", "-z", "--", rel], { cwd: repoReal, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
6067
+ const listed = spawnSync3("git", ["ls-files", "-s", "-z", "--", rel], { cwd: repoReal, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
5840
6068
  if (listed.status !== 0) return { rows: [], worktree: true, error: `git ls-files failed for ${rel}: ${listed.stderr.trim()}` };
5841
6069
  const rows = listed.stdout.split("\0").filter(Boolean).map((line) => {
5842
6070
  const [meta, path] = line.split(" ");
@@ -5846,19 +6074,19 @@ function indexRows(repoReal, rel) {
5846
6074
  }
5847
6075
  function entryHazard(path, label) {
5848
6076
  const walk = (current, rel) => {
5849
- const stat = lstatSync2(current);
6077
+ const stat = lstatSync4(current);
5850
6078
  if (rel && basename(current) === ".git") return `${label} contains a nested git repository (${rel}); refusing to move or drop another repository's work tree`;
5851
6079
  if (stat.isSymbolicLink()) {
5852
- const text = readlinkSync(current);
5853
- if (!isAbsolute(text) && !within(resolve3(dirname3(current), text), path)) {
6080
+ const text = readlinkSync2(current);
6081
+ if (!isAbsolute2(text) && !within(resolve4(dirname3(current), text), path)) {
5854
6082
  return `${label}/${rel} -> ${text} leaves the entry; moving or dropping it would change what that link resolves to`;
5855
6083
  }
5856
6084
  return void 0;
5857
6085
  }
5858
6086
  if (stat.isFile()) return void 0;
5859
6087
  if (stat.isDirectory()) {
5860
- for (const name of readdirSync2(current).sort()) {
5861
- const found = walk(join3(current, name), rel ? `${rel}/${name}` : name);
6088
+ for (const name of readdirSync3(current).sort()) {
6089
+ const found = walk(join5(current, name), rel ? `${rel}/${name}` : name);
5862
6090
  if (found) return found;
5863
6091
  }
5864
6092
  return void 0;
@@ -5869,25 +6097,25 @@ function entryHazard(path, label) {
5869
6097
  }
5870
6098
  function planSkillRoots(repoRoot, options = {}) {
5871
6099
  const selected = options.aliases ?? SUPPORTED_SKILLS_ALIASES;
5872
- const lexicalRoot = join3(repoRoot, ".agents", "skills");
6100
+ const lexicalRoot = join5(repoRoot, ".agents", "skills");
5873
6101
  const repoReal = realpathOrUndefined(repoRoot);
5874
6102
  const empty = (rootState2, reason) => {
5875
- const aliases2 = selected.map((alias) => ({ alias, path: join3(repoRoot, alias), state: "unsupported", operations: [], blocks: [reason], summary: `${alias}: blocked (${reason})` }));
6103
+ const aliases2 = selected.map((alias) => ({ alias, path: join5(repoRoot, alias), state: "unsupported", operations: [], blocks: [reason], summary: `${alias}: blocked (${reason})` }));
5876
6104
  return { repoRoot, root: lexicalRoot, rootState: rootState2, aliases: aliases2, operations: [], blocks: [reason], clean: false, incoming: [] };
5877
6105
  };
5878
6106
  if (!repoReal) return empty("unsafe", `${repoRoot} does not resolve; refusing to plan any CLI skills root`);
5879
- const lex = (path) => within(path, repoReal) ? join3(repoRoot, relative2(repoReal, path)) : path;
5880
- const rel = (path) => relative2(repoReal, path) || ".";
5881
- const agents = join3(repoReal, ".agents");
5882
- const R = join3(agents, "skills");
6107
+ const lex = (path) => within(path, repoReal) ? join5(repoRoot, relative3(repoReal, path)) : path;
6108
+ const rel = (path) => relative3(repoReal, path) || ".";
6109
+ const agents = join5(repoReal, ".agents");
6110
+ const R = join5(agents, "skills");
5883
6111
  const agentsStat = lstatOrUndefined(agents);
5884
6112
  if (agentsStat && (agentsStat.isSymbolicLink() || !agentsStat.isDirectory())) {
5885
- const what = agentsStat.isSymbolicLink() ? `a symlink (to ${readlinkSync(agents)})` : "not a directory";
6113
+ const what = agentsStat.isSymbolicLink() ? `a symlink (to ${readlinkSync2(agents)})` : "not a directory";
5886
6114
  return empty("unsafe", `.agents is ${what}; the activation root must be a real .agents/skills directory inside the repository, so no CLI skills root is planned`);
5887
6115
  }
5888
6116
  const rootStat = agentsStat ? lstatOrUndefined(R) : void 0;
5889
6117
  const rootState = !rootStat ? "absent" : rootStat.isSymbolicLink() ? "symlink" : rootStat.isDirectory() ? "directory" : "other";
5890
- const quarantine = join3(agents, SKILL_ROOTS_QUARANTINE);
6118
+ const quarantine = join5(agents, SKILL_ROOTS_QUARANTINE);
5891
6119
  if (lstatOrUndefined(quarantine)) {
5892
6120
  const reasons = describeQuarantine(quarantine, (path) => rel(path));
5893
6121
  const plan2 = empty(rootState, reasons[0]);
@@ -5907,15 +6135,15 @@ function planSkillRoots(repoRoot, options = {}) {
5907
6135
  const repoTop = gitToplevel(repoReal);
5908
6136
  let inventory;
5909
6137
  const inventoryOf = () => inventory ??= bmadCliProjectionInventory(repoReal);
5910
- const realAliasDirs = SUPPORTED_SKILLS_ALIASES.map((alias) => join3(repoReal, alias)).filter((path) => {
6138
+ const realAliasDirs = SUPPORTED_SKILLS_ALIASES.map((alias) => join5(repoReal, alias)).filter((path) => {
5911
6139
  const stat = lstatOrUndefined(path);
5912
6140
  return Boolean(stat?.isDirectory() && !stat.isSymbolicLink() && !lstatOrUndefined(dirname3(path))?.isSymbolicLink());
5913
6141
  });
5914
6142
  const infos = selected.map((alias) => {
5915
- const real = join3(repoReal, alias);
6143
+ const real = join5(repoReal, alias);
5916
6144
  const cliRoot = dirname3(real);
5917
6145
  const cliName = rel(cliRoot);
5918
- const info = { alias, path: join3(repoRoot, alias), real, cliRoot, cliName, state: "unsupported", cliRootExists: false, blocks: [], candidates: [] };
6146
+ const info = { alias, path: join5(repoRoot, alias), real, cliRoot, cliName, state: "unsupported", cliRootExists: false, blocks: [], candidates: [] };
5919
6147
  if (rootState === "other") {
5920
6148
  info.blocks.push(`.agents/skills is not a directory; refusing to plan ${alias}`);
5921
6149
  return info;
@@ -5939,10 +6167,10 @@ function planSkillRoots(repoRoot, options = {}) {
5939
6167
  return info;
5940
6168
  }
5941
6169
  if (stat.isSymbolicLink()) {
5942
- const text = readlinkSync(real);
6170
+ const text = readlinkSync2(real);
5943
6171
  info.text = text;
5944
6172
  const reached = realpathOrUndefined(real);
5945
- if (rootReal !== void 0 && reached === rootReal || rootState === "absent" && resolve3(cliRoot, text) === R) {
6173
+ if (rootReal !== void 0 && reached === rootReal || rootState === "absent" && resolve4(cliRoot, text) === R) {
5946
6174
  info.state = "alias";
5947
6175
  return info;
5948
6176
  }
@@ -5989,16 +6217,16 @@ function planSkillRoots(repoRoot, options = {}) {
5989
6217
  const name = row.path.slice(aliasRel.length + 1).split("/")[0];
5990
6218
  tracked.set(name, [...tracked.get(name) ?? [], row]);
5991
6219
  }
5992
- for (const name of readdirSync2(real).sort()) {
5993
- const path = join3(real, name);
6220
+ for (const name of readdirSync3(real).sort()) {
6221
+ const path = join5(real, name);
5994
6222
  const label = `${alias}/${name}`;
5995
- const entryStat = lstatSync2(path);
6223
+ const entryStat = lstatSync4(path);
5996
6224
  const rows = tracked.get(name) ?? [];
5997
6225
  const candidate = { alias, name, path, label, kind: entryStat.isSymbolicLink() ? "link" : entryStat.isDirectory() ? "directory" : "file", tracked: rows.map((row) => row.path) };
5998
6226
  if (name === ".git") {
5999
6227
  candidate.staticBlock = `${label} is git metadata; refusing to relocate another repository's work tree`;
6000
6228
  } else if (entryStat.isSymbolicLink()) {
6001
- candidate.text = readlinkSync(path);
6229
+ candidate.text = readlinkSync2(path);
6002
6230
  candidate.resolution = resolveIn(diskLayout, path);
6003
6231
  if (rows.length) candidate.staticBlock = `${label} -> ${candidate.text} is tracked in git; a tracked link cannot be attested BMAD installer output, so it is never untracked, moved or dropped`;
6004
6232
  } else if (!entryStat.isDirectory() && !entryStat.isFile()) {
@@ -6012,10 +6240,10 @@ function planSkillRoots(repoRoot, options = {}) {
6012
6240
  if (!candidate.staticBlock && linkRow) candidate.staticBlock = `${label} holds a tracked symlink (${linkRow.path}); a tracked link cannot be attested BMAD installer output`;
6013
6241
  if (!candidate.staticBlock && rows.length) {
6014
6242
  const paths = rows.map((row) => row.path);
6015
- const missing = paths.filter((path2) => !lstatOrUndefined(join3(repoReal, path2)));
6243
+ const missing = paths.filter((path2) => !lstatOrUndefined(join5(repoReal, path2)));
6016
6244
  if (missing.length) candidate.staticBlock = `${label} has tracked path(s) missing from the work tree (${missing[0]}); restore or commit the deletion first`;
6017
6245
  else {
6018
- const verdict = attestBmadInstallerFiles(inventoryOf(), cliRoot, paths.map((path2) => relative2(cliRoot, join3(repoReal, path2))), cliName);
6246
+ const verdict = attestBmadInstallerFiles(inventoryOf(), cliRoot, paths.map((path2) => relative3(cliRoot, join5(repoReal, path2))), cliName);
6019
6247
  if (!verdict.safe) candidate.staticBlock = `${label} is tracked in git and not attested BMAD installer output: ${verdict.reason}`;
6020
6248
  }
6021
6249
  }
@@ -6064,15 +6292,15 @@ function planSkillRoots(repoRoot, options = {}) {
6064
6292
  };
6065
6293
  const finalText = (candidate) => {
6066
6294
  const text = candidate.text;
6067
- if (isAbsolute(text)) return text;
6068
- const lexical = resolve3(dirname3(candidate.path), text);
6295
+ if (isAbsolute2(text)) return text;
6296
+ const lexical = resolve4(dirname3(candidate.path), text);
6069
6297
  const home = activeDirs.find((dir) => within(lexical, dir));
6070
- const mapped = home ? join3(R, relative2(home, lexical)) : lexical;
6071
- return relative2(R, mapped) || ".";
6298
+ const mapped = home ? join5(R, relative3(home, lexical)) : lexical;
6299
+ return relative3(R, mapped) || ".";
6072
6300
  };
6073
6301
  for (const name of [...byName.keys()].sort()) {
6074
6302
  const candidates = byName.get(name);
6075
- const counterpart = join3(R, name);
6303
+ const counterpart = join5(R, name);
6076
6304
  const counterpartLabel = `.agents/skills/${name}`;
6077
6305
  const live = [];
6078
6306
  for (const candidate of candidates) {
@@ -6087,7 +6315,7 @@ function planSkillRoots(repoRoot, options = {}) {
6087
6315
  else decide(candidate, "drop-duplicate-entry", `drop ${candidate.label} (every entry already exists byte-identical in ${where})`, extra);
6088
6316
  };
6089
6317
  if (rootEntry) {
6090
- const counterpartText = rootEntry.isSymbolicLink() ? readlinkSync(counterpart) : void 0;
6318
+ const counterpartText = rootEntry.isSymbolicLink() ? readlinkSync2(counterpart) : void 0;
6091
6319
  const rootView = beforeView(counterpart);
6092
6320
  if (rootEntry.isSymbolicLink()) {
6093
6321
  if (!rootView) {
@@ -6181,7 +6409,7 @@ function planSkillRoots(repoRoot, options = {}) {
6181
6409
  const responsible = converting.filter((info) => within(path, info.real) || [...view.hops, view.real].some((hop) => within(hop, info.real)));
6182
6410
  for (const hop of [path, ...view.hops, view.real]) {
6183
6411
  if (!within(hop, R) || hop === R) continue;
6184
- const writer = incomingByName.get(relative2(R, hop).split(sep)[0]);
6412
+ const writer = incomingByName.get(relative3(R, hop).split(sep)[0]);
6185
6413
  const info = writer && converting.find((item) => item.alias === writer.alias);
6186
6414
  if (info && !responsible.includes(info)) responsible.push(info);
6187
6415
  }
@@ -6267,10 +6495,10 @@ function planSkillRoots(repoRoot, options = {}) {
6267
6495
  const after = finalLayout(spec);
6268
6496
  for (const [name, entry] of incomingByName) {
6269
6497
  if (blocked.has(entry.alias)) continue;
6270
- const resolution = resolveIn(after, join3(R, name));
6498
+ const resolution = resolveIn(after, join5(R, name));
6271
6499
  const phys = resolution ? after.lstat(resolution.real)?.phys ?? resolution.real : void 0;
6272
- const text = entry.kind !== "link" ? void 0 : entry.incoming.kind === "link" ? entry.incoming.text : readlinkSync(entry.from);
6273
- incoming.push({ name, path: join3(lexicalRoot, name), from: lex(entry.from), kind: entry.kind, ...text === void 0 ? {} : { text }, resolves: phys ? lex(phys) : void 0 });
6500
+ const text = entry.kind !== "link" ? void 0 : entry.incoming.kind === "link" ? entry.incoming.text : readlinkSync2(entry.from);
6501
+ incoming.push({ name, path: join5(lexicalRoot, name), from: lex(entry.from), kind: entry.kind, ...text === void 0 ? {} : { text }, resolves: phys ? lex(phys) : void 0 });
6274
6502
  }
6275
6503
  }
6276
6504
  const plan = { repoRoot, root: lexicalRoot, rootState, aliases, operations, blocks, clean: blocks.length === 0, incoming };
@@ -6282,26 +6510,26 @@ function planSkillRoots(repoRoot, options = {}) {
6282
6510
  agentsExisted: Boolean(agentsStat),
6283
6511
  before,
6284
6512
  affected,
6285
- converted: converted.map((entry) => join3(repoReal, entry.alias)),
6286
- created: aliases.filter((entry) => entry.operations.some((operation) => operation.kind === "create-alias" || operation.kind === "replace-dangling-alias")).map((entry) => join3(repoReal, entry.alias))
6513
+ converted: converted.map((entry) => join5(repoReal, entry.alias)),
6514
+ created: aliases.filter((entry) => entry.operations.some((operation) => operation.kind === "create-alias" || operation.kind === "replace-dangling-alias")).map((entry) => join5(repoReal, entry.alias))
6287
6515
  });
6288
6516
  return plan;
6289
6517
  }
6290
6518
  function visibleViews(repoReal, R, view) {
6291
6519
  const views = /* @__PURE__ */ new Map();
6292
6520
  const seen = /* @__PURE__ */ new Set();
6293
- for (const location of [R, ...SUPPORTED_SKILLS_ALIASES.map((alias) => join3(repoReal, alias))]) {
6521
+ for (const location of [R, ...SUPPORTED_SKILLS_ALIASES.map((alias) => join5(repoReal, alias))]) {
6294
6522
  const resolved = realpathOrUndefined(location);
6295
6523
  if (!resolved || seen.has(resolved)) continue;
6296
6524
  seen.add(resolved);
6297
6525
  let names;
6298
6526
  try {
6299
- names = readdirSync2(location);
6527
+ names = readdirSync3(location);
6300
6528
  } catch {
6301
6529
  continue;
6302
6530
  }
6303
6531
  for (const name of names.sort()) {
6304
- const path = join3(location, name);
6532
+ const path = join5(location, name);
6305
6533
  const found = view(path);
6306
6534
  if (found) views.set(path, found);
6307
6535
  }
@@ -6317,12 +6545,12 @@ function changedPaths(operation) {
6317
6545
  return operation.from ? [operation.from, operation.path] : [operation.path];
6318
6546
  }
6319
6547
  function compileSteps(plan, facts, items) {
6320
- const physical = (path) => join3(facts.repoReal, relative2(plan.repoRoot, path));
6548
+ const physical = (path) => join5(facts.repoReal, relative3(plan.repoRoot, path));
6321
6549
  const steps = [];
6322
6550
  const park = (from, kind) => {
6323
6551
  const stat = lstatOrUndefined(from);
6324
- const text = kind === "link" && stat?.isSymbolicLink() ? readlinkSync(from) : void 0;
6325
- steps.push({ step: "rename", from, to: join3(items, `${steps.length}-${basename(from)}`), park: kind, ...text === void 0 ? {} : { text } });
6552
+ const text = kind === "link" && stat?.isSymbolicLink() ? readlinkSync2(from) : void 0;
6553
+ steps.push({ step: "rename", from, to: join5(items, `${steps.length}-${basename(from)}`), park: kind, ...text === void 0 ? {} : { text } });
6326
6554
  };
6327
6555
  for (const operation of plan.operations) {
6328
6556
  const path = physical(operation.path);
@@ -6425,15 +6653,15 @@ function runStep(step) {
6425
6653
  return;
6426
6654
  }
6427
6655
  if (lstatOrUndefined(step.to)) throw new Error(`${step.to} appeared before the move`);
6428
- if (step.park === "dir" && readdirSync2(step.from).length) throw new Error(`${step.from} is not empty (${readdirSync2(step.from)[0]})`);
6429
- if (step.park === "link" && (!lstatOrUndefined(step.from)?.isSymbolicLink() || readlinkSync(step.from) !== step.text)) throw new Error(`${step.from} is no longer the link that was planned`);
6656
+ if (step.park === "dir" && readdirSync3(step.from).length) throw new Error(`${step.from} is not empty (${readdirSync3(step.from)[0]})`);
6657
+ if (step.park === "link" && (!lstatOrUndefined(step.from)?.isSymbolicLink() || readlinkSync2(step.from) !== step.text)) throw new Error(`${step.from} is no longer the link that was planned`);
6430
6658
  renameSync(step.from, step.to);
6431
6659
  }
6432
6660
  function parkedGap(step, before, affected, hasher) {
6433
6661
  const stat = lstatOrUndefined(step.to);
6434
6662
  if (!stat) return `${step.to} is missing from the quarantine`;
6435
- if (step.park === "dir") return readdirSync2(step.to).length ? `${step.to} is no longer empty` : void 0;
6436
- if (step.park === "link") return stat.isSymbolicLink() && readlinkSync(step.to) === step.text ? void 0 : `${step.to} is no longer the planned link`;
6663
+ if (step.park === "dir") return readdirSync3(step.to).length ? `${step.to} is no longer empty` : void 0;
6664
+ if (step.park === "link") return stat.isSymbolicLink() && readlinkSync2(step.to) === step.text ? void 0 : `${step.to} is no longer the planned link`;
6437
6665
  if (!before?.node) return `${step.from} had no recorded content to compare`;
6438
6666
  const parked = viewIn(diskLayout, step.to, affected);
6439
6667
  if (!parked) return `${step.to} does not resolve`;
@@ -6447,7 +6675,7 @@ function undoStep(step) {
6447
6675
  }
6448
6676
  if (step.step === "symlink") {
6449
6677
  const stat = lstatOrUndefined(step.path);
6450
- if (!stat?.isSymbolicLink() || readlinkSync(step.path) !== step.text) throw new Error(`${step.path} is not the link this run created`);
6678
+ if (!stat?.isSymbolicLink() || readlinkSync2(step.path) !== step.text) throw new Error(`${step.path} is not the link this run created`);
6451
6679
  unlinkSync(step.path);
6452
6680
  return;
6453
6681
  }
@@ -6459,7 +6687,7 @@ function translator(steps, done) {
6459
6687
  let current = path;
6460
6688
  for (let index = 0; index < done; index++) {
6461
6689
  const step = steps[index];
6462
- if (step.step === "rename" && within(current, step.from)) current = join3(step.to, relative2(step.from, current));
6690
+ if (step.step === "rename" && within(current, step.from)) current = join5(step.to, relative3(step.from, current));
6463
6691
  }
6464
6692
  return current;
6465
6693
  };
@@ -6479,10 +6707,10 @@ function applySkillRoots(repoRoot, options) {
6479
6707
  if (!plan.operations.length) return { ok: plan.clean, changedFiles: [], details: [], blocks: plan.blocks, plan };
6480
6708
  const facts = internals.get(plan);
6481
6709
  const failed = (reason) => ({ ok: false, changedFiles: [], details: [], blocks: [...plan.blocks, reason], plan });
6482
- const agents = join3(facts.repoReal, ".agents");
6483
- const quarantine = join3(agents, SKILL_ROOTS_QUARANTINE);
6484
- const run = join3(quarantine, `${Date.now()}-${process.pid}`);
6485
- const items = join3(run, "items");
6710
+ const agents = join5(facts.repoReal, ".agents");
6711
+ const quarantine = join5(agents, SKILL_ROOTS_QUARANTINE);
6712
+ const run = join5(quarantine, `${Date.now()}-${process.pid}`);
6713
+ const items = join5(run, "items");
6486
6714
  const createdAgents = !lstatOrUndefined(agents);
6487
6715
  try {
6488
6716
  if (createdAgents) mkdirSync2(agents);
@@ -6492,12 +6720,12 @@ function applySkillRoots(repoRoot, options) {
6492
6720
  return failed(`could not create the quarantine ${quarantine}: ${error instanceof Error ? error.message : String(error)}`);
6493
6721
  }
6494
6722
  const steps = compileSteps(plan, facts, items);
6495
- const log = join3(run, "progress.log");
6723
+ const log = join5(run, "progress.log");
6496
6724
  try {
6497
- writeFileSync2(join3(run, ".gitignore"), "*\n");
6498
- writeFileSync2(join3(run, "journal.json"), `${JSON.stringify({ version: 1, repo: facts.repoReal, createdAgents, steps, operations: plan.operations.map((operation) => operation.detail) }, null, 2)}
6725
+ writeFileSync2(join5(run, ".gitignore"), "*\n");
6726
+ writeFileSync2(join5(run, "journal.json"), `${JSON.stringify({ version: 1, repo: facts.repoReal, createdAgents, steps, operations: plan.operations.map((operation) => operation.detail) }, null, 2)}
6499
6727
  `);
6500
- writeFileSync2(join3(run, "restore.sh"), restoreScript(run, quarantine, createdAgents ? agents : void 0, facts.repoReal, steps), { mode: 493 });
6728
+ writeFileSync2(join5(run, "restore.sh"), restoreScript(run, quarantine, createdAgents ? agents : void 0, facts.repoReal, steps), { mode: 493 });
6501
6729
  writeFileSync2(log, "");
6502
6730
  } catch (error) {
6503
6731
  rmSync(run, { recursive: true, force: true });
@@ -6535,11 +6763,11 @@ function applySkillRoots(repoRoot, options) {
6535
6763
  appendFileSync(log, `undone ${index}
6536
6764
  `);
6537
6765
  } catch (error) {
6538
- return failed(`${reason}; rollback stopped at step ${index} (${error instanceof Error ? error.message : String(error)}): finish it exactly with: sh ${shellQuote(join3(run, "restore.sh"))}`);
6766
+ return failed(`${reason}; rollback stopped at step ${index} (${error instanceof Error ? error.message : String(error)}): finish it exactly with: sh ${shellQuote(join5(run, "restore.sh"))}`);
6539
6767
  }
6540
6768
  }
6541
6769
  appendFileSync(log, "rolled-back\n");
6542
- if (readdirSync2(items).length) return failed(`${reason}; rolled back, but ${items} is not empty \u2014 inspect it, then remove ${run}`);
6770
+ if (readdirSync3(items).length) return failed(`${reason}; rolled back, but ${items} is not empty \u2014 inspect it, then remove ${run}`);
6543
6771
  cleanup();
6544
6772
  return failed(`rolled back: ${reason}`);
6545
6773
  };
@@ -6560,12 +6788,12 @@ function applySkillRoots(repoRoot, options) {
6560
6788
  const mismatches = [];
6561
6789
  for (const [path, view] of facts.before) {
6562
6790
  const gap = viewGap(view, viewIn(diskLayout, path, facts.affected), hasher);
6563
- if (gap) mismatches.push(`${relative2(facts.repoReal, path)}: ${gap}`);
6791
+ if (gap) mismatches.push(`${relative3(facts.repoReal, path)}: ${gap}`);
6564
6792
  }
6565
6793
  const rootReal = realpathOrUndefined(facts.R);
6566
6794
  for (const alias of [...facts.converted, ...facts.created]) {
6567
- const reaches = rootReal ? realpathOrUndefined(alias) === rootReal : resolve3(dirname3(alias), readlinkSync(alias)) === facts.R;
6568
- if (!reaches) mismatches.push(`${relative2(facts.repoReal, alias)} does not resolve to .agents/skills`);
6795
+ const reaches = rootReal ? realpathOrUndefined(alias) === rootReal : resolve4(dirname3(alias), readlinkSync2(alias)) === facts.R;
6796
+ if (!reaches) mismatches.push(`${relative3(facts.repoReal, alias)} does not resolve to .agents/skills`);
6569
6797
  }
6570
6798
  for (const step of steps) {
6571
6799
  if (step.step !== "rename" || !step.park) continue;
@@ -6575,7 +6803,7 @@ function applySkillRoots(repoRoot, options) {
6575
6803
  if (mismatches.length) return rollback(mismatches.slice(0, 5).join("; "));
6576
6804
  const tracked = plan.operations.flatMap((operation) => operation.kind === "untrack" ? operation.tracked ?? [] : []);
6577
6805
  if (tracked.length) {
6578
- const removed = spawnSync2(
6806
+ const removed = spawnSync3(
6579
6807
  "git",
6580
6808
  ["rm", "-r", "--cached", "--quiet", "--pathspec-from-file=-", "--pathspec-file-nul"],
6581
6809
  { cwd: facts.repoReal, encoding: "utf8", input: `${tracked.join("\0")}\0` }
@@ -6597,32 +6825,32 @@ function shellQuotePath(path) {
6597
6825
  var PJ_SKILLS = "pj skills";
6598
6826
  function skillCoreOptions(ctx) {
6599
6827
  return {
6600
- cwd: resolve4(ctx.repoRoot),
6601
- project: resolve4(ctx.repoRoot),
6828
+ cwd: resolve5(ctx.repoRoot),
6829
+ project: resolve5(ctx.repoRoot),
6602
6830
  scope: "project",
6603
6831
  home: ctx.homeDir,
6604
6832
  env: process.env,
6605
6833
  // Preserve the documented PJangler override while using one core resolver.
6606
- ...process.env.PJ_SKILLS_REGISTRY_ROOT?.trim() ? { registryRoot: resolve4(process.env.PJ_SKILLS_REGISTRY_ROOT.trim()) } : {}
6834
+ ...process.env.PJ_SKILLS_REGISTRY_ROOT?.trim() ? { registryRoot: resolve5(process.env.PJ_SKILLS_REGISTRY_ROOT.trim()) } : {}
6607
6835
  };
6608
6836
  }
6609
6837
  function relaySkillexCommands(text) {
6610
6838
  return text.replace(SKILLEX_COMMAND, PJ_SKILLS);
6611
6839
  }
6612
- var SKILLEX_COMMAND = /(?<![\w/@.:-])skillex(?=\s+(?:skill|set|pack|init|enable|disable|inherit|sync|status|explain|doctor|vendor|profile|migrate)\b)/g;
6840
+ var SKILLEX_COMMAND = /(?<![\w/@.:-])skillex(?=\s+(?:skill|set|pack|bmad|init|enable|disable|inherit|normalize|sync|status|explain|doctor|vendor|profile|migrate|integrations|clients|root)\b)/g;
6613
6841
  function skillDiagnostics(findings) {
6614
6842
  return findings.map(
6615
6843
  (finding2) => `${finding2.code}: ${finding2.message}${finding2.path ? ` (${finding2.path})` : ""}${finding2.fix ? ` \u2014 ${relaySkillexCommands(finding2.fix)}` : ""}`
6616
6844
  );
6617
6845
  }
6618
6846
  function migrationGuidance(ctx) {
6619
- const project = `--project ${shellQuotePath(resolve4(ctx.repoRoot))}`;
6847
+ const project = `--project ${shellQuotePath(resolve5(ctx.repoRoot))}`;
6620
6848
  return `Legacy selections or skill-root content need explicit review. Preview: ${PJ_SKILLS} migrate ${project}. Apply the reviewed plan: ${PJ_SKILLS} migrate ${project} --apply (add --mapping <file> only for content the preview reports as ambiguous). PJangler relocates CLI-root skill entries losslessly into .agents/skills so each root can become the alias, but never claims ownership of foreign or installer-owned skills and never deletes content that is not a proven duplicate.`;
6621
6849
  }
6622
6850
  function projectPaths(ctx, rel) {
6623
- const paths = /* @__PURE__ */ new Set([join4(resolve4(ctx.repoRoot), rel)]);
6851
+ const paths = /* @__PURE__ */ new Set([join6(resolve5(ctx.repoRoot), rel)]);
6624
6852
  try {
6625
- paths.add(join4(realpathSync2(ctx.repoRoot), rel));
6853
+ paths.add(join6(realpathSync2(ctx.repoRoot), rel));
6626
6854
  } catch {
6627
6855
  }
6628
6856
  return paths;
@@ -6632,7 +6860,7 @@ function activationConflict(ctx, findings) {
6632
6860
  if (!finding2?.path) return void 0;
6633
6861
  const aliases = new Set(SUPPORTED_SKILLS_ALIASES.flatMap((alias) => [...projectPaths(ctx, alias)]));
6634
6862
  if (aliases.has(finding2.path)) return { kind: "alias", path: finding2.path, name: basename2(dirname4(finding2.path)) };
6635
- if (projectPaths(ctx, join4(".agents", "skills")).has(dirname4(finding2.path)) && /collides with foreign activation content/.test(finding2.message)) {
6863
+ if (projectPaths(ctx, join6(".agents", "skills")).has(dirname4(finding2.path)) && /collides with foreign activation content/.test(finding2.message)) {
6636
6864
  return { kind: "root", path: finding2.path, name: basename2(finding2.path) };
6637
6865
  }
6638
6866
  return void 0;
@@ -6647,10 +6875,10 @@ async function canonicalSkillPath(ctx, name) {
6647
6875
  return (await projectBindings(ctx)).find((binding) => binding.name === name)?.path;
6648
6876
  }
6649
6877
  async function classifyRootCollision(ctx, path) {
6650
- const label = relative3(resolve4(ctx.repoRoot), path) || path;
6878
+ const label = relative4(resolve5(ctx.repoRoot), path) || path;
6651
6879
  let stat;
6652
6880
  try {
6653
- stat = lstatSync3(path);
6881
+ stat = lstatSync5(path);
6654
6882
  } catch {
6655
6883
  return { action: "block", detail: `${label} disappeared while resolving a root collision` };
6656
6884
  }
@@ -6661,7 +6889,7 @@ async function classifyRootCollision(ctx, path) {
6661
6889
  } catch {
6662
6890
  real = void 0;
6663
6891
  }
6664
- return classifyCollidingLink(ctx, basename2(path), label, readlinkSync2(path), real);
6892
+ return classifyCollidingLink(ctx, basename2(path), label, readlinkSync3(path), real);
6665
6893
  }
6666
6894
  function realEntryCollision(label, name, directory) {
6667
6895
  return { action: "block", detail: `${label} is a real ${directory ? "directory" : "entry"} that collides with the selected catalog skill ${name}; move or rename it, pjangler never removes real content` };
@@ -6700,14 +6928,14 @@ function realpathOf(path) {
6700
6928
  }
6701
6929
  }
6702
6930
  async function predictRootCollisions(ctx, incoming) {
6703
- const root = join4(resolve4(ctx.repoRoot), ".agents", "skills");
6704
- const physicalRoot = join4(realpathOf(ctx.repoRoot) ?? resolve4(ctx.repoRoot), ".agents", "skills");
6931
+ const root = join6(resolve5(ctx.repoRoot), ".agents", "skills");
6932
+ const physicalRoot = join6(realpathOf(ctx.repoRoot) ?? resolve5(ctx.repoRoot), ".agents", "skills");
6705
6933
  const planned = new Map(incoming.map((entry) => [entry.name, entry]));
6706
6934
  const owned = await receiptOwnedLinks(ctx);
6707
6935
  const predictions = [];
6708
6936
  for (const binding of await projectBindings(ctx)) {
6709
- const path = join4(root, binding.name);
6710
- const label = relative3(resolve4(ctx.repoRoot), path);
6937
+ const path = join6(root, binding.name);
6938
+ const label = relative4(resolve5(ctx.repoRoot), path);
6711
6939
  const catalog = new Set([binding.path, realpathOf(binding.path)].filter(Boolean));
6712
6940
  const entry = planned.get(binding.name);
6713
6941
  if (entry) {
@@ -6722,12 +6950,12 @@ async function predictRootCollisions(ctx, incoming) {
6722
6950
  }
6723
6951
  let stat;
6724
6952
  try {
6725
- stat = lstatSync3(path);
6953
+ stat = lstatSync5(path);
6726
6954
  } catch {
6727
6955
  continue;
6728
6956
  }
6729
6957
  if (stat.isSymbolicLink() && catalog.has(realpathOf(path))) continue;
6730
- if (owned.has(join4(physicalRoot, binding.name)) || owned.has(path)) continue;
6958
+ if (owned.has(join6(physicalRoot, binding.name)) || owned.has(path)) continue;
6731
6959
  predictions.push({ path, decision: await classifyRootCollision(ctx, path) });
6732
6960
  }
6733
6961
  return predictions;
@@ -6795,7 +7023,7 @@ async function synchronizeProjectSkills(ctx, extra = {}) {
6795
7023
  const replaceable = predictions.filter(({ decision }) => decision.action === "unlink");
6796
7024
  if (blocking.length) {
6797
7025
  details.push(...blocking.map(({ decision }) => `blocked: ${decision.detail}`));
6798
- if (replaceable.length) details.push(`left in place until the blocked collision(s) above are resolved: ${replaceable.map(({ path }) => relative3(resolve4(ctx.repoRoot), path)).join(", ")}`);
7026
+ if (replaceable.length) details.push(`left in place until the blocked collision(s) above are resolved: ${replaceable.map(({ path }) => relative4(resolve5(ctx.repoRoot), path)).join(", ")}`);
6799
7027
  details.push(migrationGuidance(ctx));
6800
7028
  return { ok: false, changedFiles: [...new Set(changedFiles)], details };
6801
7029
  }
@@ -7495,20 +7723,13 @@ var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
7495
7723
  # TODO: Ensure this works for all levels of nesting.
7496
7724
  # i.e. All linked agent files MUST be siblings at
7497
7725
  # any given level of nesting.`;
7498
- var SKILLEX_TOOL_VERSION = "0.1.2";
7499
- var SKILLS_SYNC_TOOLS = `{ "npm:@delorenj/skillex" = { version = "${SKILLEX_TOOL_VERSION}", allow_low_downloads = true }, node = "24" }`;
7500
7726
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
7501
7727
  patterns = ["AGENTS.md"]
7502
7728
  task = "${LINK_AGENTFILES_TASK}"
7503
7729
 
7504
7730
  ${taskHeader(LINK_AGENTFILES_TASK)}
7505
7731
  description = "Symlink all agent files to AGENTS.md"
7506
- run = ${JSON.stringify(LINK_AGENTFILES_SCRIPT)}
7507
-
7508
- ${taskHeader(SKILLS_SYNC_TASK)}
7509
- description = "Reconcile this project's selected skills"
7510
- tools = ${SKILLS_SYNC_TOOLS}
7511
- run = "skillex sync --scope project --project '{{config_root}}'"`;
7732
+ run = ${JSON.stringify(LINK_AGENTFILES_SCRIPT)}`;
7512
7733
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
7513
7734
  [tasks."version"]
7514
7735
  description = "Print the current version (vX.Y.Z)"
@@ -7539,10 +7760,10 @@ function normalizeNewlines(value) {
7539
7760
  return value.replace(/\r\n/g, "\n");
7540
7761
  }
7541
7762
  function readText(path) {
7542
- return normalizeNewlines(readFileSync3(path, "utf8"));
7763
+ return normalizeNewlines(readFileSync5(path, "utf8"));
7543
7764
  }
7544
7765
  function safeReadText2(path) {
7545
- return existsSync3(path) ? readText(path) : null;
7766
+ return existsSync4(path) ? readText(path) : null;
7546
7767
  }
7547
7768
  function ensureParent(path) {
7548
7769
  mkdirSync3(dirname5(path), { recursive: true });
@@ -7566,16 +7787,16 @@ function titleCaseSlug(slug) {
7566
7787
  return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
7567
7788
  }
7568
7789
  function readSymlinkTarget(path) {
7569
- if (!existsSync3(path)) return null;
7790
+ if (!existsSync4(path)) return null;
7570
7791
  try {
7571
- return readlinkSync3(path);
7792
+ return readlinkSync4(path);
7572
7793
  } catch {
7573
7794
  return null;
7574
7795
  }
7575
7796
  }
7576
7797
  function ensureSymlink(path, target, dryRun) {
7577
- if (existsSync3(path)) {
7578
- const stat = lstatSync4(path);
7798
+ if (existsSync4(path)) {
7799
+ const stat = lstatSync6(path);
7579
7800
  if (stat.isSymbolicLink()) {
7580
7801
  const current = readSymlinkTarget(path);
7581
7802
  if (current === target) return { changed: false };
@@ -7585,18 +7806,18 @@ function ensureSymlink(path, target, dryRun) {
7585
7806
  }
7586
7807
  return { changed: true };
7587
7808
  }
7588
- return { changed: false, blocked: `${relative4(process.cwd(), path) || path} exists and is not a symlink` };
7809
+ return { changed: false, blocked: `${relative5(process.cwd(), path) || path} exists and is not a symlink` };
7589
7810
  }
7590
7811
  if (!dryRun) symlinkSync2(target, path);
7591
7812
  return { changed: true };
7592
7813
  }
7593
7814
  function bootstrapAgentsFile(repoRoot, dryRun) {
7594
- const agentsPath = join5(repoRoot, "AGENTS.md");
7595
- if (existsSync3(agentsPath)) return { changedFiles: [], details: [] };
7815
+ const agentsPath = join7(repoRoot, "AGENTS.md");
7816
+ if (existsSync4(agentsPath)) return { changedFiles: [], details: [] };
7596
7817
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
7597
- const source = join5(repoRoot, file);
7598
- if (!existsSync3(source)) continue;
7599
- const stat = lstatSync4(source);
7818
+ const source = join7(repoRoot, file);
7819
+ if (!existsSync4(source)) continue;
7820
+ const stat = lstatSync6(source);
7600
7821
  if (stat.isSymbolicLink()) continue;
7601
7822
  if (stat.isFile()) {
7602
7823
  if (!dryRun) renameSync2(source, agentsPath);
@@ -7604,9 +7825,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
7604
7825
  }
7605
7826
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
7606
7827
  }
7607
- const readmePath = join5(repoRoot, "README.md");
7608
- if (existsSync3(readmePath)) {
7609
- const stat = lstatSync4(readmePath);
7828
+ const readmePath = join7(repoRoot, "README.md");
7829
+ if (existsSync4(readmePath)) {
7830
+ const stat = lstatSync6(readmePath);
7610
7831
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
7611
7832
  if (!dryRun) copyFileSync(readmePath, agentsPath);
7612
7833
  return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
@@ -7644,12 +7865,12 @@ function yamlGet(text, keyPath) {
7644
7865
  return "";
7645
7866
  }
7646
7867
  function discoverRoles(repoRoot) {
7647
- const rolesDir = join5(repoRoot, "agents", "hermes");
7648
- if (!existsSync3(rolesDir)) return [];
7649
- return readdirSync3(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
7650
- const roleDir = join5(rolesDir, entry.name);
7651
- const roleYamlPath = join5(roleDir, "role.yaml");
7652
- if (!existsSync3(roleYamlPath)) return null;
7868
+ const rolesDir = join7(repoRoot, "agents", "hermes");
7869
+ if (!existsSync4(rolesDir)) return [];
7870
+ return readdirSync4(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
7871
+ const roleDir = join7(rolesDir, entry.name);
7872
+ const roleYamlPath = join7(roleDir, "role.yaml");
7873
+ if (!existsSync4(roleYamlPath)) return null;
7653
7874
  const text = readText(roleYamlPath);
7654
7875
  const runtimeRepoRaw = yamlGet(text, "runtime.github_repo");
7655
7876
  return {
@@ -7681,26 +7902,26 @@ function discoverRoles(repoRoot) {
7681
7902
  }).filter((value) => Boolean(value));
7682
7903
  }
7683
7904
  function templateScript(ctx, name) {
7684
- const source = join5(ctx.pjanglerRoot, ".mise", "scripts", name);
7685
- return existsSync3(source) ? readText(source) : void 0;
7905
+ const source = join7(ctx.pjanglerRoot, ".mise", "scripts", name);
7906
+ return existsSync4(source) ? readText(source) : void 0;
7686
7907
  }
7687
7908
  function templateVersioningScript(ctx) {
7688
7909
  return templateScript(ctx, "versioning.sh");
7689
7910
  }
7690
7911
  function templateLinkAgentfilesScript(ctx) {
7691
- const source = join5(ctx.pjanglerRoot, "templates", "commonproject", "template", ".mise", "scripts", "link-agentfiles.sh");
7692
- return existsSync3(source) ? readText(source) : templateScript(ctx, "link-agentfiles.sh");
7912
+ const source = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", ".mise", "scripts", "link-agentfiles.sh");
7913
+ return existsSync4(source) ? readText(source) : templateScript(ctx, "link-agentfiles.sh");
7693
7914
  }
7694
7915
  function templateMaterializeEnvScript(ctx) {
7695
- const source = join5(ctx.pjanglerRoot, "templates", "commonproject", "template", MATERIALIZE_ENV_SCRIPT_REL);
7696
- return existsSync3(source) ? readText(source) : void 0;
7916
+ const source = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", MATERIALIZE_ENV_SCRIPT_REL);
7917
+ return existsSync4(source) ? readText(source) : void 0;
7697
7918
  }
7698
7919
  function resolveAgentHooksLayer(ctx) {
7699
7920
  const override = process.env.PJ_AGENT_HOOKS_LAYER;
7700
7921
  if (override === "0" || override === "false") return false;
7701
7922
  if (override === "1" || override === "true") return true;
7702
- if (existsSync3(join5(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
7703
- return !existsSync3(join5(ctx.homeDir, ".agents", "hooks"));
7923
+ if (existsSync4(join7(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
7924
+ return !existsSync4(join7(ctx.homeDir, ".agents", "hooks"));
7704
7925
  }
7705
7926
  function evaluateMiseConditionals(template, agentHooksLayer) {
7706
7927
  const out = [];
@@ -7730,22 +7951,22 @@ function renderGeneratedProjectMiseToml(ctx, template) {
7730
7951
  return evaluateMiseConditionals(template, resolveAgentHooksLayer(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
7731
7952
  }
7732
7953
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
7733
- const targetPath = join5(ctx.repoRoot, "mise.toml");
7734
- if (existsSync3(targetPath)) return null;
7735
- const sourcePath = join5(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
7736
- if (!existsSync3(sourcePath)) return null;
7954
+ const targetPath = join7(ctx.repoRoot, "mise.toml");
7955
+ if (existsSync4(targetPath)) return null;
7956
+ const sourcePath = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
7957
+ if (!existsSync4(sourcePath)) return null;
7737
7958
  const rendered = renderGeneratedProjectMiseToml(ctx, readText(sourcePath));
7738
7959
  changedFiles.push(targetPath);
7739
7960
  if (!ctx.dryRun) writeText(targetPath, rendered);
7740
7961
  return rendered;
7741
7962
  }
7742
7963
  function templateCommonProjectText(ctx, rel) {
7743
- const path = join5(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
7744
- return existsSync3(path) ? readText(path) : void 0;
7964
+ const path = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
7965
+ return existsSync4(path) ? readText(path) : void 0;
7745
7966
  }
7746
7967
  function lstatIfPresent(path) {
7747
7968
  try {
7748
- return lstatSync4(path);
7969
+ return lstatSync6(path);
7749
7970
  } catch (error) {
7750
7971
  if (error.code === "ENOENT") return void 0;
7751
7972
  throw error;
@@ -7753,17 +7974,17 @@ function lstatIfPresent(path) {
7753
7974
  }
7754
7975
  function templateVersionFilesConf(ctx, repoRoot) {
7755
7976
  if (isObsidianPluginRepo(ctx, repoRoot)) return OBSIDIAN_VERSION_FILES_CONF;
7756
- const packageJson = join5(repoRoot, "package.json");
7757
- return existsSync3(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
7977
+ const packageJson = join7(repoRoot, "package.json");
7978
+ return existsSync4(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
7758
7979
  }
7759
7980
  var OBSIDIAN_VERSION_FILES_CONF = "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag obsidian\n# Obsidian plugin: bump with `mise run version:bump`, release with `npm version` (bare X.Y.Z tag).\njson package.json\nobsidian manifest.json\n";
7760
7981
  function recordedProjectType(ctx) {
7761
- return record(record(readProjectJson(ctx)?.template).commonproject).project_type;
7982
+ return record2(record2(readProjectJson(ctx)?.template).commonproject).project_type;
7762
7983
  }
7763
7984
  function isObsidianPluginRepo(ctx, repoRoot) {
7764
7985
  const recorded = recordedProjectType(ctx);
7765
7986
  if (recorded !== void 0) return recorded === "obsidian-plugin";
7766
- const manifest = record(tryParseJson(safeReadText2(join5(repoRoot, "manifest.json"))));
7987
+ const manifest = record2(tryParseJson(safeReadText2(join7(repoRoot, "manifest.json"))));
7767
7988
  return typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
7768
7989
  }
7769
7990
  function unsupportedVersionTypes(conf, script) {
@@ -7973,6 +8194,7 @@ function managedLinkHookIssues(parsed) {
7973
8194
  }
7974
8195
  var RETIRED_LINK_TASKS = [
7975
8196
  "skills-sync",
8197
+ SKILLS_SYNC_TASK,
7976
8198
  "skills-provision-packs",
7977
8199
  PROVISION_PACKS_TASK,
7978
8200
  "skills-provision-bmad",
@@ -7986,8 +8208,7 @@ function managedLinkTables() {
7986
8208
  const parsed = parseToml2(LINK_AGENTFILES_WATCH_TASK_BLOCK);
7987
8209
  return {
7988
8210
  watch: { lines: chunks[0], value: parsed.watch_files[0] },
7989
- link: { lines: chunks[1], value: parsed.tasks[LINK_AGENTFILES_TASK] },
7990
- sync: { lines: chunks[2], value: parsed.tasks[SKILLS_SYNC_TASK] }
8211
+ link: { lines: chunks[1], value: parsed.tasks[LINK_AGENTFILES_TASK] }
7991
8212
  };
7992
8213
  }
7993
8214
  function managedWatchPlan(watches) {
@@ -8033,7 +8254,7 @@ function reconcileManagedLinkTables(text) {
8033
8254
  edits.push({ start: span.start, end: span.end, lines: [] });
8034
8255
  continue;
8035
8256
  }
8036
- const table = name === LINK_AGENTFILES_TASK ? managed.link : managed.sync;
8257
+ const table = managed.link;
8037
8258
  found.add(name);
8038
8259
  if (isDeepStrictEqualLoose(parseBody(lines, statements, index), table.value)) continue;
8039
8260
  if (name === LINK_AGENTFILES_TASK && !lines.slice(span.start, span.end).join("\n").includes("link-agentfiles")) {
@@ -8057,8 +8278,7 @@ function reconcileManagedLinkTables(text) {
8057
8278
  });
8058
8279
  const missing = [
8059
8280
  ...plan.managed >= 0 ? [] : [managed.watch.lines],
8060
- ...found.has(LINK_AGENTFILES_TASK) ? [] : [managed.link.lines],
8061
- ...found.has(SKILLS_SYNC_TASK) ? [] : [managed.sync.lines]
8281
+ ...found.has(LINK_AGENTFILES_TASK) ? [] : [managed.link.lines]
8062
8282
  ];
8063
8283
  if (missing.length) {
8064
8284
  const block = missing.flatMap((chunk, index) => index ? ["", ...chunk] : chunk);
@@ -8142,7 +8362,6 @@ function expectedLinkModel(base, required) {
8142
8362
  const tasks = isTable(model.tasks) ? model.tasks : model.tasks = {};
8143
8363
  for (const name of RETIRED_LINK_TASKS) delete tasks[name];
8144
8364
  tasks[LINK_AGENTFILES_TASK] = deepClone(managed.link.value);
8145
- tasks[SKILLS_SYNC_TASK] = deepClone(managed.sync.value);
8146
8365
  const watches = Array.isArray(model.watch_files) ? model.watch_files : [];
8147
8366
  const plan = managedWatchPlan(watches);
8148
8367
  model.watch_files = [
@@ -8226,7 +8445,7 @@ function describeRewrite(before, after) {
8226
8445
  return difference ? `migrate would still rewrite ${difference} in mise.toml` : "migrate would still re-lay out mise.toml (its meaning is unchanged)";
8227
8446
  }
8228
8447
  function readProjectJson(ctx) {
8229
- return tryParseJson(safeReadText2(join5(ctx.repoRoot, ".project.json")));
8448
+ return tryParseJson(safeReadText2(join7(ctx.repoRoot, ".project.json")));
8230
8449
  }
8231
8450
  function readDeclaredAgents(ctx) {
8232
8451
  const project = readProjectJson(ctx);
@@ -8243,8 +8462,8 @@ function readDeclaredAgents(ctx) {
8243
8462
  });
8244
8463
  }
8245
8464
  function readRoleYamlAt(roleDir) {
8246
- const roleYamlPath = join5(roleDir, "role.yaml");
8247
- if (!existsSync3(roleYamlPath)) return null;
8465
+ const roleYamlPath = join7(roleDir, "role.yaml");
8466
+ if (!existsSync4(roleYamlPath)) return null;
8248
8467
  const text = readText(roleYamlPath);
8249
8468
  return {
8250
8469
  role: yamlGet(text, "role"),
@@ -8255,7 +8474,7 @@ function readRoleYamlAt(roleDir) {
8255
8474
  }
8256
8475
  function declaredRoleIsUnprovisioned(repoRoot, roleDir) {
8257
8476
  if (!roleDir) return true;
8258
- return !existsSync3(join5(resolve5(repoRoot, roleDir), "role.yaml"));
8477
+ return !existsSync4(join7(resolve6(repoRoot, roleDir), "role.yaml"));
8259
8478
  }
8260
8479
  function validateDeclaredAgent(ctx, declared) {
8261
8480
  const details = [];
@@ -8263,8 +8482,8 @@ function validateDeclaredAgent(ctx, declared) {
8263
8482
  details.push(`agents.${declared.agentId}.role_dir missing`);
8264
8483
  return { valid: false, details };
8265
8484
  }
8266
- const roleDir = resolve5(ctx.repoRoot, declared.roleDir);
8267
- if (!existsSync3(roleDir)) {
8485
+ const roleDir = resolve6(ctx.repoRoot, declared.roleDir);
8486
+ if (!existsSync4(roleDir)) {
8268
8487
  details.push(`agents.${declared.agentId}.role_dir ${declared.roleDir} does not exist`);
8269
8488
  return { valid: false, roleDir, details };
8270
8489
  }
@@ -8280,13 +8499,13 @@ function validateDeclaredAgent(ctx, declared) {
8280
8499
  details.push(`agents.${declared.agentId} should map to agent_id ${roleYaml.agentId}`);
8281
8500
  }
8282
8501
  if (roleYaml.providerName) {
8283
- const dispatcher = join5(roleDir, ".scripts", "lib", "ticket-provider.sh");
8284
- if (!existsSync3(dispatcher)) {
8285
- details.push(`agents.${declared.agentId} provider dispatcher ${relative4(ctx.repoRoot, dispatcher)} missing`);
8502
+ const dispatcher = join7(roleDir, ".scripts", "lib", "ticket-provider.sh");
8503
+ if (!existsSync4(dispatcher)) {
8504
+ details.push(`agents.${declared.agentId} provider dispatcher ${relative5(ctx.repoRoot, dispatcher)} missing`);
8286
8505
  }
8287
- const provider = join5(roleDir, ".scripts", "providers", `${roleYaml.providerName}.sh`);
8288
- if (!existsSync3(provider)) {
8289
- details.push(`agents.${declared.agentId} provider script ${relative4(ctx.repoRoot, provider)} missing`);
8506
+ const provider = join7(roleDir, ".scripts", "providers", `${roleYaml.providerName}.sh`);
8507
+ if (!existsSync4(provider)) {
8508
+ details.push(`agents.${declared.agentId} provider script ${relative5(ctx.repoRoot, provider)} missing`);
8290
8509
  }
8291
8510
  }
8292
8511
  return { valid: details.length === 0, role: roleYaml.role, agentId: roleYaml.agentId, roleDir, details };
@@ -8312,7 +8531,7 @@ function canonicalProjectJson(ctx) {
8312
8531
  role.agentId || `${slug}-${role.role}`,
8313
8532
  {
8314
8533
  role: role.role,
8315
- role_dir: relative4(ctx.repoRoot, role.roleDir)
8534
+ role_dir: relative5(ctx.repoRoot, role.roleDir)
8316
8535
  }
8317
8536
  ])
8318
8537
  );
@@ -8344,7 +8563,7 @@ function canonicalProjectJson(ctx) {
8344
8563
  }
8345
8564
  agents[validated.agentId] = {
8346
8565
  role: validated.role,
8347
- role_dir: relative4(ctx.repoRoot, validated.roleDir),
8566
+ role_dir: relative5(ctx.repoRoot, validated.roleDir),
8348
8567
  ...declared.extras
8349
8568
  };
8350
8569
  }
@@ -8362,12 +8581,12 @@ function canonicalProjectJson(ctx) {
8362
8581
  };
8363
8582
  }
8364
8583
  function projectJsonFinding(ctx) {
8365
- const projectPath = join5(ctx.repoRoot, ".project.json");
8366
- const planeJsonPath = join5(ctx.repoRoot, ".plane.json");
8584
+ const projectPath = join7(ctx.repoRoot, ".project.json");
8585
+ const planeJsonPath = join7(ctx.repoRoot, ".plane.json");
8367
8586
  const details = [];
8368
8587
  const data = readProjectJson(ctx);
8369
8588
  const roles = discoverRoles(ctx.repoRoot);
8370
- if (!existsSync3(projectPath)) {
8589
+ if (!existsSync4(projectPath)) {
8371
8590
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
8372
8591
  }
8373
8592
  if (!data) {
@@ -8387,8 +8606,8 @@ function projectJsonFinding(ctx) {
8387
8606
  continue;
8388
8607
  }
8389
8608
  if (agent.role !== role.role) details.push(`agents.${role.agentId}.role should be ${role.role}`);
8390
- if (agent.role_dir !== relative4(ctx.repoRoot, role.roleDir)) {
8391
- details.push(`agents.${role.agentId}.role_dir should be ${relative4(ctx.repoRoot, role.roleDir)}`);
8609
+ if (agent.role_dir !== relative5(ctx.repoRoot, role.roleDir)) {
8610
+ details.push(`agents.${role.agentId}.role_dir should be ${relative5(ctx.repoRoot, role.roleDir)}`);
8392
8611
  }
8393
8612
  }
8394
8613
  const declaredAgents = readDeclaredAgents(ctx);
@@ -8416,7 +8635,7 @@ function projectJsonFinding(ctx) {
8416
8635
  if ("reconcile" in automation) {
8417
8636
  details.push("automation.reconcile is vestigial and should be removed; the heartbeat reads role.yaml, never this key");
8418
8637
  }
8419
- if (existsSync3(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
8638
+ if (existsSync4(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
8420
8639
  return {
8421
8640
  id: "sot.project-json",
8422
8641
  title: "Canonical .project.json",
@@ -8434,7 +8653,7 @@ var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
8434
8653
  var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
8435
8654
  var BMAD_INSTALL_TOOLS = SUPPORTED_BMAD_TOOLS;
8436
8655
  function manifestBmadModules(repoRoot) {
8437
- const manifestPath = join5(repoRoot, "_bmad", "_config", "manifest.yaml");
8656
+ const manifestPath = join7(repoRoot, "_bmad", "_config", "manifest.yaml");
8438
8657
  const raw = safeReadText2(manifestPath);
8439
8658
  if (raw === null) return { status: "absent" };
8440
8659
  try {
@@ -8443,14 +8662,16 @@ function manifestBmadModules(repoRoot) {
8443
8662
  return { status: "invalid", error: `${manifestPath} must define a modules array` };
8444
8663
  }
8445
8664
  const declared = [];
8665
+ let hasG33 = false;
8446
8666
  for (const entry of parsed.modules) {
8447
8667
  const name = typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.name : void 0;
8448
8668
  if (typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
8449
8669
  return { status: "invalid", error: `${manifestPath} contains an invalid module entry` };
8450
8670
  }
8451
- if (name !== "core" && name !== "custom") declared.push(name);
8671
+ if (name === "g33") hasG33 = true;
8672
+ if (name !== "core" && name !== "custom" && name !== "g33") declared.push(name);
8452
8673
  }
8453
- return { status: "valid", modules: Array.from(new Set(declared)) };
8674
+ return { status: "valid", modules: Array.from(new Set(declared)), hasG33 };
8454
8675
  } catch (error) {
8455
8676
  return {
8456
8677
  status: "invalid",
@@ -8458,24 +8679,24 @@ function manifestBmadModules(repoRoot) {
8458
8679
  };
8459
8680
  }
8460
8681
  }
8461
- function configuredBmadModules(repoRoot) {
8462
- const raw = safeReadText2(join5(repoRoot, "_bmad", "config.toml"));
8682
+ function configuredBmadModuleSelection(repoRoot) {
8683
+ const raw = safeReadText2(join7(repoRoot, "_bmad", "config.toml"));
8463
8684
  if (raw === null) return void 0;
8464
8685
  const modules = [...raw.matchAll(/^\[modules\.([A-Za-z0-9][A-Za-z0-9_-]*)\]\s*$/gm)].map((match) => match[1]);
8465
- return Array.from(new Set(modules));
8686
+ return { modules: Array.from(new Set(modules)).filter((name) => name !== "g33"), hasG33: modules.includes("g33") };
8466
8687
  }
8467
8688
  function selectedBmadModules(repoRoot) {
8468
8689
  const manifest = manifestBmadModules(repoRoot);
8469
8690
  if (manifest.status === "valid") return manifest.modules;
8470
8691
  if (manifest.status === "invalid") throw new Error(manifest.error);
8471
- return configuredBmadModules(repoRoot) ?? [...DEFAULT_BMAD_MODULES];
8692
+ return configuredBmadModuleSelection(repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
8472
8693
  }
8473
8694
  function requiredBmadSentinels(repoRoot, modules = selectedBmadModules(repoRoot)) {
8474
8695
  return [
8475
- join5("core", "config.yaml"),
8476
- join5("config.toml"),
8477
- join5("_config", "manifest.yaml"),
8478
- ...modules.map((module) => join5(module, "config.yaml"))
8696
+ join7("core", "config.yaml"),
8697
+ join7("config.toml"),
8698
+ join7("_config", "manifest.yaml"),
8699
+ ...modules.map((module) => join7(module, "config.yaml"))
8479
8700
  ];
8480
8701
  }
8481
8702
  function canonicalBmadProjectName(repoRoot) {
@@ -8487,7 +8708,7 @@ function bmadProjectNameIssues(repoRoot) {
8487
8708
  const expected = canonicalBmadProjectName(repoRoot);
8488
8709
  const paths = [];
8489
8710
  const details = [];
8490
- const configToml = join5(repoRoot, "_bmad", "config.toml");
8711
+ const configToml = join7(repoRoot, "_bmad", "config.toml");
8491
8712
  const tomlText = safeReadText2(configToml);
8492
8713
  const tomlMatch = tomlText?.match(/^project_name\s*=\s*"((?:\\.|[^"\\])*)"\s*$/m);
8493
8714
  let tomlName;
@@ -8502,16 +8723,16 @@ function bmadProjectNameIssues(repoRoot) {
8502
8723
  paths.push(configToml);
8503
8724
  details.push(`_bmad/config.toml project_name must be ${JSON.stringify(expected)}`);
8504
8725
  }
8505
- const bmadRoot = join5(repoRoot, "_bmad");
8506
- if (existsSync3(bmadRoot)) {
8726
+ const bmadRoot = join7(repoRoot, "_bmad");
8727
+ if (existsSync4(bmadRoot)) {
8507
8728
  let declared;
8508
8729
  try {
8509
8730
  declared = selectedBmadModules(repoRoot);
8510
8731
  } catch {
8511
- declared = readdirSync3(bmadRoot);
8732
+ declared = readdirSync4(bmadRoot);
8512
8733
  }
8513
8734
  for (const name of new Set(declared)) {
8514
- const configPath = join5(bmadRoot, name, "config.yaml");
8735
+ const configPath = join7(bmadRoot, name, "config.yaml");
8515
8736
  const raw = safeReadText2(configPath);
8516
8737
  if (raw === null) continue;
8517
8738
  let actual;
@@ -8531,7 +8752,7 @@ function bmadProjectNameIssues(repoRoot) {
8531
8752
  function isCurrentActivation(path, activations) {
8532
8753
  if (activations.owned.has(path)) return true;
8533
8754
  try {
8534
- if (activations.owned.has(join5(realpathSync3(dirname5(path)), basename3(path)))) return true;
8755
+ if (activations.owned.has(join7(realpathSync3(dirname5(path)), basename3(path)))) return true;
8535
8756
  return activations.targets.has(realpathSync3(path));
8536
8757
  } catch {
8537
8758
  return false;
@@ -8544,25 +8765,25 @@ async function skillActivations(ctx) {
8544
8765
  function evictLegacyBmadPackState(ctx, changedFiles, activations) {
8545
8766
  const details = [];
8546
8767
  const skillDirs = [
8547
- join5(ctx.repoRoot, ".agents", "skills"),
8548
- ...SUPPORTED_CLI_ROOTS.map((root) => join5(ctx.repoRoot, root, "skills"))
8768
+ join7(ctx.repoRoot, ".agents", "skills"),
8769
+ ...SUPPORTED_CLI_ROOTS.map((root) => join7(ctx.repoRoot, root, "skills"))
8549
8770
  ];
8550
8771
  for (const dir of skillDirs) {
8551
8772
  const dirStat = lstatIfPresent(dir);
8552
8773
  if (!dirStat || dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
8553
8774
  let names;
8554
8775
  try {
8555
- names = readdirSync3(dir);
8776
+ names = readdirSync4(dir);
8556
8777
  } catch {
8557
8778
  continue;
8558
8779
  }
8559
8780
  for (const name of names) {
8560
8781
  if (!name.startsWith(BMAD_SKILL_NAME_PREFIX)) continue;
8561
- const path = join5(dir, name);
8782
+ const path = join7(dir, name);
8562
8783
  if (!lstatIfPresent(path)?.isSymbolicLink()) continue;
8563
8784
  if (isCurrentActivation(path, activations)) continue;
8564
8785
  changedFiles.push(path);
8565
- details.push(`removed retired BMAD pack symlink ${relative4(ctx.repoRoot, path)}`);
8786
+ details.push(`removed retired BMAD pack symlink ${relative5(ctx.repoRoot, path)}`);
8566
8787
  if (!ctx.dryRun) unlinkSync3(path);
8567
8788
  }
8568
8789
  }
@@ -8570,7 +8791,7 @@ function evictLegacyBmadPackState(ctx, changedFiles, activations) {
8570
8791
  }
8571
8792
  function bmadInstallerInvocation(version = BMAD_INSTALLER_VERSION) {
8572
8793
  const explicit = process.env.PJ_BMAD_INSTALLER?.trim();
8573
- if (explicit) return { command: resolve5(explicit), prefixArgs: [] };
8794
+ if (explicit) return { command: resolve6(explicit), prefixArgs: [] };
8574
8795
  return {
8575
8796
  command: "npx",
8576
8797
  prefixArgs: ["-y", `${BMAD_NPM_PACKAGE}@${version}`]
@@ -8597,7 +8818,7 @@ function bmadInstallDisplay(repoRoot, modules = selectedBmadModules(repoRoot), v
8597
8818
  }
8598
8819
  function preflightBmadLifecycle(_ctx) {
8599
8820
  const invocation = bmadInstallerInvocation();
8600
- const probe = spawnSync3(invocation.command, [...invocation.prefixArgs, "--version"], {
8821
+ const probe = spawnSync4(invocation.command, [...invocation.prefixArgs, "--version"], {
8601
8822
  encoding: "utf8",
8602
8823
  timeout: 3e4
8603
8824
  });
@@ -8619,14 +8840,17 @@ ${probe.stderr ?? ""}`;
8619
8840
  }
8620
8841
  return { ok: true };
8621
8842
  }
8622
- function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot), version = BMAD_INSTALLER_VERSION) {
8843
+ async function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot), version = BMAD_INSTALLER_VERSION) {
8844
+ const installedSelection = manifestBmadModules(repoRoot);
8845
+ const preservation = g33BmadUpdateBoundary(repoRoot, installedSelection.status === "valid" && installedSelection.hasG33 || configuredBmadModuleSelection(repoRoot)?.hasG33);
8846
+ if (preservation) return { ok: false, error: preservation.summary, companion: preservation };
8623
8847
  const invocation = bmadInstallerInvocation(version);
8624
- const result2 = spawnSync3(invocation.command, [...invocation.prefixArgs, ...bmadInstallerArgs(repoRoot, modules)], { encoding: "utf8" });
8848
+ const result2 = spawnSync4(invocation.command, [...invocation.prefixArgs, ...bmadInstallerArgs(repoRoot, modules)], { encoding: "utf8" });
8625
8849
  if (result2.status !== 0) {
8626
8850
  const stdoutTail = result2.stdout?.trim().split("\n").slice(-12).join("\n");
8627
8851
  return { ok: false, error: result2.stderr?.trim() || stdoutTail || result2.error?.message || `installer exited ${result2.status}` };
8628
8852
  }
8629
- return { ok: true };
8853
+ return { ok: true, companion: await runG33Companion(repoRoot, false, "bmad-install") };
8630
8854
  }
8631
8855
  function bmadChannelTarget(distTags) {
8632
8856
  const next = distTags?.[BMAD_TARGET_CHANNEL];
@@ -8635,7 +8859,7 @@ function bmadChannelTarget(distTags) {
8635
8859
  return next ? { version: next, channel: BMAD_TARGET_CHANNEL } : void 0;
8636
8860
  }
8637
8861
  function readInstalledBmadVersion(repoRoot) {
8638
- const raw = safeReadText2(join5(repoRoot, "_bmad", "_config", "manifest.yaml"));
8862
+ const raw = safeReadText2(join7(repoRoot, "_bmad", "_config", "manifest.yaml"));
8639
8863
  if (!raw) return void 0;
8640
8864
  try {
8641
8865
  const parsed = YAML2.parse(raw);
@@ -8646,8 +8870,8 @@ function readInstalledBmadVersion(repoRoot) {
8646
8870
  }
8647
8871
  }
8648
8872
  function bmadCachePath(homeDir) {
8649
- const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join5(homeDir, ".cache");
8650
- return join5(cacheRoot, "pjangler", "bmad-dist-tags.json");
8873
+ const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join7(homeDir, ".cache");
8874
+ return join7(cacheRoot, "pjangler", "bmad-dist-tags.json");
8651
8875
  }
8652
8876
  function readBmadDistTagsCache(homeDir) {
8653
8877
  const raw = safeReadText2(bmadCachePath(homeDir));
@@ -8662,7 +8886,7 @@ function readBmadDistTagsCache(homeDir) {
8662
8886
  return void 0;
8663
8887
  }
8664
8888
  function fetchBmadDistTags() {
8665
- const result2 = spawnSync3("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
8889
+ const result2 = spawnSync4("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
8666
8890
  encoding: "utf8",
8667
8891
  timeout: 8e3
8668
8892
  });
@@ -8814,8 +9038,8 @@ var UNSUPPORTED_BMAD_ROOTS = {
8814
9038
  ".zencoder": "zencoder"
8815
9039
  };
8816
9040
  function unsupportedRootAttestation(repoRoot, rootName) {
8817
- const root = join5(repoRoot, rootName);
8818
- const stat = lstatSync4(root);
9041
+ const root = join7(repoRoot, rootName);
9042
+ const stat = lstatSync6(root);
8819
9043
  if (!stat.isDirectory() || stat.isSymbolicLink()) return { safe: false, reason: `${rootName} is not a regular generated directory` };
8820
9044
  const installerTool = UNSUPPORTED_BMAD_ROOTS[rootName];
8821
9045
  if (!installedBmadTools(repoRoot).has(installerTool)) {
@@ -8834,8 +9058,8 @@ var OPTIONAL_TEMPLATE_SCRIPTS = [
8834
9058
  function optionalTemplateScriptIssues(ctx) {
8835
9059
  const issues = [];
8836
9060
  for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
8837
- const path = join5(ctx.repoRoot, rel);
8838
- if (!existsSync3(path)) continue;
9061
+ const path = join7(ctx.repoRoot, rel);
9062
+ if (!existsSync4(path)) continue;
8839
9063
  const expected = templateCommonProjectText(ctx, rel);
8840
9064
  if (expected === void 0) continue;
8841
9065
  if (safeReadText2(path) !== expected) issues.push(`${rel} is present but has drifted from the shipped template`);
@@ -8845,8 +9069,8 @@ function optionalTemplateScriptIssues(ctx) {
8845
9069
  function refreshOptionalTemplateScripts(ctx) {
8846
9070
  const changed = [];
8847
9071
  for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
8848
- const path = join5(ctx.repoRoot, rel);
8849
- if (!existsSync3(path)) continue;
9072
+ const path = join7(ctx.repoRoot, rel);
9073
+ if (!existsSync4(path)) continue;
8850
9074
  const expected = templateCommonProjectText(ctx, rel);
8851
9075
  if (expected === void 0 || safeReadText2(path) === expected) continue;
8852
9076
  changed.push(path);
@@ -8863,14 +9087,14 @@ function createMiseChecks() {
8863
9087
  id: "mise.config-root",
8864
9088
  title: "mise config_root + AGENTS link hooks",
8865
9089
  audit: (ctx) => {
8866
- const misePath = join5(ctx.repoRoot, "mise.toml");
8867
- if (!existsSync3(misePath)) {
9090
+ const misePath = join7(ctx.repoRoot, "mise.toml");
9091
+ if (!existsSync4(misePath)) {
8868
9092
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
8869
9093
  }
8870
9094
  const text = readText(misePath);
8871
9095
  const details = [];
8872
- const linkAgentfilesPath = join5(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
8873
- if (!existsSync3(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
9096
+ const linkAgentfilesPath = join7(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
9097
+ if (!existsSync4(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
8874
9098
  const toml = [];
8875
9099
  const manual = [];
8876
9100
  const parsed = parsedOrUndefined(text);
@@ -8917,10 +9141,10 @@ function createMiseChecks() {
8917
9141
  };
8918
9142
  },
8919
9143
  migrate: (ctx, finding2) => {
8920
- const path = join5(ctx.repoRoot, "mise.toml");
9144
+ const path = join7(ctx.repoRoot, "mise.toml");
8921
9145
  const changedFiles = [];
8922
9146
  const details = [];
8923
- if (!existsSync3(path)) {
9147
+ if (!existsSync4(path)) {
8924
9148
  if (ensureMiseTomlFromTemplate(ctx, changedFiles) === null) {
8925
9149
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
8926
9150
  }
@@ -8936,7 +9160,7 @@ function createMiseChecks() {
8936
9160
  if (!ctx.dryRun) writeText(path, plan.text);
8937
9161
  text = plan.text;
8938
9162
  }
8939
- const linkAgentfilesPath = join5(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
9163
+ const linkAgentfilesPath = join7(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
8940
9164
  const expectedScript = templateLinkAgentfilesScript(ctx);
8941
9165
  if (expectedScript === void 0) {
8942
9166
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/link-agentfiles.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
@@ -8974,13 +9198,13 @@ function createMiseChecks() {
8974
9198
  title: "managed mise versioning block",
8975
9199
  audit: (ctx) => {
8976
9200
  const details = [];
8977
- const misePath = join5(ctx.repoRoot, "mise.toml");
8978
- const versioningPath = join5(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
8979
- const manifestPath = join5(ctx.repoRoot, ".mise", "version-files.conf");
9201
+ const misePath = join7(ctx.repoRoot, "mise.toml");
9202
+ const versioningPath = join7(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
9203
+ const manifestPath = join7(ctx.repoRoot, ".mise", "version-files.conf");
8980
9204
  const text = safeReadText2(misePath);
8981
9205
  if (!text?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
8982
- if (!existsSync3(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
8983
- if (!existsSync3(manifestPath)) details.push(".mise/version-files.conf missing");
9206
+ if (!existsSync4(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
9207
+ if (!existsSync4(manifestPath)) details.push(".mise/version-files.conf missing");
8984
9208
  const conf = safeReadText2(manifestPath);
8985
9209
  const script = safeReadText2(versioningPath);
8986
9210
  if (conf !== null && script !== null) {
@@ -9002,8 +9226,8 @@ function createMiseChecks() {
9002
9226
  migrate: (ctx, finding2) => {
9003
9227
  const changedFiles = [];
9004
9228
  const details = [];
9005
- const misePath = join5(ctx.repoRoot, "mise.toml");
9006
- if (!existsSync3(misePath)) {
9229
+ const misePath = join7(ctx.repoRoot, "mise.toml");
9230
+ if (!existsSync4(misePath)) {
9007
9231
  if (ensureMiseTomlFromTemplate(ctx, changedFiles) === null) {
9008
9232
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
9009
9233
  }
@@ -9020,7 +9244,7 @@ function createMiseChecks() {
9020
9244
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
9021
9245
  if (!ctx.dryRun) writeText(misePath, versioningPlan.text);
9022
9246
  }
9023
- const versioningPath = join5(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
9247
+ const versioningPath = join7(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
9024
9248
  const expectedScript = templateVersioningScript(ctx);
9025
9249
  if (expectedScript === void 0) {
9026
9250
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/versioning.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
@@ -9032,7 +9256,7 @@ function createMiseChecks() {
9032
9256
  chmodSync(versioningPath, 493);
9033
9257
  }
9034
9258
  }
9035
- const manifestPath = join5(ctx.repoRoot, ".mise", "version-files.conf");
9259
+ const manifestPath = join7(ctx.repoRoot, ".mise", "version-files.conf");
9036
9260
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
9037
9261
  if (safeReadText2(manifestPath) !== expectedManifest) {
9038
9262
  changedFiles.push(manifestPath);
@@ -9062,7 +9286,7 @@ function planVersioningBlock(original) {
9062
9286
  let replaced = [...VERSIONING_TASK_NAMES];
9063
9287
  try {
9064
9288
  if (oldBlock) {
9065
- replaced = Object.keys(record(record(parseToml2(oldBlock)).tasks));
9289
+ replaced = Object.keys(record2(record2(parseToml2(oldBlock)).tasks));
9066
9290
  text = text.replace(blockPattern, VERSIONING_BLOCK);
9067
9291
  } else {
9068
9292
  const statements = scanToml(text);
@@ -9085,7 +9309,7 @@ function planVersioningBlock(original) {
9085
9309
  const verdict = checkRewrite(expected, text);
9086
9310
  return verdict.ok ? { text } : { text: original, refused: verdict.reason };
9087
9311
  }
9088
- var record = (value) => isTable(value) ? value : {};
9312
+ var record2 = (value) => isTable(value) ? value : {};
9089
9313
  var RETIRED_SKILL_SCRIPTS = [
9090
9314
  SYNC_SKILLS_SCRIPT_REL,
9091
9315
  PROVISION_PACKS_SCRIPT_REL,
@@ -9094,39 +9318,30 @@ var RETIRED_SKILL_SCRIPTS = [
9094
9318
  ".mise/scripts/unlink-project-skills-from-clis.sh"
9095
9319
  ];
9096
9320
  function retiredSkillsScripts(ctx) {
9097
- return RETIRED_SKILL_SCRIPTS.map((path) => join5(ctx.repoRoot, path)).filter((path) => Boolean(lstatIfPresent(path)));
9321
+ return RETIRED_SKILL_SCRIPTS.map((path) => join7(ctx.repoRoot, path)).filter((path) => Boolean(lstatIfPresent(path)));
9098
9322
  }
9099
9323
  function skillsWiringFindings(text) {
9100
- if (text === null) return { fixable: ["mise.toml is missing; add the explicit skills:sync task"], manual: [] };
9324
+ if (text === null) return { fixable: [], manual: [] };
9101
9325
  const fixable = [];
9102
9326
  const manual = [];
9103
9327
  let parsed = parsedOrUndefined(text);
9104
9328
  if (!parsed) {
9105
9329
  const baseline = parseBaseline(text);
9106
- if ("refused" in baseline) return { fixable: [], manual: ["mise.toml is invalid TOML; repair it before running skills:sync"] };
9330
+ if ("refused" in baseline) return { fixable: [], manual: ["mise.toml is invalid TOML; repair it before pjangler can retire skill tasks"] };
9107
9331
  parsed = baseline.parsed;
9108
9332
  fixable.push("mise.toml repeats the close of a [hooks] array (an earlier pjangler run wrote it); migrate repairs it");
9109
9333
  }
9110
- const record2 = (value) => isTable(value) ? value : {};
9111
- const tasks = record2(parsed.tasks), task = record2(tasks[SKILLS_SYNC_TASK]);
9112
- const run = Array.isArray(task.run) && task.run.length === 1 ? task.run[0] : task.run;
9113
- if (typeof run !== "string" || !/^skillex\s+sync\s+--scope\s+project\s+--project\s+(['"])\{\{config_root\}\}\1\s*$/.test(run)) {
9114
- fixable.push("skills:sync must explicitly target {{config_root}} with the Node CLI");
9115
- }
9116
- const pin = record2(task.tools)["npm:@delorenj/skillex"];
9117
- const pinned = typeof pin === "string" ? pin : record2(pin).version;
9118
- if (pinned !== SKILLEX_TOOL_VERSION) {
9119
- fixable.push(`skills:sync must pin @delorenj/skillex ${SKILLEX_TOOL_VERSION} in its own tools table`);
9120
- } else if (typeof pin === "string") {
9121
- fixable.push(`skills:sync pins @delorenj/skillex as the plain string "${pin}", which mise 2026.9 refuses to install inside its 30-day minimumPackageAge; pin { version = "${SKILLEX_TOOL_VERSION}", allow_low_downloads = true }`);
9122
- } else if (record2(pin).allow_low_downloads !== true) {
9123
- fixable.push(`skills:sync must approve @delorenj/skillex ${SKILLEX_TOOL_VERSION} with allow_low_downloads = true (mise 2026.9 refuses to install it otherwise)`);
9334
+ const record3 = (value) => isTable(value) ? value : {};
9335
+ const tasks = record3(parsed.tasks);
9336
+ if (tasks[SKILLS_SYNC_TASK] !== void 0) {
9337
+ fixable.push(`Retire the ${SKILLS_SYNC_TASK} mise task; invoke skillex directly (pj skills sync --scope project --project .)`);
9124
9338
  }
9125
9339
  const retired = /sync-skills\.py|provision-(?:packs|bmad-skills)\.py|skills[:\-]provision/;
9126
9340
  const renames = new Map(RETIRED_TASK_RENAMES);
9127
- const replaced = /* @__PURE__ */ new Set([...RETIRED_LINK_TASKS, LINK_AGENTFILES_TASK, SKILLS_SYNC_TASK]);
9341
+ const replaced = /* @__PURE__ */ new Set([...RETIRED_LINK_TASKS, LINK_AGENTFILES_TASK]);
9128
9342
  for (const [name, entry] of Object.entries(tasks)) {
9129
- if (!retired.test(name) && !retired.test(JSON.stringify(record2(entry).run ?? ""))) continue;
9343
+ if (name === SKILLS_SYNC_TASK) continue;
9344
+ if (!retired.test(name) && !retired.test(JSON.stringify(record3(entry).run ?? ""))) continue;
9130
9345
  if (replaced.has(renames.get(name) ?? name)) {
9131
9346
  if (!fixable.includes("Retire Python skill sync/provision tasks from mise.toml")) fixable.push("Retire Python skill sync/provision tasks from mise.toml");
9132
9347
  } else {
@@ -9140,7 +9355,7 @@ function skillsWiringFindings(text) {
9140
9355
  }
9141
9356
  manual.push(...decisions.manual.filter((entry) => SKILL_WRITER_MENTION.test(entry.command)).map(manualHookLine));
9142
9357
  const watches = Array.isArray(parsed.watch_files) ? parsed.watch_files : [];
9143
- if (managedWatchPlan(watches).skills.length) fixable.push("Remove the skills watch hook; run skills:sync explicitly");
9358
+ if (managedWatchPlan(watches).skills.length) fixable.push("Remove the skills watch hook; run pj skills sync explicitly, never automatically");
9144
9359
  return { fixable, manual };
9145
9360
  }
9146
9361
  function skillsWiringIssues(text) {
@@ -9154,15 +9369,16 @@ function createAgentHooksChecks() {
9154
9369
  title: "Skillex project skills",
9155
9370
  audit: async (ctx) => {
9156
9371
  const finding2 = await auditProjectSkills(ctx);
9157
- const mise = safeReadText2(join5(ctx.repoRoot, "mise.toml"));
9372
+ const mise = safeReadText2(join7(ctx.repoRoot, "mise.toml"));
9158
9373
  const wiring = skillsWiringIssues(mise);
9159
9374
  const retired = retiredSkillsScripts(ctx);
9160
9375
  const unsafe = retired.filter((path) => lstatIfPresent(path)?.isDirectory());
9161
9376
  const details = [...finding2.details, ...wiring, ...retired.map((path) => unsafe.includes(path) ? `Inspect and preserve retired script directory: ${path}` : `${path} is a retired skill writer`)];
9377
+ const miseDrift = mise !== null && upsertLinkAgentfilesBlock(mise, ctx) !== mise;
9162
9378
  return {
9163
9379
  ...finding2,
9164
9380
  status: wiring.length || retired.length ? "fail" : finding2.status,
9165
- fixable: unsafe.length ? false : finding2.fixable || wiring.length > 0 || retired.length > 0,
9381
+ fixable: unsafe.length ? false : finding2.fixable || wiring.length > 0 || retired.length > 0 || miseDrift,
9166
9382
  summary: details.length ? `${details.length} Skillex finding(s)` : finding2.summary,
9167
9383
  details
9168
9384
  };
@@ -9171,7 +9387,7 @@ function createAgentHooksChecks() {
9171
9387
  const changedFiles = [];
9172
9388
  const details = [];
9173
9389
  const result2 = (status, summary) => ({ id: finding2.id, title: finding2.title, status, summary, changedFiles: [...new Set(changedFiles)], details });
9174
- let currentMise = safeReadText2(join5(ctx.repoRoot, "mise.toml"));
9390
+ let currentMise = safeReadText2(join7(ctx.repoRoot, "mise.toml"));
9175
9391
  if (currentMise === null) {
9176
9392
  const template = templateCommonProjectText(ctx, "mise.toml.jinja");
9177
9393
  if (template === void 0) return result2("blocked", "Generated-project mise template is unavailable");
@@ -9188,7 +9404,7 @@ function createAgentHooksChecks() {
9188
9404
  changedFiles.push(path);
9189
9405
  if (!ctx.dryRun) unlinkSync3(path);
9190
9406
  }
9191
- const misePath = join5(ctx.repoRoot, "mise.toml");
9407
+ const misePath = join7(ctx.repoRoot, "mise.toml");
9192
9408
  const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
9193
9409
  if (safeReadText2(misePath) !== nextMise) {
9194
9410
  changedFiles.push(misePath);
@@ -9207,16 +9423,16 @@ function createAgentHooksChecks() {
9207
9423
  if (!reconciled.ok || !roots.ok) {
9208
9424
  return result2(changedFiles.length ? "partial" : "blocked", "Skillex requires attention before project migration");
9209
9425
  }
9210
- return changedFiles.length ? result2("applied", "Project skills use the Node core and explicit sync task") : result2("noop", "No changes required");
9426
+ return changedFiles.length ? result2("applied", "Project skills use the Node core and direct invocation") : result2("noop", "No changes required");
9211
9427
  }
9212
9428
  },
9213
9429
  {
9214
9430
  id: "sot.agent-symlinks",
9215
9431
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
9216
9432
  audit: (ctx) => {
9217
- const agentsPath = join5(ctx.repoRoot, "AGENTS.md");
9218
- if (!existsSync3(agentsPath)) {
9219
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync3(join5(ctx.repoRoot, file)));
9433
+ const agentsPath = join7(ctx.repoRoot, "AGENTS.md");
9434
+ if (!existsSync4(agentsPath)) {
9435
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync4(join7(ctx.repoRoot, file)));
9220
9436
  if (fallbackSources.length === 0) {
9221
9437
  return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
9222
9438
  }
@@ -9231,7 +9447,7 @@ function createAgentHooksChecks() {
9231
9447
  }
9232
9448
  const details = [];
9233
9449
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
9234
- const full = join5(ctx.repoRoot, file);
9450
+ const full = join7(ctx.repoRoot, file);
9235
9451
  const target = readSymlinkTarget(full);
9236
9452
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
9237
9453
  }
@@ -9255,7 +9471,7 @@ function createAgentHooksChecks() {
9255
9471
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
9256
9472
  }
9257
9473
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
9258
- const full = join5(ctx.repoRoot, file);
9474
+ const full = join7(ctx.repoRoot, file);
9259
9475
  const result2 = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
9260
9476
  if (result2.blocked) blockedDetails.push(result2.blocked);
9261
9477
  if (result2.changed) changedFiles.push(full);
@@ -9282,7 +9498,7 @@ function createProjectJsonChecks() {
9282
9498
  const changedFiles = [];
9283
9499
  const blockedDetails = [];
9284
9500
  const droppedDetails = [];
9285
- const path = join5(ctx.repoRoot, ".project.json");
9501
+ const path = join7(ctx.repoRoot, ".project.json");
9286
9502
  const existing = readProjectJson(ctx) ?? {};
9287
9503
  const canonical = canonicalProjectJson(ctx);
9288
9504
  const preservedDetails = [];
@@ -9314,11 +9530,11 @@ function createProjectJsonChecks() {
9314
9530
  changedFiles.push(path);
9315
9531
  if (!ctx.dryRun) writeText(path, expected);
9316
9532
  }
9317
- const planeJson = join5(ctx.repoRoot, ".plane.json");
9318
- if (existsSync3(planeJson)) {
9533
+ const planeJson = join7(ctx.repoRoot, ".plane.json");
9534
+ if (existsSync4(planeJson)) {
9319
9535
  const backup = `${planeJson}.migrated-backup`;
9320
- if (existsSync3(backup)) {
9321
- blockedDetails.push(`cannot back up .plane.json because ${relative4(ctx.repoRoot, backup)} already exists`);
9536
+ if (existsSync4(backup)) {
9537
+ blockedDetails.push(`cannot back up .plane.json because ${relative5(ctx.repoRoot, backup)} already exists`);
9322
9538
  } else {
9323
9539
  changedFiles.push(backup);
9324
9540
  if (!ctx.dryRun) renameSync2(planeJson, backup);
@@ -9346,10 +9562,10 @@ function createMiseOpInjectChecks() {
9346
9562
  const details = [];
9347
9563
  let envOpNeedsHands = false;
9348
9564
  const optedOut = envMaterializationOptedOut(ctx);
9349
- const envOpPath = join5(ctx.repoRoot, ".env.op");
9350
- const envOpExists = existsSync3(envOpPath);
9565
+ const envOpPath = join7(ctx.repoRoot, ".env.op");
9566
+ const envOpExists = existsSync4(envOpPath);
9351
9567
  const envOp = envOpExists ? readText(envOpPath) : void 0;
9352
- const gitignore = safeReadText2(join5(ctx.repoRoot, ".gitignore"));
9568
+ const gitignore = safeReadText2(join7(ctx.repoRoot, ".gitignore"));
9353
9569
  if (!envOpExists) {
9354
9570
  details.push(".env.op missing");
9355
9571
  } else if (!envOp?.trim()) {
@@ -9369,7 +9585,7 @@ function createMiseOpInjectChecks() {
9369
9585
  if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
9370
9586
  if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
9371
9587
  if (!gitignore?.includes("!.env.op")) details.push(".gitignore should unignore .env.op");
9372
- const misePath = join5(ctx.repoRoot, "mise.toml");
9588
+ const misePath = join7(ctx.repoRoot, "mise.toml");
9373
9589
  const miseText = safeReadText2(misePath);
9374
9590
  if (!miseText) {
9375
9591
  details.push("mise.toml missing for .env materialization hook");
@@ -9392,15 +9608,15 @@ function createMiseOpInjectChecks() {
9392
9608
  else if (plan.text !== miseText && !mise.length) mise.push(describeRewrite(miseText, plan.text));
9393
9609
  details.push(...mise);
9394
9610
  }
9395
- const materializePath = join5(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
9611
+ const materializePath = join7(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
9396
9612
  const expectedMaterializer = templateMaterializeEnvScript(ctx);
9397
9613
  if (optedOut) {
9398
- if (existsSync3(materializePath)) details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is present, but .project.json declares secrets.materialize_env: false`);
9614
+ if (existsSync4(materializePath)) details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is present, but .project.json declares secrets.materialize_env: false`);
9399
9615
  } else if (!expectedMaterializer) {
9400
9616
  details.push("pjangler package is missing the managed materialize-env.sh source");
9401
9617
  } else if (safeReadText2(materializePath) !== expectedMaterializer) {
9402
9618
  details.push(`${MATERIALIZE_ENV_SCRIPT_REL} missing or drifted`);
9403
- } else if ((lstatSync4(materializePath).mode & 73) === 0) {
9619
+ } else if ((lstatSync6(materializePath).mode & 73) === 0) {
9404
9620
  details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is not executable`);
9405
9621
  }
9406
9622
  return {
@@ -9416,13 +9632,13 @@ function createMiseOpInjectChecks() {
9416
9632
  const changedFiles = [];
9417
9633
  const details = [];
9418
9634
  let envOpNeedsHands = false;
9419
- const envOpPath = join5(ctx.repoRoot, ".env.op");
9420
- const canonicalEnvOpPath = join5(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op");
9421
- if (!existsSync3(canonicalEnvOpPath)) {
9635
+ const envOpPath = join7(ctx.repoRoot, ".env.op");
9636
+ const canonicalEnvOpPath = join7(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op");
9637
+ if (!existsSync4(canonicalEnvOpPath)) {
9422
9638
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing the neutral .env.op template", changedFiles: [], details: [] };
9423
9639
  }
9424
9640
  const canonicalEnvOp = readText(canonicalEnvOpPath);
9425
- if (!existsSync3(envOpPath) || !readText(envOpPath).trim()) {
9641
+ if (!existsSync4(envOpPath) || !readText(envOpPath).trim()) {
9426
9642
  changedFiles.push(envOpPath);
9427
9643
  if (!ctx.dryRun) writeText(envOpPath, canonicalEnvOp);
9428
9644
  } else {
@@ -9446,7 +9662,7 @@ function createMiseOpInjectChecks() {
9446
9662
  }
9447
9663
  }
9448
9664
  }
9449
- const gitignorePath = join5(ctx.repoRoot, ".gitignore");
9665
+ const gitignorePath = join7(ctx.repoRoot, ".gitignore");
9450
9666
  const gitignore = safeReadText2(gitignorePath) ?? "";
9451
9667
  const requiredBlock = `# Secrets \u2014 .env is materialized from .env.op by \`op inject\` on mise enter,
9452
9668
  # staged through a mktemp file and moved into place only on success.
@@ -9459,7 +9675,7 @@ function createMiseOpInjectChecks() {
9459
9675
  changedFiles.push(gitignorePath);
9460
9676
  if (!ctx.dryRun) writeText(gitignorePath, `${gitignore.replace(/\s*$/, "")}${gitignore.trim() ? "\n\n" : ""}${requiredBlock}`);
9461
9677
  }
9462
- const misePath = join5(ctx.repoRoot, "mise.toml");
9678
+ const misePath = join7(ctx.repoRoot, "mise.toml");
9463
9679
  let currentMise = safeReadText2(misePath);
9464
9680
  if (currentMise === null) {
9465
9681
  const initialized = ensureMiseTomlFromTemplate(ctx, changedFiles);
@@ -9476,16 +9692,16 @@ function createMiseOpInjectChecks() {
9476
9692
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
9477
9693
  if (!ctx.dryRun) writeText(misePath, opPlan.text);
9478
9694
  }
9479
- const materializePath = join5(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
9695
+ const materializePath = join7(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
9480
9696
  const expectedMaterializer = templateMaterializeEnvScript(ctx);
9481
9697
  if (optedOut) {
9482
- if (existsSync3(materializePath)) {
9698
+ if (existsSync4(materializePath)) {
9483
9699
  changedFiles.push(materializePath);
9484
9700
  if (!ctx.dryRun) rmSync2(materializePath);
9485
9701
  }
9486
9702
  } else if (!expectedMaterializer) {
9487
9703
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing materialize-env.sh", changedFiles: [], details: [] };
9488
- } else if (safeReadText2(materializePath) !== expectedMaterializer || existsSync3(materializePath) && (lstatSync4(materializePath).mode & 73) === 0) {
9704
+ } else if (safeReadText2(materializePath) !== expectedMaterializer || existsSync4(materializePath) && (lstatSync6(materializePath).mode & 73) === 0) {
9489
9705
  changedFiles.push(materializePath);
9490
9706
  if (!ctx.dryRun) {
9491
9707
  writeText(materializePath, expectedMaterializer);
@@ -9547,7 +9763,7 @@ function createProjectProvenanceChecks() {
9547
9763
  title: ".copier-answers.yml provenance + drift report",
9548
9764
  audit: (ctx) => {
9549
9765
  const details = [];
9550
- const path = join5(ctx.repoRoot, ".copier-answers.yml");
9766
+ const path = join7(ctx.repoRoot, ".copier-answers.yml");
9551
9767
  const text = safeReadText2(path);
9552
9768
  const project = readProjectJson(ctx);
9553
9769
  if (!text) {
@@ -9575,11 +9791,11 @@ function createProjectProvenanceChecks() {
9575
9791
  migrate: (ctx, finding2) => {
9576
9792
  const changedFiles = [];
9577
9793
  const project = canonicalProjectJson(ctx);
9578
- const path = join5(ctx.repoRoot, ".copier-answers.yml");
9794
+ const path = join7(ctx.repoRoot, ".copier-answers.yml");
9579
9795
  const current = safeReadText2(path);
9580
9796
  const existing = current ? YAML2.parseDocument(current) : void 0;
9581
9797
  const document = existing && !existing.errors.length && YAML2.isMap(existing.contents) ? existing : new YAML2.Document({});
9582
- document.set("_src_path", join5(ctx.pjanglerRoot, "templates", "commonproject"));
9798
+ document.set("_src_path", join7(ctx.pjanglerRoot, "templates", "commonproject"));
9583
9799
  document.set("project_description", String(project.project_description));
9584
9800
  document.set("project_name", String(project.project_name));
9585
9801
  document.set("ticket_provider", String(project.ticket_provider?.type ?? "plane"));
@@ -9604,9 +9820,9 @@ ${body}`;
9604
9820
  }
9605
9821
  function supportedCliProjectionIssues(repoRoot, plan) {
9606
9822
  const issues = [];
9607
- const managedSkills = join5(repoRoot, ".agents", "skills");
9823
+ const managedSkills = join7(repoRoot, ".agents", "skills");
9608
9824
  for (const rootName of SUPPORTED_CLI_ROOTS) {
9609
- const root = join5(repoRoot, rootName);
9825
+ const root = join7(repoRoot, rootName);
9610
9826
  const rootStat = lstatIfPresent(root);
9611
9827
  if (!rootStat) {
9612
9828
  issues.push(`${rootName} missing`);
@@ -9616,7 +9832,7 @@ function supportedCliProjectionIssues(repoRoot, plan) {
9616
9832
  issues.push(`${rootName} must be a real configuration directory`);
9617
9833
  continue;
9618
9834
  }
9619
- const skills = join5(root, "skills");
9835
+ const skills = join7(root, "skills");
9620
9836
  const skillsStat = lstatIfPresent(skills);
9621
9837
  if (!skillsStat) {
9622
9838
  issues.push(`${rootName}/skills missing`);
@@ -9625,7 +9841,7 @@ function supportedCliProjectionIssues(repoRoot, plan) {
9625
9841
  let projectedSkills = skills;
9626
9842
  if (skillsStat.isSymbolicLink()) {
9627
9843
  try {
9628
- readlinkSync3(skills);
9844
+ readlinkSync4(skills);
9629
9845
  } catch {
9630
9846
  issues.push(`${rootName}/skills is an unreadable symlink`);
9631
9847
  continue;
@@ -9653,9 +9869,9 @@ function supportedCliProjectionIssues(repoRoot, plan) {
9653
9869
  issues.push(`${rootName}/skills is not a directory`);
9654
9870
  continue;
9655
9871
  }
9656
- const hasGeneratedSkill = existsSync3(projectedSkills) && readdirSync3(projectedSkills).some((name) => {
9657
- const skillFile = join5(projectedSkills, name, "SKILL.md");
9658
- return existsSync3(skillFile) && lstatSync4(skillFile).isFile();
9872
+ const hasGeneratedSkill = existsSync4(projectedSkills) && readdirSync4(projectedSkills).some((name) => {
9873
+ const skillFile = join7(projectedSkills, name, "SKILL.md");
9874
+ return existsSync4(skillFile) && lstatSync6(skillFile).isFile();
9659
9875
  });
9660
9876
  if (!hasGeneratedSkill) issues.push(`${rootName}/skills contains no BMAD skill configuration`);
9661
9877
  }
@@ -9681,7 +9897,7 @@ var LEGACY_CLI_GITIGNORE_LINES = /* @__PURE__ */ new Set([
9681
9897
  "# some CLIs get a real directory here and some get a symlink."
9682
9898
  ]);
9683
9899
  function supportedCliGitignoreIssues(repoRoot) {
9684
- const lines = (safeReadText2(join5(repoRoot, ".gitignore")) ?? "").split(/\r?\n/);
9900
+ const lines = (safeReadText2(join7(repoRoot, ".gitignore")) ?? "").split(/\r?\n/);
9685
9901
  const legacy = lines.filter((line) => LEGACY_CLI_GITIGNORE_LINES.has(line.trim()));
9686
9902
  return [
9687
9903
  ...legacy.length ? [`.gitignore contains ${legacy.length} legacy client-root override line(s); only .agents is canonical`] : [],
@@ -9689,7 +9905,7 @@ function supportedCliGitignoreIssues(repoRoot) {
9689
9905
  ];
9690
9906
  }
9691
9907
  function ensureSupportedCliGitignore(ctx) {
9692
- const path = join5(ctx.repoRoot, ".gitignore");
9908
+ const path = join7(ctx.repoRoot, ".gitignore");
9693
9909
  const current = safeReadText2(path) ?? "";
9694
9910
  const kept = current.split(/\r?\n/).filter((line) => {
9695
9911
  const trimmed2 = line.trim();
@@ -9727,10 +9943,10 @@ function createBmadChecks() {
9727
9943
  fixable: false
9728
9944
  };
9729
9945
  }
9730
- const targetRoot = join5(ctx.repoRoot, "_bmad");
9731
- const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
9946
+ const targetRoot = join7(ctx.repoRoot, "_bmad");
9947
+ const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
9732
9948
  const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
9733
- const missing = sentinels.filter((file) => !existsSync3(join5(targetRoot, file)));
9949
+ const missing = sentinels.filter((file) => !existsSync4(join7(targetRoot, file)));
9734
9950
  const projectNameIssues = bmadProjectNameIssues(ctx.repoRoot);
9735
9951
  const details = [
9736
9952
  ...missing.map((file) => `_bmad/${file}`),
@@ -9758,10 +9974,21 @@ function createBmadChecks() {
9758
9974
  details: [manifestSelection.error]
9759
9975
  };
9760
9976
  }
9761
- const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
9977
+ const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
9978
+ const preservation = g33BmadUpdateBoundary(ctx.repoRoot, manifestSelection.status === "valid" && manifestSelection.hasG33 || configuredBmadModuleSelection(ctx.repoRoot)?.hasG33);
9979
+ if (preservation) return {
9980
+ id: finding2.id,
9981
+ title: finding2.title,
9982
+ status: "blocked",
9983
+ summary: preservation.summary,
9984
+ changedFiles: [],
9985
+ details: companionDetails(preservation),
9986
+ bmadCompanion: preservation
9987
+ };
9762
9988
  if (ctx.dryRun) {
9763
9989
  const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
9764
- changedFiles.push(...sentinels.map((file) => join5(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync3(path)));
9990
+ const companion = await runG33Companion(ctx.repoRoot, true, "bmad-install");
9991
+ changedFiles.push(...sentinels.map((file) => join7(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync4(path)));
9765
9992
  changedFiles.push(...bmadProjectNameIssues(ctx.repoRoot).paths);
9766
9993
  return {
9767
9994
  id: finding2.id,
@@ -9770,16 +9997,18 @@ function createBmadChecks() {
9770
9997
  summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
9771
9998
  changedFiles,
9772
9999
  details: [
9773
- `Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules)}`
9774
- ]
10000
+ `Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules)}`,
10001
+ ...companionDetails(companion)
10002
+ ],
10003
+ bmadCompanion: companion
9775
10004
  };
9776
10005
  }
9777
10006
  const expectedChangedPaths = [
9778
- ...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) => join5(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync3(path)),
10007
+ ...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) => join7(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync4(path)),
9779
10008
  ...bmadProjectNameIssues(ctx.repoRoot).paths
9780
10009
  ];
9781
10010
  const evicted = evictLegacyBmadPackState(ctx, changedFiles, await skillActivations(ctx));
9782
- const install = runBmadInstall(ctx.repoRoot, selectedModules);
10011
+ const install = await runBmadInstall(ctx.repoRoot, selectedModules);
9783
10012
  if (!install.ok) {
9784
10013
  return {
9785
10014
  id: finding2.id,
@@ -9790,14 +10019,15 @@ function createBmadChecks() {
9790
10019
  details: [...evicted, install.error ?? "Unknown error"]
9791
10020
  };
9792
10021
  }
9793
- changedFiles.push(...expectedChangedPaths.filter(existsSync3));
10022
+ changedFiles.push(...expectedChangedPaths.filter(existsSync4), ...install.companion?.changedFiles ?? []);
9794
10023
  return {
9795
10024
  id: finding2.id,
9796
10025
  title: finding2.title,
9797
10026
  status: changedFiles.length ? "applied" : "noop",
9798
10027
  summary: changedFiles.length ? "Installed BMAD scaffold via bmad-method" : "No changes required",
9799
10028
  changedFiles,
9800
- details: evicted
10029
+ details: [...evicted, ...install.companion ? companionDetails(install.companion) : []],
10030
+ bmadCompanion: install.companion
9801
10031
  };
9802
10032
  }
9803
10033
  },
@@ -9811,7 +10041,7 @@ function createBmadChecks() {
9811
10041
  id: "bmad.version",
9812
10042
  title: "BMAD version currency",
9813
10043
  status: "skip",
9814
- summary: existsSync3(join5(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
10044
+ summary: existsSync4(join7(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
9815
10045
  details: [],
9816
10046
  fixable: false
9817
10047
  };
@@ -9899,7 +10129,7 @@ function createBmadChecks() {
9899
10129
  }
9900
10130
  const installed = readInstalledBmadVersion(ctx.repoRoot);
9901
10131
  const available = ctx.bmadVersionPin?.trim() ?? bmadChannelTarget(resolveBmadDistTags(ctx.homeDir)?.distTags)?.version;
9902
- const manifestPath = join5(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
10132
+ const manifestPath = join7(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
9903
10133
  const manifestSelection = manifestBmadModules(ctx.repoRoot);
9904
10134
  if (manifestSelection.status === "invalid") {
9905
10135
  return {
@@ -9911,8 +10141,19 @@ function createBmadChecks() {
9911
10141
  details: [manifestSelection.error]
9912
10142
  };
9913
10143
  }
9914
- const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
10144
+ const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
10145
+ const preservation = g33BmadUpdateBoundary(ctx.repoRoot, manifestSelection.status === "valid" && manifestSelection.hasG33 || configuredBmadModuleSelection(ctx.repoRoot)?.hasG33);
10146
+ if (preservation) return {
10147
+ id: finding2.id,
10148
+ title: finding2.title,
10149
+ status: "blocked",
10150
+ summary: preservation.summary,
10151
+ changedFiles: [],
10152
+ details: companionDetails(preservation),
10153
+ bmadCompanion: preservation
10154
+ };
9915
10155
  if (ctx.dryRun) {
10156
+ const companion = await runG33Companion(ctx.repoRoot, true, "bmad-install");
9916
10157
  return {
9917
10158
  id: finding2.id,
9918
10159
  title: finding2.title,
@@ -9920,11 +10161,13 @@ function createBmadChecks() {
9920
10161
  summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
9921
10162
  changedFiles: [manifestPath],
9922
10163
  details: [
9923
- `Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL)}`
9924
- ]
10164
+ `Would run: ${bmadInstallDisplay(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL)}`,
10165
+ ...companionDetails(companion)
10166
+ ],
10167
+ bmadCompanion: companion
9925
10168
  };
9926
10169
  }
9927
- const install = runBmadInstall(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL);
10170
+ const install = await runBmadInstall(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL);
9928
10171
  if (!install.ok) {
9929
10172
  return {
9930
10173
  id: finding2.id,
@@ -9941,7 +10184,8 @@ function createBmadChecks() {
9941
10184
  const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
9942
10185
  const changed = Array.from(/* @__PURE__ */ new Set([
9943
10186
  ...upgraded ? [manifestPath] : [],
9944
- ...evictedChanges
10187
+ ...evictedChanges,
10188
+ ...install.companion?.changedFiles ?? []
9945
10189
  ]));
9946
10190
  return {
9947
10191
  id: finding2.id,
@@ -9949,7 +10193,8 @@ function createBmadChecks() {
9949
10193
  status: changed.length ? "applied" : "noop",
9950
10194
  summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
9951
10195
  changedFiles: changed,
9952
- details: evicted
10196
+ details: [...evicted, ...install.companion ? companionDetails(install.companion) : []],
10197
+ bmadCompanion: install.companion
9953
10198
  };
9954
10199
  }
9955
10200
  },
@@ -9958,7 +10203,7 @@ function createBmadChecks() {
9958
10203
  title: "Supported BMAD CLI projection roots",
9959
10204
  audit: (ctx) => {
9960
10205
  const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
9961
- const present = unsupportedNames.filter((name) => existsSync3(join5(ctx.repoRoot, name)));
10206
+ const present = unsupportedNames.filter((name) => existsSync4(join7(ctx.repoRoot, name)));
9962
10207
  const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
9963
10208
  const plan = planSkillRoots(ctx.repoRoot);
9964
10209
  const supportedIssues = supportedCliProjectionIssues(ctx.repoRoot, plan);
@@ -9980,7 +10225,7 @@ function createBmadChecks() {
9980
10225
  },
9981
10226
  migrate: (ctx, finding2) => {
9982
10227
  const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
9983
- const present = unsupportedNames.filter((name) => existsSync3(join5(ctx.repoRoot, name)));
10228
+ const present = unsupportedNames.filter((name) => existsSync4(join7(ctx.repoRoot, name)));
9984
10229
  const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
9985
10230
  const blocked = attestations.filter((entry) => !entry.safe);
9986
10231
  if (blocked.length) {
@@ -10005,7 +10250,7 @@ function createBmadChecks() {
10005
10250
  };
10006
10251
  }
10007
10252
  const gitignoreChanges = ensureSupportedCliGitignore(ctx);
10008
- const removedRoots = attestations.map((entry) => join5(ctx.repoRoot, entry.name));
10253
+ const removedRoots = attestations.map((entry) => join7(ctx.repoRoot, entry.name));
10009
10254
  if (!ctx.dryRun) for (const path of removedRoots) rmSync2(path, { recursive: true, force: true });
10010
10255
  const changedFiles = [.../* @__PURE__ */ new Set([...projectionResult.changedFiles, ...gitignoreChanges, ...removedRoots])].sort();
10011
10256
  return {
@@ -10064,11 +10309,11 @@ function linkedPlaneBoard(ctx) {
10064
10309
  return { boardId, workspace };
10065
10310
  }
10066
10311
  function runPx(args, ctx) {
10067
- const probe = spawnSync3("which", ["px"], { encoding: "utf8" });
10312
+ const probe = spawnSync4("which", ["px"], { encoding: "utf8" });
10068
10313
  if (probe.status !== 0) {
10069
10314
  return { error: "px not found on PATH \u2014 install Pilot to manage board schemas" };
10070
10315
  }
10071
- const result2 = spawnSync3("px", args, {
10316
+ const result2 = spawnSync4("px", args, {
10072
10317
  cwd: ctx.repoRoot,
10073
10318
  encoding: "utf8",
10074
10319
  shell: false,
@@ -10248,7 +10493,7 @@ var AgentHooksRecipe = class extends Recipe {
10248
10493
  printNextSteps() {
10249
10494
  console.log("\u{1FA9D} Agent-hooks layer installed!");
10250
10495
  console.log(" Next steps:");
10251
- console.log(" 1. mise run skills:sync # sync .agents/skills.json into local CLI dirs");
10496
+ console.log(" 1. pj skills sync --scope project --project . # sync .agents/skills.json into local CLI dirs");
10252
10497
  console.log(" 2. mise run hooks:sync # generate .claude/settings.json + inject codex/kimi/hermes");
10253
10498
  console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
10254
10499
  console.log(" 4. mise run hindsight:setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
@@ -10258,11 +10503,12 @@ var AgentHooksRecipe = class extends Recipe {
10258
10503
 
10259
10504
  // src/recipes/BmadRecipe.ts
10260
10505
  var BmadRecipe = class extends Recipe {
10261
- checks = createBmadChecks();
10506
+ requiredChecks = createBmadChecks();
10507
+ checks = [...this.requiredChecks, createG33CompanionCheck()];
10262
10508
  metadata = {
10263
10509
  id: "bmad",
10264
10510
  name: "bmad",
10265
- description: "BMAD methodology and the supported CLI projections",
10511
+ description: "BMAD methodology, supported CLI projections, and the optional owner-provided g33 companion",
10266
10512
  dependencies: ["agent-hooks"],
10267
10513
  commands: [],
10268
10514
  publicRuleIds: this.checks.map((check) => check.id)
@@ -10270,8 +10516,31 @@ var BmadRecipe = class extends Recipe {
10270
10516
  constructor(context) {
10271
10517
  super(context);
10272
10518
  }
10273
- init(ctx, _input) {
10274
- return this.initializeOwnedChecks(ctx);
10519
+ async init(ctx, _input) {
10520
+ const companions = [];
10521
+ const checks = this.requiredChecks.map((check) => ({
10522
+ ...check,
10523
+ migrate: async (...args) => {
10524
+ const result2 = await check.migrate(...args);
10525
+ if (result2.bmadCompanion) companions.push(result2.bmadCompanion);
10526
+ return result2;
10527
+ }
10528
+ }));
10529
+ const required = await this.initializeOwnedChecks(ctx, checks);
10530
+ if (!required.ok) return { ...required, ...companions.length ? { bmadCompanion: companions.at(-1) } : {} };
10531
+ const companion = companions.at(-1) ?? await runG33Companion(ctx.repoRoot, ctx.dryRun);
10532
+ return {
10533
+ ...required,
10534
+ bmadCompanion: companion,
10535
+ changedFiles: [.../* @__PURE__ */ new Set([...required.changedFiles, ...companion.changedFiles])].sort(),
10536
+ logs: [...required.logs, ...companionDetails(companion)],
10537
+ phases: [...required.phases, {
10538
+ id: "bmad.g33-companion",
10539
+ status: companion.status === "planned" ? "planned" : companion.status === "changed" ? "changed" : companion.status === "unchanged" ? "unchanged" : "skipped",
10540
+ message: `Optional g33 (${companion.status}): ${companion.summary}`,
10541
+ changedFiles: companion.changedFiles
10542
+ }]
10543
+ };
10275
10544
  }
10276
10545
  printNextSteps() {
10277
10546
  console.log(`BMAD lifecycle initialized for the ${SUPPORTED_CLIS.length} supported CLIs.`);
@@ -10595,24 +10864,24 @@ var NodeRecipe = class extends Recipe {
10595
10864
 
10596
10865
  // src/recipes/ProjectRecipe.ts
10597
10866
  init_project();
10598
- import { spawnSync as spawnSync11 } from "node:child_process";
10599
- import { existsSync as existsSync14, lstatSync as lstatSync12, readFileSync as readFileSync14, rmSync as rmSync5 } from "node:fs";
10600
- import { dirname as dirname14, isAbsolute as isAbsolute8, join as join18, relative as relativePath, resolve as resolve17 } from "node:path";
10867
+ import { spawnSync as spawnSync12 } from "node:child_process";
10868
+ import { existsSync as existsSync14, lstatSync as lstatSync13, readFileSync as readFileSync15, rmSync as rmSync5 } from "node:fs";
10869
+ import { dirname as dirname14, isAbsolute as isAbsolute9, join as join19, relative as relativePath, resolve as resolve18 } from "node:path";
10601
10870
  init_tree_diff();
10602
10871
 
10603
10872
  // src/recipes/NotebookRecipe.ts
10604
10873
  init_project();
10605
- import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
10606
- import { join as join17 } from "node:path";
10874
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
10875
+ import { join as join18 } from "node:path";
10607
10876
 
10608
10877
  // src/notebook/checks.ts
10609
10878
  init_config();
10610
10879
  init_notes();
10611
10880
  init_output();
10612
10881
  init_state();
10613
- import { spawnSync as spawnSync5 } from "node:child_process";
10614
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
10615
- import { join as join13 } from "node:path";
10882
+ import { spawnSync as spawnSync6 } from "node:child_process";
10883
+ import { existsSync as existsSync10, readFileSync as readFileSync12 } from "node:fs";
10884
+ import { join as join14 } from "node:path";
10616
10885
  var NOTEBOOK_RULE_IDS = [
10617
10886
  "notebook.configuration",
10618
10887
  "notebook.binding",
@@ -10629,10 +10898,10 @@ function result(check, status, summary, changedFiles = [], details = []) {
10629
10898
  return { id: check.id, title: check.title, status, summary, changedFiles, details };
10630
10899
  }
10631
10900
  function manifestNotebook(repo) {
10632
- const path = join13(repo, ".project.json");
10901
+ const path = join14(repo, ".project.json");
10633
10902
  if (!existsSync10(path)) return null;
10634
10903
  try {
10635
- const parsed = JSON.parse(readFileSync11(path, "utf8"));
10904
+ const parsed = JSON.parse(readFileSync12(path, "utf8"));
10636
10905
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
10637
10906
  const notebook = parsed.notebook;
10638
10907
  return notebook && typeof notebook === "object" && !Array.isArray(notebook) ? notebook : null;
@@ -10861,10 +11130,10 @@ function captureAudit(ctx) {
10861
11130
  function overviewMigrate(ctx, current) {
10862
11131
  if (ctx.dryRun) return result(current, "applied", "Would reconcile drifted Project Overview note");
10863
11132
  if (!ctx.live) return result(current, "blocked", "Reconciling the remote Overview note requires --live");
10864
- const pjanglerEntry = ctx.pjanglerRoot ? join13(ctx.pjanglerRoot, "dist", "index.js") : null;
11133
+ const pjanglerEntry = ctx.pjanglerRoot ? join14(ctx.pjanglerRoot, "dist", "index.js") : null;
10865
11134
  const entry = pjanglerEntry && existsSync10(pjanglerEntry) ? pjanglerEntry : "pj";
10866
11135
  const cmd = entry === "pj" ? ["pj"] : [process.execPath, entry];
10867
- const spawned = spawnSync5(cmd[0], [...cmd.slice(1), "notebook", "overview", ctx.repoRoot], {
11136
+ const spawned = spawnSync6(cmd[0], [...cmd.slice(1), "notebook", "overview", ctx.repoRoot], {
10868
11137
  encoding: "utf8",
10869
11138
  timeout: 1e4,
10870
11139
  env: { ...process.env, ...ctx.registryPath ? { PJ_PROJECT_REGISTRY: ctx.registryPath } : {} }
@@ -10909,7 +11178,7 @@ init_project();
10909
11178
  init_config();
10910
11179
  init_open_notebook_client();
10911
11180
  init_notes();
10912
- import { resolve as resolve16 } from "node:path";
11181
+ import { resolve as resolve17 } from "node:path";
10913
11182
 
10914
11183
  // src/notebook/journal-key.ts
10915
11184
  init_types();
@@ -10943,18 +11212,18 @@ function journalNoteOperationId(period, key) {
10943
11212
  init_notes();
10944
11213
  init_git_evidence();
10945
11214
  init_types();
10946
- import { spawnSync as spawnSync7 } from "node:child_process";
11215
+ import { spawnSync as spawnSync8 } from "node:child_process";
10947
11216
  import { realpathSync as realpathSync8 } from "node:fs";
10948
- import { relative as relative10, resolve as resolve13, sep as sep5 } from "node:path";
11217
+ import { relative as relative10, resolve as resolve14, sep as sep5 } from "node:path";
10949
11218
  var DEFAULT_OVERVIEW_REFERENCES = [".project.json", "README.md", "AGENTS.md", "CLAUDE.md", "docs/architecture.md"];
10950
11219
  function git2(repo, args, timeout) {
10951
- const result2 = spawnSync7("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 1024 * 1024, timeout, shell: false });
11220
+ const result2 = spawnSync8("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 1024 * 1024, timeout, shell: false });
10952
11221
  return { ok: result2.status === 0, stdout: result2.stdout?.trim() ?? "" };
10953
11222
  }
10954
11223
  function normalizedReference(repo, value) {
10955
11224
  if (!value || value.includes("\0") || value.startsWith("/") || value.split(/[\\/]/u).includes("..")) throw new NotebookError("INVALID_INPUT", `Overview reference is not a contained relative path: ${value}`);
10956
11225
  const root = realpathSync8(repo);
10957
- const candidate = resolve13(root, value);
11226
+ const candidate = resolve14(root, value);
10958
11227
  const rel = relative10(root, candidate).split(sep5).join("/");
10959
11228
  if (!rel || rel === ".." || rel.startsWith("../")) throw new NotebookError("INVALID_INPUT", `Overview reference escapes the repository: ${value}`);
10960
11229
  return rel.normalize("NFC");
@@ -11087,21 +11356,21 @@ init_git_evidence();
11087
11356
  init_notes();
11088
11357
  init_state();
11089
11358
  init_types();
11090
- import { spawn, spawnSync as spawnSync8 } from "node:child_process";
11359
+ import { spawn, spawnSync as spawnSync9 } from "node:child_process";
11091
11360
  import { createHash as createHash8, randomUUID as randomUUID5 } from "node:crypto";
11092
- import { chmodSync as chmodSync4, closeSync as closeSync5, constants as constants4, copyFileSync as copyFileSync3, existsSync as existsSync11, fstatSync as fstatSync3, fsyncSync as fsyncSync4, lstatSync as lstatSync11, mkdirSync as mkdirSync6, openSync as openSync5, readFileSync as readFileSync12, readSync as readSync3, readdirSync as readdirSync7, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync4, symlinkSync as symlinkSync3, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
11093
- import { basename as basename8, dirname as dirname13, isAbsolute as isAbsolute6, join as join16, parse as parse3, relative as relative11, resolve as resolve14, sep as sep6 } from "node:path";
11361
+ import { chmodSync as chmodSync4, closeSync as closeSync5, constants as constants4, copyFileSync as copyFileSync3, existsSync as existsSync11, fstatSync as fstatSync3, fsyncSync as fsyncSync4, lstatSync as lstatSync12, mkdirSync as mkdirSync6, openSync as openSync5, readFileSync as readFileSync13, readSync as readSync3, readdirSync as readdirSync7, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync4, symlinkSync as symlinkSync3, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
11362
+ import { basename as basename8, dirname as dirname13, isAbsolute as isAbsolute7, join as join17, parse as parse3, relative as relative11, resolve as resolve15, sep as sep6 } from "node:path";
11094
11363
  import { fileURLToPath as fileURLToPath3 } from "node:url";
11095
11364
  function bundledSkillCandidates() {
11096
11365
  const candidates = [];
11097
11366
  let cursor = dirname13(fileURLToPath3(import.meta.url));
11098
11367
  for (let depth = 0; depth < 8; depth++) {
11099
- candidates.push(join16(cursor, "dist", "assets", "project-notebook-skill"), join16(cursor, "assets", "project-notebook-skill"));
11368
+ candidates.push(join17(cursor, "dist", "assets", "project-notebook-skill"), join17(cursor, "assets", "project-notebook-skill"));
11100
11369
  const parent = dirname13(cursor);
11101
11370
  if (parent === cursor) break;
11102
11371
  cursor = parent;
11103
11372
  }
11104
- return [...new Set(candidates.map((candidate) => resolve14(candidate)))];
11373
+ return [...new Set(candidates.map((candidate) => resolve15(candidate)))];
11105
11374
  }
11106
11375
  function safeSkillRelativePath(value) {
11107
11376
  if (!value || value.startsWith("/") || value.includes("\0")) return false;
@@ -11114,14 +11383,14 @@ function assertOwnedSkillTree(source) {
11114
11383
  assertNoSymlinkComponents(source);
11115
11384
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
11116
11385
  const walk = (directory) => {
11117
- const directoryStat = lstatSync11(directory);
11386
+ const directoryStat = lstatSync12(directory);
11118
11387
  if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-directory component");
11119
11388
  if (uid !== void 0 && directoryStat.uid !== uid) throw new NotebookError("CONFLICT", "Project Notebook skill source is not owned by the current user");
11120
11389
  if (directoryStat.mode & 3586) throw new NotebookError("CONFLICT", "Project Notebook skill source has unsafe directory mode bits");
11121
11390
  for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11122
11391
  if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
11123
- const path = join16(directory, entry.name);
11124
- const stat = lstatSync11(path);
11392
+ const path = join17(directory, entry.name);
11393
+ const stat = lstatSync12(path);
11125
11394
  if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a symlink");
11126
11395
  if (entry.isDirectory()) walk(path);
11127
11396
  else if (!entry.isFile()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-regular entry");
@@ -11138,16 +11407,16 @@ function enumerateSkillPayload(source) {
11138
11407
  const walk = (directory) => {
11139
11408
  for (const entry of readdirSync7(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
11140
11409
  if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
11141
- const path = join16(directory, entry.name);
11410
+ const path = join17(directory, entry.name);
11142
11411
  const rel = relative11(source, path).split(sep6).join("/");
11143
11412
  if (!rel.includes("/") && (rel === "export-manifest.json" || rel === "SHA256SUMS" || rel === ".source.yaml")) continue;
11144
11413
  if (!safeSkillRelativePath(rel)) throw new NotebookError("CONFLICT", `Project Notebook skill export path is unsafe: ${rel}`);
11145
- const stat = lstatSync11(path);
11414
+ const stat = lstatSync12(path);
11146
11415
  if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a symlink");
11147
11416
  if (entry.isDirectory()) walk(path);
11148
11417
  else if (entry.isFile()) {
11149
11418
  const expectedMode = rel.endsWith(".sh") || rel.startsWith("scripts/") ? "0755" : "0644";
11150
- result2.push({ path: rel, sha256: createHash8("sha256").update(readFileSync12(path)).digest("hex"), mode: expectedMode });
11419
+ result2.push({ path: rel, sha256: createHash8("sha256").update(readFileSync13(path)).digest("hex"), mode: expectedMode });
11151
11420
  } else throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
11152
11421
  }
11153
11422
  };
@@ -11155,9 +11424,9 @@ function enumerateSkillPayload(source) {
11155
11424
  return result2.sort((a, b) => a.path.localeCompare(b.path, "en"));
11156
11425
  }
11157
11426
  function parsePackedManifest(source) {
11158
- const manifestPath = join16(source, "export-manifest.json");
11427
+ const manifestPath = join17(source, "export-manifest.json");
11159
11428
  if (!existsSync11(manifestPath)) return null;
11160
- const value = JSON.parse(readFileSync12(manifestPath, "utf8"));
11429
+ const value = JSON.parse(readFileSync13(manifestPath, "utf8"));
11161
11430
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is invalid");
11162
11431
  const manifest = value;
11163
11432
  if (manifest.schema_version !== 1 || manifest.skill !== "project-notebook" || !Array.isArray(manifest.files)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is incompatible");
@@ -11166,10 +11435,10 @@ function parsePackedManifest(source) {
11166
11435
  if (!entry || typeof entry.path !== "string" || !/^[a-f0-9]{64}$/u.test(entry.sha256) || !/^(?:0644|0755)$/u.test(entry.mode)) throw new NotebookError("CONFLICT", "Project Notebook skill export entry is invalid");
11167
11436
  if (!safeSkillRelativePath(entry.path) || paths.has(entry.path)) throw new NotebookError("CONFLICT", "Project Notebook skill export path is unsafe or duplicated");
11168
11437
  paths.add(entry.path);
11169
- const path = join16(source, ...entry.path.split("/"));
11170
- const stat = lstatSync11(path);
11438
+ const path = join17(source, ...entry.path.split("/"));
11439
+ const stat = lstatSync12(path);
11171
11440
  if (!stat.isFile() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
11172
- const actual = createHash8("sha256").update(readFileSync12(path)).digest("hex");
11441
+ const actual = createHash8("sha256").update(readFileSync13(path)).digest("hex");
11173
11442
  if (actual !== entry.sha256) throw new NotebookError("CONFLICT", `Project Notebook skill digest mismatch: ${entry.path}`);
11174
11443
  const actualMode = stat.mode & 511;
11175
11444
  const executable = entry.mode === "0755";
@@ -11181,12 +11450,12 @@ function parsePackedManifest(source) {
11181
11450
  if (JSON.stringify(actualPaths) !== JSON.stringify(manifest.files.map((entry) => entry.path))) throw new NotebookError("CONFLICT", "Project Notebook skill manifest does not exactly enumerate its payload");
11182
11451
  const sums = `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
11183
11452
  `;
11184
- if (!existsSync11(join16(source, "SHA256SUMS")) || readFileSync12(join16(source, "SHA256SUMS"), "utf8") !== sums) throw new NotebookError("CONFLICT", "Project Notebook skill SHA256SUMS is missing or stale");
11453
+ if (!existsSync11(join17(source, "SHA256SUMS")) || readFileSync13(join17(source, "SHA256SUMS"), "utf8") !== sums) throw new NotebookError("CONFLICT", "Project Notebook skill SHA256SUMS is missing or stale");
11185
11454
  return manifest;
11186
11455
  }
11187
11456
  function expectedPackedSkill() {
11188
11457
  for (const candidate of bundledSkillCandidates()) {
11189
- if (!existsSync11(join16(candidate, "export-manifest.json"))) continue;
11458
+ if (!existsSync11(join17(candidate, "export-manifest.json"))) continue;
11190
11459
  assertOwnedSkillTree(candidate);
11191
11460
  const manifest = parsePackedManifest(candidate);
11192
11461
  if (!manifest) continue;
@@ -11196,7 +11465,7 @@ function expectedPackedSkill() {
11196
11465
  throw new NotebookError("NOT_CONFIGURED", "PJ\xE1ngler package has no verified Project Notebook skill export");
11197
11466
  }
11198
11467
  function isCanonicalSkillexProjectionPath(path) {
11199
- return resolve14(path).split(sep6).join("/").endsWith("/all-skills/project-notebook");
11468
+ return resolve15(path).split(sep6).join("/").endsWith("/all-skills/project-notebook");
11200
11469
  }
11201
11470
  function isVerifiedCanonicalSkillexProjection(path) {
11202
11471
  if (!isCanonicalSkillexProjectionPath(path)) return false;
@@ -11216,7 +11485,7 @@ function describeProjectNotebookSkillDrift(source) {
11216
11485
  }
11217
11486
  let actual;
11218
11487
  try {
11219
- const absolute = resolve14(source);
11488
+ const absolute = resolve15(source);
11220
11489
  const packed = parsePackedManifest(absolute);
11221
11490
  actual = packed?.files ?? enumerateSkillPayload(absolute);
11222
11491
  } catch (error) {
@@ -11240,7 +11509,7 @@ function describeProjectNotebookSkillDrift(source) {
11240
11509
  return drift.length ? drift.slice(0, 20) : ["no per-file drift detected; the export enumeration itself differs"];
11241
11510
  }
11242
11511
  function verifyProjectNotebookSkillExport(source) {
11243
- const absolute = resolve14(source);
11512
+ const absolute = resolve15(source);
11244
11513
  assertOwnedSkillTree(absolute);
11245
11514
  const packed = parsePackedManifest(absolute);
11246
11515
  const actual = packed ?? { schema_version: 1, skill: "project-notebook", files: enumerateSkillPayload(absolute) };
@@ -11252,14 +11521,14 @@ function verifyProjectNotebookSkillExport(source) {
11252
11521
  }
11253
11522
  function resolveProjectNotebookSkillSource(env2 = process.env) {
11254
11523
  if (env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT) {
11255
- const explicit = resolve14(env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT);
11256
- if (!existsSync11(join16(explicit, "SKILL.md"))) throw new NotebookError("NOT_CONFIGURED", "Configured Project Notebook skill source is unavailable");
11524
+ const explicit = resolve15(env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT);
11525
+ if (!existsSync11(join17(explicit, "SKILL.md"))) throw new NotebookError("NOT_CONFIGURED", "Configured Project Notebook skill source is unavailable");
11257
11526
  verifyProjectNotebookSkillExport(explicit);
11258
11527
  return explicit;
11259
11528
  }
11260
11529
  if (env2.PJ_SKILLS_REGISTRY_ROOT) {
11261
- const canonical = resolve14(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
11262
- if (existsSync11(join16(canonical, "SKILL.md"))) {
11530
+ const canonical = resolve15(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
11531
+ if (existsSync11(join17(canonical, "SKILL.md"))) {
11263
11532
  verifyProjectNotebookSkillExport(canonical);
11264
11533
  return canonical;
11265
11534
  }
@@ -11274,7 +11543,7 @@ var REPAIR_COMMAND = "pj notebook skill --apply";
11274
11543
  function probeCanonicalSkillexRootProjection(skillsRoot, link) {
11275
11544
  let rootLink;
11276
11545
  try {
11277
- rootLink = lstatSync11(skillsRoot);
11546
+ rootLink = lstatSync12(skillsRoot);
11278
11547
  } catch (error) {
11279
11548
  if (error.code === "ENOENT") return { state: "absent" };
11280
11549
  throw error;
@@ -11287,15 +11556,15 @@ function probeCanonicalSkillexRootProjection(skillsRoot, link) {
11287
11556
  if (uid !== void 0 && rootLink.uid !== uid) throw new Error("owner");
11288
11557
  const globalRoot = realpathSync9(skillsRoot);
11289
11558
  assertNoSymlinkComponents(globalRoot);
11290
- const globalStat = lstatSync11(globalRoot);
11559
+ const globalStat = lstatSync12(globalRoot);
11291
11560
  if (!globalStat.isDirectory() || globalStat.isSymbolicLink()) throw new Error("root-type");
11292
11561
  if (uid !== void 0 && globalStat.uid !== uid) throw new Error("root-owner");
11293
11562
  if (globalStat.mode & 3586) throw new Error("root-mode");
11294
11563
  const skillSetsRoot = dirname13(globalRoot);
11295
11564
  if (basename8(globalRoot) !== "global" || basename8(skillSetsRoot) !== "skill-sets") throw new Error("layout");
11296
11565
  const checkoutRoot = dirname13(skillSetsRoot);
11297
- const expectedSource = join16(checkoutRoot, "all-skills", "project-notebook");
11298
- const linkStat = lstatSync11(link);
11566
+ const expectedSource = join17(checkoutRoot, "all-skills", "project-notebook");
11567
+ const linkStat = lstatSync12(link);
11299
11568
  if (!linkStat.isSymbolicLink()) throw new Error("projection-type");
11300
11569
  if (uid !== void 0 && linkStat.uid !== uid) throw new Error("projection-owner");
11301
11570
  const projectedSource = realpathSync9(link);
@@ -11331,28 +11600,28 @@ function installPackagedProjectNotebookSkill(input = {}) {
11331
11600
  const manifest = verifyProjectNotebookSkillExport(source);
11332
11601
  const digest = createHash8("sha256").update(JSON.stringify(manifest)).digest("hex");
11333
11602
  const home = env2.HOME;
11334
- if (!home || !resolve14(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to install the Project Notebook skill");
11335
- const skillsRoot = join16(home, ".agents", "skills");
11336
- const link = join16(skillsRoot, "project-notebook");
11603
+ if (!home || !resolve15(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to install the Project Notebook skill");
11604
+ const skillsRoot = join17(home, ".agents", "skills");
11605
+ const link = join17(skillsRoot, "project-notebook");
11337
11606
  const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
11338
11607
  if (probe.state === "adopted") return { installed: false, path: link, digest };
11339
11608
  if (probe.state === "declined") return { installed: false, path: link, digest, blocked: probe.block };
11340
- const dataRoot = resolve14(env2.XDG_DATA_HOME || join16(home, ".local", "share"), "pjangler", "skills", "project-notebook");
11341
- const payload = join16(dataRoot, `${PJANGLER_VERSION}-${digest}`);
11609
+ const dataRoot = resolve15(env2.XDG_DATA_HOME || join17(home, ".local", "share"), "pjangler", "skills", "project-notebook");
11610
+ const payload = join17(dataRoot, `${PJANGLER_VERSION}-${digest}`);
11342
11611
  assertNoSymlinkComponents(dirname13(dataRoot), true);
11343
11612
  mkdirSync6(dataRoot, { recursive: true, mode: 448 });
11344
11613
  assertNoSymlinkComponents(dataRoot);
11345
- const dataStat = lstatSync11(dataRoot);
11614
+ const dataStat = lstatSync12(dataRoot);
11346
11615
  if (!dataStat.isDirectory() || dataStat.isSymbolicLink() || typeof process.getuid === "function" && dataStat.uid !== process.getuid()) throw new NotebookError("CONFLICT", "Project Notebook skill data root is not a current-user directory");
11347
11616
  chmodSync4(dataRoot, 448);
11348
11617
  if (!existsSync11(payload)) {
11349
- const staging = join16(dataRoot, `.staging-${randomUUID5()}`);
11618
+ const staging = join17(dataRoot, `.staging-${randomUUID5()}`);
11350
11619
  mkdirSync6(staging, { recursive: false, mode: 448 });
11351
11620
  try {
11352
11621
  for (const entry of manifest.files) {
11353
- const destination = join16(staging, ...entry.path.split("/"));
11622
+ const destination = join17(staging, ...entry.path.split("/"));
11354
11623
  mkdirSync6(dirname13(destination), { recursive: true, mode: 493 });
11355
- copyFileSync3(join16(source, ...entry.path.split("/")), destination);
11624
+ copyFileSync3(join17(source, ...entry.path.split("/")), destination);
11356
11625
  const mode = entry.mode === "0755" ? 493 : 420;
11357
11626
  chmodSync4(destination, mode);
11358
11627
  const fd = openSync5(destination, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
@@ -11364,9 +11633,9 @@ function installPackagedProjectNotebookSkill(input = {}) {
11364
11633
  closeSync5(fd);
11365
11634
  }
11366
11635
  }
11367
- writeFileSync7(join16(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
11636
+ writeFileSync7(join17(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
11368
11637
  `, { mode: 420, flag: "wx" });
11369
- writeFileSync7(join16(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
11638
+ writeFileSync7(join17(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
11370
11639
  `, { mode: 420, flag: "wx" });
11371
11640
  verifyProjectNotebookSkillExport(staging);
11372
11641
  renameSync6(staging, payload);
@@ -11374,7 +11643,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
11374
11643
  if (existsSync11(staging)) rmSync4(staging, { recursive: true, force: true });
11375
11644
  }
11376
11645
  } else {
11377
- const stat = lstatSync11(payload);
11646
+ const stat = lstatSync12(payload);
11378
11647
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Installed Project Notebook payload is not a real directory");
11379
11648
  verifyProjectNotebookSkillExport(payload);
11380
11649
  }
@@ -11384,7 +11653,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
11384
11653
  let linkExists = existsSync11(link);
11385
11654
  if (!linkExists) {
11386
11655
  try {
11387
- lstatSync11(link);
11656
+ lstatSync12(link);
11388
11657
  linkExists = true;
11389
11658
  } catch {
11390
11659
  }
@@ -11401,7 +11670,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
11401
11670
  repair: REPAIR_COMMAND
11402
11671
  }
11403
11672
  });
11404
- const stat = lstatSync11(link);
11673
+ const stat = lstatSync12(link);
11405
11674
  if (!stat.isSymbolicLink()) return foreign(["the path is a real file or directory, not a PJ\xE1ngler-owned link"]);
11406
11675
  const target = realpathSync9(link);
11407
11676
  if (target !== realpathSync9(payload) && !isVerifiedCanonicalSkillexProjection(target)) {
@@ -11435,9 +11704,9 @@ function collectSupersededPayloads(dataRoot, keep) {
11435
11704
  }
11436
11705
  for (const name of entries) {
11437
11706
  if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?-[0-9a-f]{64}$/u.test(name)) continue;
11438
- const candidate = join16(dataRoot, name);
11707
+ const candidate = join17(dataRoot, name);
11439
11708
  try {
11440
- const stat = lstatSync11(candidate);
11709
+ const stat = lstatSync12(candidate);
11441
11710
  if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
11442
11711
  if (kept.has(realpathSync9(candidate))) continue;
11443
11712
  rmSync4(candidate, { recursive: true, force: true });
@@ -11450,9 +11719,9 @@ function collectSupersededPayloads(dataRoot, keep) {
11450
11719
  function repairProjectNotebookSkillProjection(input = {}) {
11451
11720
  const env2 = input.env ?? process.env;
11452
11721
  const home = env2.HOME;
11453
- if (!home || !resolve14(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to repair the Project Notebook skill projection");
11454
- const skillsRoot = join16(home, ".agents", "skills");
11455
- const link = join16(skillsRoot, "project-notebook");
11722
+ if (!home || !resolve15(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to repair the Project Notebook skill projection");
11723
+ const skillsRoot = join17(home, ".agents", "skills");
11724
+ const link = join17(skillsRoot, "project-notebook");
11456
11725
  const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
11457
11726
  if (probe.state === "adopted") return { status: "clean", summary: "Canonical Skillex projection already matches the version-pinned export", source: probe.source, drift: [], changed_files: [] };
11458
11727
  if (probe.state !== "declined") {
@@ -11466,7 +11735,7 @@ function repairProjectNotebookSkillProjection(input = {}) {
11466
11735
  const present = [];
11467
11736
  const walk = (directory) => {
11468
11737
  for (const entry of readdirSync7(directory, { withFileTypes: true })) {
11469
- const path = join16(directory, entry.name);
11738
+ const path = join17(directory, entry.name);
11470
11739
  const rel = relative11(source, path).split(sep6).join("/");
11471
11740
  if (entry.isDirectory()) {
11472
11741
  walk(path);
@@ -11480,11 +11749,11 @@ function repairProjectNotebookSkillProjection(input = {}) {
11480
11749
  walk(source);
11481
11750
  const stale = present.filter((rel) => !wanted.has(rel)).sort();
11482
11751
  const changed = expected.manifest.files.filter((entry) => {
11483
- const path = join16(source, ...entry.path.split("/"));
11752
+ const path = join17(source, ...entry.path.split("/"));
11484
11753
  if (!existsSync11(path)) return true;
11485
- const stat = lstatSync11(path);
11754
+ const stat = lstatSync12(path);
11486
11755
  if (!stat.isFile()) return true;
11487
- if (createHash8("sha256").update(readFileSync12(path)).digest("hex") !== entry.sha256) return true;
11756
+ if (createHash8("sha256").update(readFileSync13(path)).digest("hex") !== entry.sha256) return true;
11488
11757
  const mode = stat.mode & 511;
11489
11758
  const executable = entry.mode === "0755";
11490
11759
  return executable && (mode & 64) === 0 || !executable && (mode & 73) !== 0;
@@ -11499,15 +11768,15 @@ function repairProjectNotebookSkillProjection(input = {}) {
11499
11768
  summary: `Would restore ${affected.length} file(s) in ${source} from the version-pinned export`,
11500
11769
  source,
11501
11770
  drift: probe.block.details,
11502
- changed_files: affected.map((rel) => join16(source, ...rel.split("/")))
11771
+ changed_files: affected.map((rel) => join17(source, ...rel.split("/")))
11503
11772
  };
11504
11773
  }
11505
- for (const rel of stale) rmSync4(join16(source, ...rel.split("/")), { force: true });
11774
+ for (const rel of stale) rmSync4(join17(source, ...rel.split("/")), { force: true });
11506
11775
  for (const rel of changed) {
11507
- const destination = join16(source, ...rel.split("/"));
11776
+ const destination = join17(source, ...rel.split("/"));
11508
11777
  const entry = expected.manifest.files.find((item) => item.path === rel);
11509
11778
  mkdirSync6(dirname13(destination), { recursive: true, mode: 493 });
11510
- copyFileSync3(join16(expected.source, ...rel.split("/")), destination);
11779
+ copyFileSync3(join17(expected.source, ...rel.split("/")), destination);
11511
11780
  chmodSync4(destination, entry.mode === "0755" ? 493 : 420);
11512
11781
  }
11513
11782
  verifyProjectNotebookSkillExport(source);
@@ -11516,21 +11785,21 @@ function repairProjectNotebookSkillProjection(input = {}) {
11516
11785
  summary: `Restored ${affected.length} file(s) in ${source} from the version-pinned export; commit the change in the Skillex checkout`,
11517
11786
  source,
11518
11787
  drift: probe.block.details,
11519
- changed_files: affected.map((rel) => join16(source, ...rel.split("/")))
11788
+ changed_files: affected.map((rel) => join17(source, ...rel.split("/")))
11520
11789
  };
11521
11790
  }
11522
11791
  function inspectProjectNotebookIntegration(env2 = process.env) {
11523
11792
  const home = env2.HOME;
11524
11793
  if (!home) return { skill_installed: false, hooks_projected: false, details: ["HOME is unavailable"] };
11525
- const skillsRoot = join16(home, ".agents", "skills");
11526
- const link = join16(skillsRoot, "project-notebook");
11794
+ const skillsRoot = join17(home, ".agents", "skills");
11795
+ const link = join17(skillsRoot, "project-notebook");
11527
11796
  try {
11528
11797
  const expected = expectedPackedSkill();
11529
- const dataRoot = resolve14(env2.XDG_DATA_HOME || join16(home, ".local", "share"), "pjangler", "skills", "project-notebook");
11530
- const expectedPayload = join16(dataRoot, `${PJANGLER_VERSION}-${expected.digest}`);
11798
+ const dataRoot = resolve15(env2.XDG_DATA_HOME || join17(home, ".local", "share"), "pjangler", "skills", "project-notebook");
11799
+ const expectedPayload = join17(dataRoot, `${PJANGLER_VERSION}-${expected.digest}`);
11531
11800
  const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
11532
11801
  if (probe.state === "declined") return { skill_installed: false, hooks_projected: false, details: probe.block.details, blocked: probe.block };
11533
- const stat = lstatSync11(link);
11802
+ const stat = lstatSync12(link);
11534
11803
  if (!stat.isSymbolicLink()) {
11535
11804
  return {
11536
11805
  skill_installed: false,
@@ -11567,8 +11836,8 @@ function inspectProjectNotebookIntegration(env2 = process.env) {
11567
11836
  }
11568
11837
  }
11569
11838
  function projectorArguments(source, command, input) {
11570
- const script = join16(source, "scripts", "project-hooks.py");
11571
- const args = [script, command, "--master", join16(source, "hooks", "hooks.master.json"), "--fragment", join16(source, "hooks", "claude.settings.json"), "--target", input.target];
11839
+ const script = join17(source, "scripts", "project-hooks.py");
11840
+ const args = [script, command, "--master", join17(source, "hooks", "hooks.master.json"), "--fragment", join17(source, "hooks", "claude.settings.json"), "--target", input.target];
11572
11841
  if (command === "check") args.push("--json");
11573
11842
  else if (input.stateHome) args.push("--state-home", input.stateHome);
11574
11843
  return args;
@@ -11590,8 +11859,8 @@ function checkProjectNotebookHooks(input = {}) {
11590
11859
  verifyProjectNotebookSkillExport(source);
11591
11860
  const home = env2.HOME;
11592
11861
  if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to check Project Notebook hooks");
11593
- const target = resolve14(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join16(home, ".claude", "settings.json"));
11594
- const result2 = spawnSync8("/usr/bin/python3", ["-I", ...projectorArguments(source, "check", { target })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
11862
+ const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join17(home, ".claude", "settings.json"));
11863
+ const result2 = spawnSync9("/usr/bin/python3", ["-I", ...projectorArguments(source, "check", { target })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
11595
11864
  if (result2.status !== 0 && result2.status !== 1) throw new NotebookError("CONFLICT", (result2.stderr || "Project Notebook projector check failed").trim().slice(0, 512));
11596
11865
  try {
11597
11866
  const parsed = JSON.parse(result2.stdout);
@@ -11608,13 +11877,13 @@ function installProjectNotebookIntegration(input = {}) {
11608
11877
  const source = realpathSync9(skill.path);
11609
11878
  const home = env2.HOME;
11610
11879
  if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to install Project Notebook hooks");
11611
- const target = resolve14(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join16(home, ".claude", "settings.json"));
11612
- const stateHome = resolve14(env2.XDG_STATE_HOME || join16(home, ".local", "state"));
11613
- const result2 = spawnSync8("/usr/bin/python3", ["-I", ...projectorArguments(source, "install", { target, stateHome })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
11880
+ const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join17(home, ".claude", "settings.json"));
11881
+ const stateHome = resolve15(env2.XDG_STATE_HOME || join17(home, ".local", "state"));
11882
+ const result2 = spawnSync9("/usr/bin/python3", ["-I", ...projectorArguments(source, "install", { target, stateHome })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
11614
11883
  if (result2.status !== 0) {
11615
11884
  if (skill.installed) {
11616
11885
  try {
11617
- const stat = lstatSync11(skill.path);
11886
+ const stat = lstatSync12(skill.path);
11618
11887
  if (stat.isSymbolicLink() && realpathSync9(skill.path) === source) unlinkSync6(skill.path);
11619
11888
  } catch {
11620
11889
  }
@@ -11628,14 +11897,14 @@ function boundedDiagnostic(value, max = 512) {
11628
11897
  return message.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").slice(0, max);
11629
11898
  }
11630
11899
  function assertNoSymlinkComponents(path, allowMissing = false) {
11631
- const absolute = resolve14(path);
11900
+ const absolute = resolve15(path);
11632
11901
  const root = parse3(absolute).root;
11633
11902
  let cursor = root;
11634
11903
  for (const component of absolute.slice(root.length).split(sep6).filter(Boolean)) {
11635
- cursor = join16(cursor, component);
11904
+ cursor = join17(cursor, component);
11636
11905
  let stat;
11637
11906
  try {
11638
- stat = lstatSync11(cursor);
11907
+ stat = lstatSync12(cursor);
11639
11908
  } catch (error) {
11640
11909
  if (allowMissing && error.code === "ENOENT") return;
11641
11910
  throw error;
@@ -11835,7 +12104,7 @@ var NotebookModule = class {
11835
12104
  return {
11836
12105
  changedFiles: [
11837
12106
  ...installed.skill.installed ? [installed.skill.path] : [],
11838
- ...installed.hooksChanged ? [resolve16(env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? `${home}/.claude/settings.json`)] : []
12107
+ ...installed.hooksChanged ? [resolve17(env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? `${home}/.claude/settings.json`)] : []
11839
12108
  ],
11840
12109
  ...installed.blocked ? { blocked: installed.blocked } : {}
11841
12110
  };
@@ -12241,17 +12510,17 @@ init_remote_mutation_journal();
12241
12510
  function integrationEnvironment(module, ctx) {
12242
12511
  const env2 = { ...module.environment, HOME: ctx.homeDir };
12243
12512
  if (module.environment.HOME !== ctx.homeDir) {
12244
- env2.XDG_DATA_HOME = join17(ctx.homeDir, ".local", "share");
12245
- env2.XDG_STATE_HOME = join17(ctx.homeDir, ".local", "state");
12246
- env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS = join17(ctx.homeDir, ".claude", "settings.json");
12513
+ env2.XDG_DATA_HOME = join18(ctx.homeDir, ".local", "share");
12514
+ env2.XDG_STATE_HOME = join18(ctx.homeDir, ".local", "state");
12515
+ env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS = join18(ctx.homeDir, ".claude", "settings.json");
12247
12516
  }
12248
12517
  return env2;
12249
12518
  }
12250
12519
  function resolvedForPlan(plan) {
12251
- const manifestPath = join17(plan.project.repo_path, ".project.json");
12520
+ const manifestPath = join18(plan.project.repo_path, ".project.json");
12252
12521
  let manifest = plan.manifest;
12253
12522
  if (existsSync13(manifestPath)) {
12254
- const parsed = JSON.parse(readFileSync13(manifestPath, "utf8"));
12523
+ const parsed = JSON.parse(readFileSync14(manifestPath, "utf8"));
12255
12524
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${manifestPath} must contain a JSON object`);
12256
12525
  manifest = parsed;
12257
12526
  }
@@ -12392,7 +12661,7 @@ var PRODUCTION_RUNTIME = {
12392
12661
  executePlan: executeProjectInitPlan,
12393
12662
  preflightBmad: preflightBmadLifecycle,
12394
12663
  runGit(cwd, args, options) {
12395
- const result2 = spawnSync11("git", [...args], {
12664
+ const result2 = spawnSync12("git", [...args], {
12396
12665
  cwd,
12397
12666
  encoding: "utf8",
12398
12667
  env: options?.env ? { ...process.env, ...options.env } : process.env
@@ -12406,13 +12675,13 @@ var PRODUCTION_RUNTIME = {
12406
12675
  }
12407
12676
  };
12408
12677
  function unsafeToRemove(targetDir) {
12409
- const absolute = resolve17(targetDir);
12678
+ const absolute = resolve18(targetDir);
12410
12679
  let cursor = absolute;
12411
12680
  const seen = /* @__PURE__ */ new Set();
12412
12681
  while (!seen.has(cursor)) {
12413
12682
  seen.add(cursor);
12414
12683
  try {
12415
- const stat = lstatSync12(cursor);
12684
+ const stat = lstatSync13(cursor);
12416
12685
  if (stat.isSymbolicLink()) {
12417
12686
  return cursor === absolute ? `${absolute} is a symlink; removing it would leave the tree it points at orphaned` : `${cursor} is a symlink on the path to ${absolute}; a recursive remove would traverse it`;
12418
12687
  }
@@ -12440,12 +12709,12 @@ function publicMigration(report) {
12440
12709
  };
12441
12710
  }
12442
12711
  function hasGitRepository(runtime, targetDir) {
12443
- if (!existsSync14(join18(targetDir, ".git"))) return false;
12712
+ if (!existsSync14(join19(targetDir, ".git"))) return false;
12444
12713
  return runtime.runGit(targetDir, ["rev-parse", "--is-inside-work-tree"]).status === 0;
12445
12714
  }
12446
12715
  function refreshPlanFromCanonicalManifest(plan) {
12447
- const manifestPath = join18(plan.project.repo_path, ".project.json");
12448
- const manifest = JSON.parse(readFileSync14(manifestPath, "utf8"));
12716
+ const manifestPath = join19(plan.project.repo_path, ".project.json");
12717
+ const manifest = JSON.parse(readFileSync15(manifestPath, "utf8"));
12449
12718
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
12450
12719
  throw new Error(`${manifestPath} must contain a JSON object`);
12451
12720
  }
@@ -12537,6 +12806,8 @@ var ProjectRecipe = class extends Recipe {
12537
12806
  repoRoot: targetDir,
12538
12807
  bmadVersionPin: mode === "create" ? BMAD_INSTALLER_VERSION : ctx.bmadVersionPin
12539
12808
  };
12809
+ let bmadCompanion;
12810
+ let g33DeferredForEnrollment = false;
12540
12811
  let migrationReport;
12541
12812
  let audit;
12542
12813
  let notebookPlan;
@@ -12577,6 +12848,10 @@ var ProjectRecipe = class extends Recipe {
12577
12848
  message: planBlocked ? "Project plan skipped after failed preflight" : executed.ok ? "Project plan executed" : executed.errors.join("; ")
12578
12849
  });
12579
12850
  if (errors.length === 0 && executed.ok && mode === "create") {
12851
+ try {
12852
+ g33DeferredForEnrollment = !g33CompanionEnabled(targetDir);
12853
+ } catch {
12854
+ }
12580
12855
  const dependencyResult = await this.registry.initDependencies(this.metadata.id, transactionContext, normalized, ["notebook"]);
12581
12856
  logs.push(...dependencyResult.logs);
12582
12857
  errors.push(...dependencyResult.errors);
@@ -12657,7 +12932,7 @@ var ProjectRecipe = class extends Recipe {
12657
12932
  if (hasGitRepository(this.runtime, targetDir)) {
12658
12933
  phases.push({ id: "project.git", status: "unchanged", changedFiles: [], message: "Git repository already initialized" });
12659
12934
  } else {
12660
- const gitPath = join18(targetDir, ".git");
12935
+ const gitPath = join19(targetDir, ".git");
12661
12936
  for (const { args, label, options } of [
12662
12937
  { args: ["init", "--initial-branch=main"], label: "git init" },
12663
12938
  { args: ["add", "-A"], label: "git add" },
@@ -12757,6 +13032,17 @@ var ProjectRecipe = class extends Recipe {
12757
13032
  errors.push(`project manifest refresh after external effects failed: ${error instanceof Error ? error.message : String(error)}`);
12758
13033
  }
12759
13034
  }
13035
+ if (errors.length === 0 && registryFinalizerEligible && mode === "create" && g33DeferredForEnrollment && plan.project.ticket_provider.state === "linked") {
13036
+ bmadCompanion = await runG33Companion(targetDir, ctx.dryRun);
13037
+ logs.push(...companionDetails(bmadCompanion));
13038
+ changedFiles.push(...bmadCompanion.changedFiles);
13039
+ phases.push({
13040
+ id: "project.optional:g33-companion",
13041
+ status: bmadCompanion.status === "changed" ? "changed" : bmadCompanion.status === "planned" ? "planned" : bmadCompanion.status === "unchanged" ? "unchanged" : "skipped",
13042
+ changedFiles: bmadCompanion.changedFiles,
13043
+ message: `Optional g33 (${bmadCompanion.status}): ${bmadCompanion.summary}`
13044
+ });
13045
+ }
12760
13046
  if (registryFinalizerEligible && registryActions.length) {
12761
13047
  rollbackEligible = false;
12762
13048
  const registryPlan = { ...plan, actions: registryActions };
@@ -12810,8 +13096,8 @@ var ProjectRecipe = class extends Recipe {
12810
13096
  } else try {
12811
13097
  rmSync5(targetDir, { recursive: true, force: true });
12812
13098
  const insideTarget = (path) => {
12813
- const relative12 = relativePath(resolve17(targetDir), resolve17(path));
12814
- return relative12 === "" || !relative12.startsWith("..") && !isAbsolute8(relative12);
13099
+ const relative12 = relativePath(resolve18(targetDir), resolve18(path));
13100
+ return relative12 === "" || !relative12.startsWith("..") && !isAbsolute9(relative12);
12815
13101
  };
12816
13102
  const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
12817
13103
  changedFiles.length = 0;
@@ -12847,7 +13133,8 @@ var ProjectRecipe = class extends Recipe {
12847
13133
  audit,
12848
13134
  selectedOperations: normalized.selectedOperations ?? [],
12849
13135
  selectedParityRules: normalized.selectedRuleIds ?? [],
12850
- migrationReport
13136
+ migrationReport,
13137
+ ...bmadCompanion ? { bmadCompanion } : {}
12851
13138
  };
12852
13139
  }
12853
13140
  printNextSteps() {
@@ -12855,12 +13142,12 @@ var ProjectRecipe = class extends Recipe {
12855
13142
  };
12856
13143
 
12857
13144
  // src/recipes/registry.ts
12858
- import { readFileSync as readFileSync15 } from "node:fs";
12859
- import { join as join19 } from "node:path";
13145
+ import { readFileSync as readFileSync16 } from "node:fs";
13146
+ import { join as join20 } from "node:path";
12860
13147
  function projectWaivers(repoRoot) {
12861
13148
  let waive;
12862
13149
  try {
12863
- waive = JSON.parse(readFileSync15(join19(repoRoot, ".project.json"), "utf8"))?.parity?.waive;
13150
+ waive = JSON.parse(readFileSync16(join20(repoRoot, ".project.json"), "utf8"))?.parity?.waive;
12864
13151
  } catch {
12865
13152
  return /* @__PURE__ */ new Map();
12866
13153
  }
@@ -13201,8 +13488,8 @@ var recipeRegistry = new RecipeRegistry([
13201
13488
 
13202
13489
  // src/commands/AgentHooksCommands.ts
13203
13490
  import { homedir as homedir6 } from "node:os";
13204
- import { join as join20, dirname as dirname15 } from "node:path";
13205
- import { existsSync as existsSync15, cpSync, mkdirSync as mkdirSync7, readFileSync as readFileSync16, writeFileSync as writeFileSync8 } from "node:fs";
13491
+ import { join as join21, dirname as dirname15 } from "node:path";
13492
+ import { existsSync as existsSync15, cpSync, mkdirSync as mkdirSync7, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "node:fs";
13206
13493
  import { fileURLToPath as fileURLToPath4 } from "node:url";
13207
13494
  init_project();
13208
13495
  import { parse as parseToml3 } from "smol-toml";
@@ -13215,16 +13502,16 @@ function resolveTemplateRoot() {
13215
13502
  try {
13216
13503
  let dir = dirname15(fileURLToPath4(import.meta.url));
13217
13504
  for (let i = 0; i < 8; i++) {
13218
- candidates.push(join20(dir, "templates", "commonproject", "template"));
13505
+ candidates.push(join21(dir, "templates", "commonproject", "template"));
13219
13506
  const parent = dirname15(dir);
13220
13507
  if (parent === dir) break;
13221
13508
  dir = parent;
13222
13509
  }
13223
13510
  } catch {
13224
13511
  }
13225
- candidates.push(join20(homedir6(), "code", "pjangler", "templates", "commonproject", "template"));
13512
+ candidates.push(join21(homedir6(), "code", "pjangler", "templates", "commonproject", "template"));
13226
13513
  for (const c of candidates) {
13227
- if (existsSync15(join20(c, ".agents", "hooks", "hooks.master.json"))) return c;
13514
+ if (existsSync15(join21(c, ".agents", "hooks", "hooks.master.json"))) return c;
13228
13515
  }
13229
13516
  throw new Error(
13230
13517
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -13250,8 +13537,8 @@ var CopyAgentHooksTree = class extends Command {
13250
13537
  const created = [];
13251
13538
  const skipped = [];
13252
13539
  for (const { rel, dir } of items) {
13253
- const src = join20(templateRoot, rel);
13254
- const dest = join20(this.context.targetDir, rel);
13540
+ const src = join21(templateRoot, rel);
13541
+ const dest = join21(this.context.targetDir, rel);
13255
13542
  if (!existsSync15(src)) continue;
13256
13543
  if (existsSync15(dest) && !this.context.force) {
13257
13544
  skipped.push(rel);
@@ -13279,14 +13566,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
13279
13566
  if (!resolveAgentHooksLayer2()) {
13280
13567
  return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
13281
13568
  }
13282
- const misePath = join20(this.context.targetDir, "mise.toml");
13569
+ const misePath = join21(this.context.targetDir, "mise.toml");
13283
13570
  if (!existsSync15(misePath)) {
13284
13571
  return {
13285
13572
  success: false,
13286
13573
  message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj add mise` first, then re-run."
13287
13574
  };
13288
13575
  }
13289
- let content = readFileSync16(misePath, "utf8");
13576
+ let content = readFileSync17(misePath, "utf8");
13290
13577
  if (content.includes(_WireMiseAgentHooks.MARKER)) {
13291
13578
  return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
13292
13579
  }
@@ -13326,21 +13613,13 @@ ${leaveBlock}`);
13326
13613
  }
13327
13614
  wiredHooks = true;
13328
13615
  }
13329
- const skillsSync = [
13330
- '[tasks."skills:sync"]',
13331
- `description = "Reconcile this project's selected skills"`,
13332
- `tools = ${SKILLS_SYNC_TOOLS}`,
13333
- `run = "skillex sync --scope project --project '${cr}'"`,
13334
- ""
13335
- ];
13336
- const managedSkillsSync = parseToml3(skillsSync.join("\n")).tasks["skills:sync"];
13337
- const keepSkillsSync = existingTasks["skills:sync"] !== void 0 && canonicalJson(existingTasks["skills:sync"]) === canonicalJson(managedSkillsSync);
13338
13616
  const appended = [
13339
13617
  "",
13340
13618
  _WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
13341
13619
  // PJAN-61: task names use the colon namespace form. A colon is not legal
13342
13620
  // in a BARE toml key, so every header here MUST stay quoted.
13343
- ...keepSkillsSync ? [] : skillsSync,
13621
+ // PJAN-135 retired 2026-10-10: no skills:sync task is appended — skill
13622
+ // operations invoke skillex directly (`pj skills …`), never a mise task.
13344
13623
  "[[watch_files]]",
13345
13624
  'patterns = [".agents/hooks/hooks.master.json"]',
13346
13625
  'task = "hooks:sync"',
@@ -13509,7 +13788,7 @@ if __name__ == "__main__":
13509
13788
 
13510
13789
  // src/commands/AddMiseCodegraphScript.ts
13511
13790
  import { chmodSync as chmodSync5 } from "fs";
13512
- import { join as join21 } from "path";
13791
+ import { join as join22 } from "path";
13513
13792
  var AddMiseCodegraphScript = class extends Command {
13514
13793
  async invoke() {
13515
13794
  const filePath = ".mise/scripts/codegraph.sh";
@@ -13563,7 +13842,7 @@ fi
13563
13842
  `;
13564
13843
  this.writeFile(filePath, content);
13565
13844
  if (!this.context.dryRun) {
13566
- chmodSync5(join21(this.context.targetDir, filePath), 493);
13845
+ chmodSync5(join22(this.context.targetDir, filePath), 493);
13567
13846
  }
13568
13847
  return {
13569
13848
  success: true,
@@ -13601,18 +13880,18 @@ SECRET_KEY=""
13601
13880
  // src/parity/index.ts
13602
13881
  import { existsSync as existsSync16 } from "node:fs";
13603
13882
  import { homedir as homedir7 } from "node:os";
13604
- import { dirname as dirname16, join as join22, resolve as resolve18 } from "node:path";
13883
+ import { dirname as dirname16, join as join23, resolve as resolve19 } from "node:path";
13605
13884
  import { fileURLToPath as fileURLToPath5 } from "node:url";
13606
13885
  function resolvePjanglerRoot2() {
13607
13886
  let dir = dirname16(fileURLToPath5(import.meta.url));
13608
13887
  while (dir !== dirname16(dir)) {
13609
- if (existsSync16(join22(dir, "package.json")) && existsSync16(join22(dir, "templates", "commonproject", "copier.yml"))) return dir;
13888
+ if (existsSync16(join23(dir, "package.json")) && existsSync16(join23(dir, "templates", "commonproject", "copier.yml"))) return dir;
13610
13889
  dir = dirname16(dir);
13611
13890
  }
13612
- return resolve18(process.cwd());
13891
+ return resolve19(process.cwd());
13613
13892
  }
13614
13893
  function lifecycleContext(repoArg, dryRun, acceptRegistryMatches = false, overrides = {}) {
13615
- const repoRoot = resolve18(repoArg ?? process.cwd());
13894
+ const repoRoot = resolve19(repoArg ?? process.cwd());
13616
13895
  return {
13617
13896
  ...overrides,
13618
13897
  targetDir: repoRoot,
@@ -13781,13 +14060,13 @@ init_version();
13781
14060
  // src/project/info.ts
13782
14061
  init_boardUrl();
13783
14062
  init_project();
13784
- import { readFileSync as readFileSync17 } from "node:fs";
13785
- import { join as join23, resolve as resolve19 } from "node:path";
14063
+ import { readFileSync as readFileSync18 } from "node:fs";
14064
+ import { join as join24, resolve as resolve20 } from "node:path";
13786
14065
  function readManifest2(root) {
13787
- const path = join23(root, ".project.json");
14066
+ const path = join24(root, ".project.json");
13788
14067
  let manifest;
13789
14068
  try {
13790
- const value = JSON.parse(readFileSync17(path, "utf8"));
14069
+ const value = JSON.parse(readFileSync18(path, "utf8"));
13791
14070
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected a JSON object");
13792
14071
  manifest = value;
13793
14072
  } catch (error) {
@@ -13827,8 +14106,8 @@ function readProjectInfo(input = {}) {
13827
14106
  const info = {
13828
14107
  ok: true,
13829
14108
  project_id: projectId,
13830
- repo_path: resolve19(root),
13831
- manifest_path: join23(resolve19(root), ".project.json"),
14109
+ repo_path: resolve20(root),
14110
+ manifest_path: join24(resolve20(root), ".project.json"),
13832
14111
  manifest,
13833
14112
  registry: { location, status: "indexed" }
13834
14113
  };
@@ -13849,27 +14128,27 @@ function readProjectInfo(input = {}) {
13849
14128
  info.registry = { location, status: "not-indexed", message: "This project is not indexed. Run pj init --apply from its repository." };
13850
14129
  return info;
13851
14130
  }
13852
- if (resolve19(indexed.repo_path) !== resolve19(root)) {
14131
+ if (resolve20(indexed.repo_path) !== resolve20(root)) {
13853
14132
  info.registry = { location, status: "stale", message: `The registry points to ${indexed.repo_path}; this manifest is at ${root}. Reindex the repository.` };
13854
14133
  }
13855
14134
  return info;
13856
14135
  }
13857
14136
 
13858
14137
  // src/describe/index.ts
13859
- import { existsSync as existsSync17, readFileSync as readFileSync18, readdirSync as readdirSync8, statSync as statSync6 } from "node:fs";
13860
- import { join as join25, resolve as resolve20 } from "node:path";
14138
+ import { existsSync as existsSync17, readFileSync as readFileSync19, readdirSync as readdirSync8, statSync as statSync6 } from "node:fs";
14139
+ import { join as join26, resolve as resolve21 } from "node:path";
13861
14140
  init_project();
13862
14141
 
13863
14142
  // src/describe/activity.ts
13864
- import { spawn as spawn2, spawnSync as spawnSync12 } from "node:child_process";
14143
+ import { spawn as spawn2, spawnSync as spawnSync13 } from "node:child_process";
13865
14144
  import { statSync as statSync5 } from "node:fs";
13866
- import { join as join24 } from "node:path";
14145
+ import { join as join25 } from "node:path";
13867
14146
  var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
13868
14147
  var MAX_DIRTY_STATS = 500;
13869
14148
  var GIT_TIMEOUT_MS = 5e3;
13870
14149
  var GIT_MAX_BUFFER = 16 * 1024 * 1024;
13871
14150
  function git3(repo, args) {
13872
- const result2 = spawnSync12("git", ["-C", repo, ...args], {
14151
+ const result2 = spawnSync13("git", ["-C", repo, ...args], {
13873
14152
  encoding: "utf8",
13874
14153
  timeout: GIT_TIMEOUT_MS,
13875
14154
  maxBuffer: GIT_MAX_BUFFER
@@ -13992,7 +14271,7 @@ function uncommittedSource(repo, paths) {
13992
14271
  let newest = 0;
13993
14272
  for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
13994
14273
  try {
13995
- const mtime = Math.floor(statSync5(join24(repo, path)).mtimeMs / 1e3);
14274
+ const mtime = Math.floor(statSync5(join25(repo, path)).mtimeMs / 1e3);
13996
14275
  if (mtime > newest) newest = mtime;
13997
14276
  } catch {
13998
14277
  }
@@ -14101,7 +14380,7 @@ var CONFIG_FILES = [
14101
14380
  ];
14102
14381
  function readJson(path) {
14103
14382
  try {
14104
- const parsed = JSON.parse(readFileSync18(path, "utf8"));
14383
+ const parsed = JSON.parse(readFileSync19(path, "utf8"));
14105
14384
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
14106
14385
  } catch {
14107
14386
  return void 0;
@@ -14123,7 +14402,7 @@ function describeType(repo) {
14123
14402
  const evidence = [];
14124
14403
  const note = (signal, file) => evidence.push(`${signal} (${file})`);
14125
14404
  for (const marker of LANGUAGE_MARKERS) {
14126
- if (!existsSync17(join25(repo, marker.file))) continue;
14405
+ if (!existsSync17(join26(repo, marker.file))) continue;
14127
14406
  if (!languages.includes(marker.language)) {
14128
14407
  languages.push(marker.language);
14129
14408
  note(marker.language, marker.file);
@@ -14137,9 +14416,9 @@ function describeType(repo) {
14137
14416
  }
14138
14417
  } catch {
14139
14418
  }
14140
- const pkg = readJson(join25(repo, "package.json"));
14419
+ const pkg = readJson(join26(repo, "package.json"));
14141
14420
  if (pkg) {
14142
- if (existsSync17(join25(repo, "tsconfig.json"))) {
14421
+ if (existsSync17(join26(repo, "tsconfig.json"))) {
14143
14422
  const index = languages.indexOf("javascript");
14144
14423
  if (index >= 0) languages.splice(index, 1);
14145
14424
  if (!languages.includes("typescript")) {
@@ -14173,26 +14452,26 @@ function describeType(repo) {
14173
14452
  ["hermes-fleet-host", "agents/hermes"]
14174
14453
  ];
14175
14454
  for (const [role, marker] of roleMarkers) {
14176
- if (!existsSync17(join25(repo, marker))) continue;
14455
+ if (!existsSync17(join26(repo, marker))) continue;
14177
14456
  roles.push(role);
14178
14457
  note(role, marker);
14179
14458
  }
14180
14459
  return { primaryLanguage: languages[0], languages, roles, evidence };
14181
14460
  }
14182
14461
  function describeIdentity(repo, registryPath) {
14183
- const manifestPath = join25(repo, ".project.json");
14462
+ const manifestPath = join26(repo, ".project.json");
14184
14463
  const manifest = readJson(manifestPath);
14185
14464
  const drift = [];
14186
- let record2;
14465
+ let record3;
14187
14466
  let registryReadable = true;
14188
14467
  try {
14189
14468
  const registry = loadProjectRegistry(registryPath);
14190
14469
  const identity = manifest?.project_id ?? manifest?.project_slug;
14191
14470
  const slug = typeof identity === "string" ? identity.toLowerCase() : void 0;
14192
- const resolved = resolve20(repo);
14193
- record2 = (slug ? registry.projects[slug] : void 0) ?? Object.values(registry.projects).find((project) => resolve20(project.repo_path) === resolved);
14471
+ const resolved = resolve21(repo);
14472
+ record3 = (slug ? registry.projects[slug] : void 0) ?? Object.values(registry.projects).find((project) => resolve21(project.repo_path) === resolved);
14194
14473
  const statuses = registry.__registry_status;
14195
- const status = record2 && statuses?.[record2.slug];
14474
+ const status = record3 && statuses?.[record3.slug];
14196
14475
  if (status && status.status !== "ok") {
14197
14476
  drift.push({ note: `canonical manifest is ${status.status}; displaying the last indexed record. ${status.error ?? ""}`, command: "pjangler doctor" });
14198
14477
  }
@@ -14200,14 +14479,14 @@ function describeIdentity(repo, registryPath) {
14200
14479
  registryReadable = false;
14201
14480
  drift.push({ note: `registry unreadable: ${err instanceof Error ? err.message : String(err)}` });
14202
14481
  }
14203
- if (manifest && !record2 && registryReadable) {
14482
+ if (manifest && !record3 && registryReadable) {
14204
14483
  drift.push({ note: ".project.json exists but this repo is not in the pjangler registry" });
14205
14484
  }
14206
- if (record2 && !manifest) {
14207
- drift.push({ note: `registered as ${record2.slug} but .project.json is missing`, command: "pjangler doctor" });
14485
+ if (record3 && !manifest) {
14486
+ drift.push({ note: `registered as ${record3.slug} but .project.json is missing`, command: "pjangler doctor" });
14208
14487
  }
14209
- if (record2 && resolve20(record2.repo_path) !== resolve20(repo)) {
14210
- drift.push({ note: `registry repo_path points elsewhere: ${record2.repo_path}`, command: "pjangler doctor" });
14488
+ if (record3 && resolve21(record3.repo_path) !== resolve21(repo)) {
14489
+ drift.push({ note: `registry repo_path points elsewhere: ${record3.repo_path}`, command: "pjangler doctor" });
14211
14490
  }
14212
14491
  const manifestProvider = manifest?.ticket_provider;
14213
14492
  const provider = manifestProvider ? {
@@ -14216,13 +14495,13 @@ function describeIdentity(repo, registryPath) {
14216
14495
  identifier: manifestProvider.identifier,
14217
14496
  board_id: manifestProvider.board_id,
14218
14497
  state: manifestProvider.state
14219
- } : record2?.ticket_provider;
14220
- if (manifestProvider && record2) {
14498
+ } : record3?.ticket_provider;
14499
+ if (manifestProvider && record3) {
14221
14500
  const fields = [
14222
- ["type", provider?.type, record2.ticket_provider.type],
14223
- ["workspace", provider?.workspace, record2.ticket_provider.workspace],
14224
- ["identifier", provider?.identifier, record2.ticket_provider.identifier],
14225
- ["board_id", provider?.board_id, record2.ticket_provider.board_id]
14501
+ ["type", provider?.type, record3.ticket_provider.type],
14502
+ ["workspace", provider?.workspace, record3.ticket_provider.workspace],
14503
+ ["identifier", provider?.identifier, record3.ticket_provider.identifier],
14504
+ ["board_id", provider?.board_id, record3.ticket_provider.board_id]
14226
14505
  ];
14227
14506
  for (const [field2, fromManifest, fromRegistry] of fields) {
14228
14507
  if ((fromManifest ?? "") === (fromRegistry ?? "")) continue;
@@ -14232,7 +14511,7 @@ function describeIdentity(repo, registryPath) {
14232
14511
  }
14233
14512
  }
14234
14513
  const manifestAgents = manifest?.agents ?? {};
14235
- const agents = record2 ? Object.entries(record2.agents).map(([name, agent]) => ({
14514
+ const agents = record3 ? Object.entries(record3.agents).map(([name, agent]) => ({
14236
14515
  name,
14237
14516
  role: agent.role,
14238
14517
  provisioningState: agent.provisioning_state,
@@ -14245,11 +14524,11 @@ function describeIdentity(repo, registryPath) {
14245
14524
  }));
14246
14525
  return {
14247
14526
  manifest: Boolean(manifest),
14248
- registered: Boolean(record2),
14527
+ registered: Boolean(record3),
14249
14528
  registryPath,
14250
- slug: record2?.slug ?? (manifest?.project_id ?? manifest?.project_slug),
14251
- name: record2?.name ?? manifest?.project_name,
14252
- description: record2?.description ?? manifest?.project_description,
14529
+ slug: record3?.slug ?? (manifest?.project_id ?? manifest?.project_slug),
14530
+ name: record3?.name ?? manifest?.project_name,
14531
+ description: record3?.description ?? manifest?.project_description,
14253
14532
  ticketProvider: provider ? {
14254
14533
  type: provider.type,
14255
14534
  workspace: provider.workspace,
@@ -14273,12 +14552,12 @@ function describeSubsystems(repo, findings) {
14273
14552
  const markers = SUBSYSTEM_MARKERS[metadata.id] ?? [];
14274
14553
  const evidence = metadata.id === "notebook" ? (() => {
14275
14554
  try {
14276
- const manifest = JSON.parse(readFileSync18(join25(repo, ".project.json"), "utf8"));
14555
+ const manifest = JSON.parse(readFileSync19(join26(repo, ".project.json"), "utf8"));
14277
14556
  return manifest.notebook && typeof manifest.notebook === "object" ? [".project.json#notebook"] : [];
14278
14557
  } catch {
14279
14558
  return [];
14280
14559
  }
14281
- })() : markers.filter((marker) => existsSync17(join25(repo, marker)));
14560
+ })() : markers.filter((marker) => existsSync17(join26(repo, marker)));
14282
14561
  const rules = (byRecipe.get(metadata.id) ?? []).map((finding2) => ({
14283
14562
  id: finding2.id,
14284
14563
  title: finding2.title,
@@ -14296,8 +14575,8 @@ function describeSubsystems(repo, findings) {
14296
14575
  function describeNotebook(repo, registryPath) {
14297
14576
  try {
14298
14577
  const registry = loadProjectRegistry(registryPath);
14299
- const project = Object.values(registry.projects).find((entry) => resolve20(entry.repo_path) === resolve20(repo));
14300
- const manifest = existsSync17(join25(repo, ".project.json")) ? JSON.parse(readFileSync18(join25(repo, ".project.json"), "utf8")) : void 0;
14578
+ const project = Object.values(registry.projects).find((entry) => resolve21(entry.repo_path) === resolve21(repo));
14579
+ const manifest = existsSync17(join26(repo, ".project.json")) ? JSON.parse(readFileSync19(join26(repo, ".project.json"), "utf8")) : void 0;
14301
14580
  const declared = Boolean(project?.notebook || manifest?.notebook && typeof manifest.notebook === "object");
14302
14581
  if (!declared) return { declared: false, bindingState: null, notebookId: null, overviewNoteId: null, health: null, remoteCheck: "skip", captureAdmission: null };
14303
14582
  const config = loadEffectiveNotebookConfig(repo, registryPath);
@@ -14315,7 +14594,7 @@ function describeNotebook(repo, registryPath) {
14315
14594
  }
14316
14595
  }
14317
14596
  function describeConfigFiles(repo) {
14318
- return CONFIG_FILES.filter((spec) => existsSync17(join25(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
14597
+ return CONFIG_FILES.filter((spec) => existsSync17(join26(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
14319
14598
  }
14320
14599
  function describeNextSteps(description, findings) {
14321
14600
  const steps = [];
@@ -14393,7 +14672,7 @@ function describeNextSteps(description, findings) {
14393
14672
  return steps;
14394
14673
  }
14395
14674
  async function describeProject(input = {}) {
14396
- const repo = resolve20(input.repoArg ?? process.cwd());
14675
+ const repo = resolve21(input.repoArg ?? process.cwd());
14397
14676
  if (!existsSync17(repo)) throw new Error(`Path does not exist: ${repo}`);
14398
14677
  if (!statSync6(repo).isDirectory()) throw new Error(`Not a directory: ${repo}`);
14399
14678
  const registryPath = input.registryPath ?? projectRegistryPath();
@@ -14582,7 +14861,7 @@ function validateExternalEffectConsent(input, options) {
14582
14861
  return selected;
14583
14862
  }
14584
14863
  function resolveTargetDir(targetDir) {
14585
- const dir = resolve21(targetDir ?? process.cwd());
14864
+ const dir = resolve22(targetDir ?? process.cwd());
14586
14865
  if (!existsSync18(dir)) {
14587
14866
  throw new Error(`Target directory does not exist: ${dir}`);
14588
14867
  }
@@ -14594,7 +14873,7 @@ function resolveTargetDir(targetDir) {
14594
14873
  function resolvePjanglerRoot3() {
14595
14874
  let dir = dirname17(fileURLToPath6(import.meta.url));
14596
14875
  while (dir !== dirname17(dir)) {
14597
- if (existsSync18(join26(dir, "package.json")) && existsSync18(join26(dir, "templates", "commonproject", "copier.yml"))) {
14876
+ if (existsSync18(join27(dir, "package.json")) && existsSync18(join27(dir, "templates", "commonproject", "copier.yml"))) {
14598
14877
  return dir;
14599
14878
  }
14600
14879
  dir = dirname17(dir);
@@ -14889,12 +15168,12 @@ server.registerTool(
14889
15168
  );
14890
15169
  const pjanglerRoot = resolvePjanglerRoot3();
14891
15170
  const projectSlug = validateSafePathSegment(input.projectSlug ?? slugify(input.projectName), "Project slug");
14892
- const explicitTargetDir = input.targetDir ? resolve21(input.targetDir) : void 0;
14893
- const parentDir = resolve21(input.parentDir ?? (explicitTargetDir ? dirname17(explicitTargetDir) : process.cwd()));
15171
+ const explicitTargetDir = input.targetDir ? resolve22(input.targetDir) : void 0;
15172
+ const parentDir = resolve22(input.parentDir ?? (explicitTargetDir ? dirname17(explicitTargetDir) : process.cwd()));
14894
15173
  if (!existsSync18(parentDir) || !statSync7(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
14895
15174
  const targetDir = resolveContainedPath(
14896
15175
  parentDir,
14897
- explicitTargetDir ?? join26(parentDir, projectSlug),
15176
+ explicitTargetDir ?? join27(parentDir, projectSlug),
14898
15177
  "Bootstrap target"
14899
15178
  );
14900
15179
  const overwrite = input.overwrite ?? input.force ?? false;
@@ -15017,7 +15296,7 @@ server.registerTool(
15017
15296
  registryPath: input.registryPath,
15018
15297
  force: input.force ?? false,
15019
15298
  overwrite: input.force ?? false,
15020
- scaffold: !(input.targetDir && existsSync18(join26(resolve21(input.targetDir), ".git")))
15299
+ scaffold: !(input.targetDir && existsSync18(join27(resolve22(input.targetDir), ".git")))
15021
15300
  });
15022
15301
  if (!input.apply) return asText(publicCompositeProjectResponse(publicProjectPlan(plan), plan));
15023
15302
  const preflight = await preflightProjectApply(plan, resolvePjanglerRoot3());