@delorenj/pjangler 1.5.49 → 1.5.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +1105 -637
  2. package/dist/mcp-server.js +981 -522
  3. package/package.json +3 -2
@@ -437,7 +437,7 @@ var init_executionBinding = __esm({
437
437
  });
438
438
 
439
439
  // src/project/manifestIndex.ts
440
- import { basename as basename5, dirname as dirname9, join as join12, resolve as resolve7 } from "node:path";
440
+ import { basename as basename6, dirname as dirname9, join as join13, resolve as resolve7 } from "node:path";
441
441
  function normalizeProjectId2(value) {
442
442
  try {
443
443
  return normalizeProjectId(value);
@@ -484,24 +484,24 @@ var init_manifestIndex = __esm({
484
484
  });
485
485
 
486
486
  // src/lifecycle/preflight.ts
487
- import { createHash as createHash8 } from "node:crypto";
488
- import { accessSync, constants, existsSync as existsSync9, lstatSync as lstatSync10, readFileSync as readFileSync11, readdirSync as readdirSync8, realpathSync as realpathSync5, statSync as statSync3 } from "node:fs";
487
+ import { createHash as createHash9 } from "node:crypto";
488
+ import { accessSync, constants as constants2, existsSync as existsSync9, lstatSync as lstatSync11, readFileSync as readFileSync12, readdirSync as readdirSync9, realpathSync as realpathSync5, statSync as statSync3 } from "node:fs";
489
489
  import { tmpdir as tmpdir2, userInfo } from "node:os";
490
- import { basename as basename6, delimiter, dirname as dirname10, isAbsolute as isAbsolute5, join as join13, relative as relative10, resolve as resolve8 } from "node:path";
490
+ import { basename as basename7, delimiter, dirname as dirname10, isAbsolute as isAbsolute5, join as join14, relative as relative10, resolve as resolve8 } from "node:path";
491
491
  import YAML5 from "yaml";
492
492
  function containedBy(parent, candidate) {
493
493
  const rel = relative10(resolve8(parent), resolve8(candidate));
494
494
  return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
495
495
  }
496
496
  function accountMiseShimsDir(home) {
497
- return resolve8(join13(home, ".local", "share", "mise", "shims"));
497
+ return resolve8(join14(home, ".local", "share", "mise", "shims"));
498
498
  }
499
499
  function inactiveMiseShim(candidate, shimsDir) {
500
500
  if (dirname10(candidate) !== shimsDir) return void 0;
501
501
  try {
502
- if (!lstatSync10(candidate).isSymbolicLink()) return void 0;
502
+ if (!lstatSync11(candidate).isSymbolicLink()) return void 0;
503
503
  const realPath = realpathSync5(candidate);
504
- if (basename6(realPath) !== "mise") return void 0;
504
+ if (basename7(realPath) !== "mise") return void 0;
505
505
  return { path: candidate, realPath, reason: "inactive-mise-shim" };
506
506
  } catch {
507
507
  return void 0;
@@ -513,8 +513,8 @@ function resolveCopierOnPath(env2, shimsDir) {
513
513
  const entry = rawEntry || process.cwd();
514
514
  const candidate = resolve8(entry, process.platform === "win32" ? "copier.exe" : "copier");
515
515
  try {
516
- accessSync(candidate, constants.X_OK);
517
- const stat = lstatSync10(candidate);
516
+ accessSync(candidate, constants2.X_OK);
517
+ const stat = lstatSync11(candidate);
518
518
  if (!stat.isFile() && !stat.isSymbolicLink()) continue;
519
519
  } catch {
520
520
  continue;
@@ -532,7 +532,7 @@ function describeSkipped(skipped) {
532
532
  return skipped.map((entry) => `${entry.path} -> ${entry.realPath}`).join(", ");
533
533
  }
534
534
  function sha256(path) {
535
- return createHash8("sha256").update(readFileSync11(path)).digest("base64url");
535
+ return createHash9("sha256").update(readFileSync12(path)).digest("base64url");
536
536
  }
537
537
  function fingerprint(path) {
538
538
  const absolute = resolve8(path);
@@ -570,7 +570,7 @@ function sameFingerprint(expected) {
570
570
  function consoleScriptContract(path) {
571
571
  let text;
572
572
  try {
573
- text = readFileSync11(path, "utf8").slice(0, 32 * 1024);
573
+ text = readFileSync12(path, "utf8").slice(0, 32 * 1024);
574
574
  } catch (error) {
575
575
  return { ok: false, error: `cannot read Copier launcher: ${error instanceof Error ? error.message : String(error)}` };
576
576
  }
@@ -581,7 +581,7 @@ function consoleScriptContract(path) {
581
581
  return { ok: false, error: "Copier launcher must use one absolute Python interpreter" };
582
582
  }
583
583
  const interpreterPath = resolve8(shebang[0]);
584
- const interpreter = basename6(interpreterPath);
584
+ const interpreter = basename7(interpreterPath);
585
585
  if (!/^python(?:\d+(?:\.\d+)*)?$/.test(interpreter)) {
586
586
  return { ok: false, error: "Copier launcher is not an absolute Python console script" };
587
587
  }
@@ -592,16 +592,16 @@ function consoleScriptContract(path) {
592
592
  }
593
593
  function defaultUvToolRoots(home) {
594
594
  return [
595
- join13(home, ".local", "share", "uv", "tools", "copier"),
596
- join13(home, "Library", "Application Support", "uv", "tools", "copier")
595
+ join14(home, ".local", "share", "uv", "tools", "copier"),
596
+ join14(home, "Library", "Application Support", "uv", "tools", "copier")
597
597
  ].map((path) => resolve8(path));
598
598
  }
599
599
  function locateUvSitePackages(toolRoot) {
600
- const lib = join13(toolRoot, "lib");
600
+ const lib = join14(toolRoot, "lib");
601
601
  const candidates = [];
602
- for (const entry of readdirSync8(lib, { withFileTypes: true })) {
602
+ for (const entry of readdirSync9(lib, { withFileTypes: true })) {
603
603
  if (!entry.isDirectory() || !/^python\d+(?:\.\d+)*$/.test(entry.name)) continue;
604
- const sitePackages = join13(lib, entry.name, "site-packages");
604
+ const sitePackages = join14(lib, entry.name, "site-packages");
605
605
  if (existsSync9(sitePackages)) candidates.push(sitePackages);
606
606
  }
607
607
  if (candidates.length !== 1) throw new Error(`expected one UV Copier site-packages directory, found ${candidates.length}`);
@@ -618,13 +618,13 @@ function parseRecordLine(line) {
618
618
  };
619
619
  }
620
620
  function attestUvCopier(candidate, realCandidate, home) {
621
- const toolRoot = defaultUvToolRoots(home).find((root) => realCandidate === join13(root, "bin", "copier"));
621
+ const toolRoot = defaultUvToolRoots(home).find((root) => realCandidate === join14(root, "bin", "copier"));
622
622
  if (!toolRoot) {
623
623
  return { ok: false, error: `refusing untrusted PATH-shadowed Copier executable: ${candidate}` };
624
624
  }
625
625
  const supportedEntries = new Set([
626
- join13(home, ".local", "bin", "copier"),
627
- join13(toolRoot, "bin", "copier")
626
+ join14(home, ".local", "bin", "copier"),
627
+ join14(toolRoot, "bin", "copier")
628
628
  ].map((path) => resolve8(path)));
629
629
  if (!supportedEntries.has(resolve8(candidate))) {
630
630
  return { ok: false, error: `refusing non-canonical UV Copier entry point: ${candidate}` };
@@ -634,46 +634,46 @@ function attestUvCopier(candidate, realCandidate, home) {
634
634
  if (!launcher.ok || !launcher.interpreter) {
635
635
  return { ...launcher, executable: realCandidate, realExecutable: realCandidate };
636
636
  }
637
- const expectedInterpreter = join13(toolRoot, "bin", basename6(launcher.interpreter));
637
+ const expectedInterpreter = join14(toolRoot, "bin", basename7(launcher.interpreter));
638
638
  if (resolve8(launcher.interpreter) !== resolve8(expectedInterpreter)) {
639
639
  return { ok: false, error: "UV Copier launcher interpreter is outside the attested tool environment" };
640
640
  }
641
641
  const interpreterReal = realpathSync5(launcher.interpreter);
642
642
  const uvPythonRoots = [
643
- join13(home, ".local", "share", "uv", "python"),
644
- join13(home, "Library", "Application Support", "uv", "python")
643
+ join14(home, ".local", "share", "uv", "python"),
644
+ join14(home, "Library", "Application Support", "uv", "python")
645
645
  ];
646
646
  if (!uvPythonRoots.some((root) => containedBy(root, interpreterReal))) {
647
647
  return { ok: false, error: "UV Copier interpreter is not managed by the canonical UV Python installation" };
648
648
  }
649
- const receiptPath = join13(toolRoot, "uv-receipt.toml");
650
- const receipt = readFileSync11(receiptPath, "utf8");
649
+ const receiptPath = join14(toolRoot, "uv-receipt.toml");
650
+ const receipt = readFileSync12(receiptPath, "utf8");
651
651
  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)) {
652
652
  return { ok: false, error: "UV tool receipt does not bind the copier entry point to the Copier package" };
653
653
  }
654
654
  const sitePackages = locateUvSitePackages(toolRoot);
655
- const distInfos = readdirSync8(sitePackages, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^copier-[^-]+\.dist-info$/i.test(entry.name)).map((entry) => join13(sitePackages, entry.name));
655
+ const distInfos = readdirSync9(sitePackages, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^copier-[^-]+\.dist-info$/i.test(entry.name)).map((entry) => join14(sitePackages, entry.name));
656
656
  if (distInfos.length !== 1) {
657
657
  return { ok: false, error: `expected one installed Copier distribution, found ${distInfos.length}` };
658
658
  }
659
659
  const distInfo = realpathSync5(distInfos[0]);
660
- const metadataPath = join13(distInfo, "METADATA");
661
- const entryPointsPath = join13(distInfo, "entry_points.txt");
662
- const recordPath = join13(distInfo, "RECORD");
663
- const metadata = readFileSync11(metadataPath, "utf8");
660
+ const metadataPath = join14(distInfo, "METADATA");
661
+ const entryPointsPath = join14(distInfo, "entry_points.txt");
662
+ const recordPath = join14(distInfo, "RECORD");
663
+ const metadata = readFileSync12(metadataPath, "utf8");
664
664
  const name = metadata.match(/^Name:\s*(.+)$/mi)?.[1]?.trim();
665
665
  const version = metadata.match(/^Version:\s*(.+)$/mi)?.[1]?.trim();
666
666
  if (name?.toLowerCase() !== "copier" || !version || !/^9(?:\.|$)/.test(version)) {
667
667
  return { ok: false, error: "installed distribution is not a Copier 9 package" };
668
668
  }
669
- const entryPoints = readFileSync11(entryPointsPath, "utf8");
669
+ const entryPoints = readFileSync12(entryPointsPath, "utf8");
670
670
  if (!/^copier\s*=\s*copier\.__main__:CopierApp\.run\s*$/m.test(entryPoints)) {
671
671
  return { ok: false, error: "installed Copier distribution has an unexpected console entry point" };
672
672
  }
673
- const recordEntries = readFileSync11(recordPath, "utf8").split(/\r?\n/).map(parseRecordLine).filter((entry) => Boolean(entry));
673
+ const recordEntries = readFileSync12(recordPath, "utf8").split(/\r?\n/).map(parseRecordLine).filter((entry) => Boolean(entry));
674
674
  const selected = recordEntries.filter((entry) => {
675
675
  const normalized = entry.relativePath.replaceAll("\\", "/");
676
- return normalized.startsWith("copier/") || normalized === `${basename6(distInfo)}/METADATA` || normalized === `${basename6(distInfo)}/entry_points.txt` || normalized === "../../../bin/copier";
676
+ return normalized.startsWith("copier/") || normalized === `${basename7(distInfo)}/METADATA` || normalized === `${basename7(distInfo)}/entry_points.txt` || normalized === "../../../bin/copier";
677
677
  });
678
678
  if (!selected.some((entry) => entry.relativePath === "../../../bin/copier") || !selected.some((entry) => entry.relativePath.replaceAll("\\", "/") === "copier/__main__.py")) {
679
679
  return { ok: false, error: "Copier RECORD does not bind its launcher and package entry point" };
@@ -690,7 +690,7 @@ function attestUvCopier(candidate, realCandidate, home) {
690
690
  }
691
691
  attestedFiles.set(resolve8(path), fingerprint(path));
692
692
  }
693
- for (const path of [realCandidate, join13(toolRoot, "pyvenv.cfg"), receiptPath, metadataPath, entryPointsPath, recordPath]) {
693
+ for (const path of [realCandidate, join14(toolRoot, "pyvenv.cfg"), receiptPath, metadataPath, entryPointsPath, recordPath]) {
694
694
  attestedFiles.set(resolve8(path), fingerprint(path));
695
695
  }
696
696
  const identity = {
@@ -765,18 +765,18 @@ function regularContainedFile(root, path, label) {
765
765
  const rootReal = realpathSync5(root);
766
766
  const fileReal = realpathSync5(path);
767
767
  if (!containedBy(rootReal, fileReal)) return { ok: false, error: `${label} escapes its vendored template root` };
768
- if (!lstatSync10(path).isFile()) return { ok: false, error: `${label} is not a regular file` };
768
+ if (!lstatSync11(path).isFile()) return { ok: false, error: `${label} is not a regular file` };
769
769
  return { ok: true };
770
770
  } catch (error) {
771
771
  return { ok: false, error: `${label} is unavailable: ${error instanceof Error ? error.message : String(error)}` };
772
772
  }
773
773
  }
774
774
  function parseCopierConfig(templateRoot, label) {
775
- const configPath = join13(templateRoot, "copier.yml");
775
+ const configPath = join14(templateRoot, "copier.yml");
776
776
  const file = regularContainedFile(templateRoot, configPath, `${label} copier.yml`);
777
777
  if (!file.ok) return { result: file };
778
778
  try {
779
- const parsed = YAML5.parse(readFileSync11(configPath, "utf8"));
779
+ const parsed = YAML5.parse(readFileSync12(configPath, "utf8"));
780
780
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
781
781
  return { result: { ok: false, error: `${label} copier.yml must contain a mapping` } };
782
782
  }
@@ -794,13 +794,13 @@ function parseCopierConfig(templateRoot, label) {
794
794
  }
795
795
  function requireFiles(templateRoot, files, label) {
796
796
  for (const rel of files) {
797
- const result2 = regularContainedFile(templateRoot, join13(templateRoot, rel), `${label} ${rel}`);
797
+ const result2 = regularContainedFile(templateRoot, join14(templateRoot, rel), `${label} ${rel}`);
798
798
  if (!result2.ok) return result2;
799
799
  }
800
800
  return { ok: true };
801
801
  }
802
802
  function preflightCommonProjectTemplate(pjanglerRoot) {
803
- const templateRoot = join13(resolve8(pjanglerRoot), "templates", "commonproject");
803
+ const templateRoot = join14(resolve8(pjanglerRoot), "templates", "commonproject");
804
804
  const parsed = parseCopierConfig(templateRoot, "CommonProject template");
805
805
  if (!parsed.result.ok) return parsed.result;
806
806
  const files = requireFiles(templateRoot, [
@@ -811,7 +811,7 @@ function preflightCommonProjectTemplate(pjanglerRoot) {
811
811
  "template/mise.toml.jinja"
812
812
  ], "CommonProject template");
813
813
  if (!files.ok) return files;
814
- const projectJson = readFileSync11(join13(templateRoot, "template", ".project.json.jinja"), "utf8");
814
+ const projectJson = readFileSync12(join14(templateRoot, "template", ".project.json.jinja"), "utf8");
815
815
  for (const key of ["project_name", "repo_path", "ticket_provider", "agents"]) {
816
816
  if (!projectJson.includes(`"${key}"`)) return { ok: false, error: `CommonProject projection is missing ${key}` };
817
817
  }
@@ -836,12 +836,12 @@ var init_preflight = __esm({
836
836
  });
837
837
 
838
838
  // src/project/boardUrl.ts
839
- import { existsSync as existsSync10, readFileSync as readFileSync12, statSync as statSync4 } from "node:fs";
840
- import { dirname as dirname11, isAbsolute as isAbsolute6, join as join14, resolve as resolve9 } from "node:path";
839
+ import { existsSync as existsSync10, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
840
+ import { dirname as dirname11, isAbsolute as isAbsolute6, join as join15, resolve as resolve9 } from "node:path";
841
841
  function findProjectRoot(from) {
842
842
  let dir = resolve9(from);
843
843
  for (; ; ) {
844
- if (existsSync10(join14(dir, ".project.json"))) return dir;
844
+ if (existsSync10(join15(dir, ".project.json"))) return dir;
845
845
  const parent = dirname11(dir);
846
846
  if (parent === dir) return void 0;
847
847
  dir = parent;
@@ -854,8 +854,8 @@ var init_boardUrl = __esm({
854
854
  });
855
855
 
856
856
  // src/utils/version.ts
857
- import { readFileSync as readFileSync13 } from "node:fs";
858
- import { dirname as dirname12, join as join15 } from "node:path";
857
+ import { readFileSync as readFileSync14 } from "node:fs";
858
+ import { dirname as dirname12, join as join16 } from "node:path";
859
859
  import { fileURLToPath } from "node:url";
860
860
  var PJANGLER_VERSION;
861
861
  var init_version = __esm({
@@ -866,7 +866,7 @@ var init_version = __esm({
866
866
  let dir = dirname12(fileURLToPath(import.meta.url));
867
867
  for (let i = 0; i < 4; i++) {
868
868
  try {
869
- const raw = readFileSync13(join15(dir, "package.json"), "utf8");
869
+ const raw = readFileSync14(join16(dir, "package.json"), "utf8");
870
870
  return JSON.parse(raw).version ?? "0.0.0";
871
871
  } catch {
872
872
  const parent = dirname12(dir);
@@ -896,9 +896,9 @@ var init_boardQuery = __esm({
896
896
 
897
897
  // src/project/index.ts
898
898
  import { spawnSync as spawnSync6 } from "node:child_process";
899
- import { chmodSync as chmodSync4, closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync11, fchmodSync, fsyncSync, lstatSync as lstatSync11, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync14, realpathSync as realpathSync6, renameSync as renameSync5, rmSync as rmSync6, statSync as statSync5, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
899
+ import { chmodSync as chmodSync4, closeSync as closeSync3, copyFileSync as copyFileSync2, existsSync as existsSync11, fchmodSync as fchmodSync2, fsyncSync, lstatSync as lstatSync12, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, openSync as openSync3, readFileSync as readFileSync15, realpathSync as realpathSync6, renameSync as renameSync5, rmSync as rmSync6, statSync as statSync5, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "node:fs";
900
900
  import { homedir as homedir5, tmpdir as tmpdir3 } from "node:os";
901
- import { basename as basename7, delimiter as delimiter2, dirname as dirname13, isAbsolute as isAbsolute7, join as join16, relative as relative11, resolve as resolve10, sep as sep3, win32 } from "node:path";
901
+ import { basename as basename8, delimiter as delimiter2, dirname as dirname13, isAbsolute as isAbsolute7, join as join17, relative as relative11, resolve as resolve10, sep as sep3, win32 } from "node:path";
902
902
  import { fileURLToPath as fileURLToPath2 } from "node:url";
903
903
  import YAML6 from "yaml";
904
904
  function normalizeProjectType(value, label = "Project type") {
@@ -908,7 +908,7 @@ function normalizeProjectType(value, label = "Project type") {
908
908
  }
909
909
  function looksLikeObsidianPlugin(dir) {
910
910
  try {
911
- const manifest = JSON.parse(readFileSync14(join16(dir, "manifest.json"), "utf8"));
911
+ const manifest = JSON.parse(readFileSync15(join17(dir, "manifest.json"), "utf8"));
912
912
  return isRecord2(manifest) && typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
913
913
  } catch {
914
914
  return false;
@@ -916,10 +916,10 @@ function looksLikeObsidianPlugin(dir) {
916
916
  }
917
917
  function assertTemplateOffersProjectType(pjanglerRoot, projectType) {
918
918
  if (projectType === "base") return;
919
- const copierYml = join16(pjanglerRoot, "templates", "commonproject", "copier.yml");
919
+ const copierYml = join17(pjanglerRoot, "templates", "commonproject", "copier.yml");
920
920
  let choices;
921
921
  try {
922
- const parsed = YAML6.parse(readFileSync14(copierYml, "utf8"));
922
+ const parsed = YAML6.parse(readFileSync15(copierYml, "utf8"));
923
923
  choices = isRecord2(parsed) && isRecord2(parsed.project_type) ? parsed.project_type.choices : void 0;
924
924
  } catch {
925
925
  choices = void 0;
@@ -942,9 +942,9 @@ function carriedAutomation(automation) {
942
942
  return Object.keys(rest).length ? { automation: rest } : {};
943
943
  }
944
944
  function synchronizeCopierIdentity(manifestPath, manifest) {
945
- const answersPath = join16(dirname13(manifestPath), ".copier-answers.yml");
945
+ const answersPath = join17(dirname13(manifestPath), ".copier-answers.yml");
946
946
  if (!existsSync11(answersPath)) return [];
947
- const current = readFileSync14(answersPath, "utf8");
947
+ const current = readFileSync15(answersPath, "utf8");
948
948
  const document = YAML6.parseDocument(current);
949
949
  if (document.errors.length) return [];
950
950
  const name = String(document.get("project_name") ?? "");
@@ -954,7 +954,7 @@ function synchronizeCopierIdentity(manifestPath, manifest) {
954
954
  document.set("project_description", manifest.project_description);
955
955
  const next = String(document);
956
956
  if (next === current) return [];
957
- writeFileSync9(answersPath, next, "utf8");
957
+ writeFileSync10(answersPath, next, "utf8");
958
958
  return [answersPath];
959
959
  }
960
960
  function projectRegistryPath(env2 = process.env) {
@@ -978,7 +978,7 @@ function loadProjectRegistry(path = projectRegistryPath()) {
978
978
  return registry2;
979
979
  }
980
980
  if (!existsSync11(path)) return emptyProjectRegistry();
981
- const raw = YAML6.parse(readFileSync14(path, "utf8"));
981
+ const raw = YAML6.parse(readFileSync15(path, "utf8"));
982
982
  if (raw == null) return emptyProjectRegistry();
983
983
  if (!isRecord2(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
984
984
  const registry = raw;
@@ -1046,11 +1046,11 @@ function mergeYamlMapping(document, path, desired, ownedKeys = []) {
1046
1046
  }
1047
1047
  function fsyncDirectory(path) {
1048
1048
  try {
1049
- const fd = openSync2(path, "r");
1049
+ const fd = openSync3(path, "r");
1050
1050
  try {
1051
1051
  fsyncSync(fd);
1052
1052
  } finally {
1053
- closeSync2(fd);
1053
+ closeSync3(fd);
1054
1054
  }
1055
1055
  } catch {
1056
1056
  }
@@ -1064,14 +1064,14 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1064
1064
  return;
1065
1065
  }
1066
1066
  validateProjectRegistry(registry);
1067
- mkdirSync7(dirname13(path), { recursive: true });
1067
+ mkdirSync8(dirname13(path), { recursive: true });
1068
1068
  let text;
1069
1069
  let mode = 420;
1070
1070
  if (existsSync11(path)) {
1071
- const stat = lstatSync11(path);
1071
+ const stat = lstatSync12(path);
1072
1072
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Project registry must be a regular file: ${path}`);
1073
1073
  mode = stat.mode & 511;
1074
- const current = readFileSync14(path, "utf8");
1074
+ const current = readFileSync15(path, "utf8");
1075
1075
  const document = YAML6.parseDocument(current);
1076
1076
  if (document.errors.length) throw new Error(`Project registry YAML is invalid: ${path}`);
1077
1077
  setYamlLeaf(document, ["schema_version"], registry.schema_version);
@@ -1092,22 +1092,22 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1092
1092
  const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
1093
1093
  let fd;
1094
1094
  try {
1095
- fd = openSync2(temp, "wx", mode);
1096
- writeFileSync9(fd, text, "utf8");
1097
- fchmodSync(fd, mode);
1095
+ fd = openSync3(temp, "wx", mode);
1096
+ writeFileSync10(fd, text, "utf8");
1097
+ fchmodSync2(fd, mode);
1098
1098
  fsyncSync(fd);
1099
- closeSync2(fd);
1099
+ closeSync3(fd);
1100
1100
  fd = void 0;
1101
1101
  renameSync5(temp, path);
1102
1102
  chmodSync4(path, mode);
1103
1103
  fsyncDirectory(dirname13(path));
1104
1104
  } catch (error) {
1105
1105
  if (fd !== void 0) try {
1106
- closeSync2(fd);
1106
+ closeSync3(fd);
1107
1107
  } catch {
1108
1108
  }
1109
1109
  try {
1110
- unlinkSync6(temp);
1110
+ unlinkSync7(temp);
1111
1111
  } catch {
1112
1112
  }
1113
1113
  throw error;
@@ -1215,18 +1215,18 @@ function ticketProviderKeyVars(provider, workspace) {
1215
1215
  return ticketProviderKeyGroups(provider, workspace).flat();
1216
1216
  }
1217
1217
  function ticketProviderSecretsPath(env2 = process.env) {
1218
- const base = env2.XDG_CONFIG_HOME || join16(env2.HOME || homedir5(), ".config");
1219
- return join16(base, "zshyzsh", "secrets.zsh");
1218
+ const base = env2.XDG_CONFIG_HOME || join17(env2.HOME || homedir5(), ".config");
1219
+ return join17(base, "zshyzsh", "secrets.zsh");
1220
1220
  }
1221
1221
  function ticketProviderFleetEnvPath(env2 = process.env) {
1222
- return env2.HERMES_FLEET_ENV?.trim() || join16(env2.HOME || homedir5(), ".hermes", "fleet.env");
1222
+ return env2.HERMES_FLEET_ENV?.trim() || join17(env2.HOME || homedir5(), ".hermes", "fleet.env");
1223
1223
  }
1224
1224
  function readShellAssignments(path, keys) {
1225
1225
  const found = {};
1226
1226
  if (!existsSync11(path)) return found;
1227
1227
  let text;
1228
1228
  try {
1229
- text = readFileSync14(path, "utf8");
1229
+ text = readFileSync15(path, "utf8");
1230
1230
  } catch {
1231
1231
  return found;
1232
1232
  }
@@ -1262,7 +1262,7 @@ function resolveTicketProviderCredentials(input) {
1262
1262
  }
1263
1263
  }
1264
1264
  const candidates = [];
1265
- if (input.repoPath) candidates.push(join16(input.repoPath, ".env"));
1265
+ if (input.repoPath) candidates.push(join17(input.repoPath, ".env"));
1266
1266
  candidates.push(ticketProviderFleetEnvPath(env2));
1267
1267
  candidates.push(ticketProviderSecretsPath(env2));
1268
1268
  for (const candidate of candidates) {
@@ -1280,21 +1280,26 @@ function resolveTicketProviderAdapter(provider, env2 = process.env) {
1280
1280
  const file = `${provider}.sh`;
1281
1281
  const candidates = [];
1282
1282
  const override = env2[TICKET_PROVIDER_ADAPTERS_ENV];
1283
- if (override) candidates.push(join16(override, file));
1284
- const relativeRoot = join16("krebs", "adapters", "tp");
1283
+ if (override) candidates.push(join17(override, file));
1284
+ const relativeRoot = join17("krebs", "adapters", "tp");
1285
1285
  try {
1286
1286
  let dir = dirname13(fileURLToPath2(import.meta.url));
1287
1287
  for (let depth = 0; depth < 8; depth++) {
1288
- candidates.push(join16(dir, relativeRoot, file));
1288
+ candidates.push(join17(dir, relativeRoot, file));
1289
1289
  const parent = dirname13(dir);
1290
1290
  if (parent === dir) break;
1291
1291
  dir = parent;
1292
1292
  }
1293
1293
  } catch {
1294
1294
  }
1295
- candidates.push(join16(homedir5(), "code", "33GOD", relativeRoot, file));
1295
+ candidates.push(join17(homedir5(), "code", "33GOD", relativeRoot, file));
1296
1296
  return candidates.find((candidate) => existsSync11(candidate));
1297
1297
  }
1298
+ function providerBoardName(provider, name) {
1299
+ if (provider !== "plane") return name;
1300
+ const cleaned = name.replace(/[^\p{L}\p{N}_\s]+/gu, " ").replace(/\s+/g, " ").trim();
1301
+ return cleaned || name;
1302
+ }
1298
1303
  function provisionTicketProviderBoard(action, env2 = process.env) {
1299
1304
  const provider = action.provider;
1300
1305
  const groups = ticketProviderKeyGroups(provider, action.workspace);
@@ -1310,7 +1315,7 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
1310
1315
  ok: true,
1311
1316
  skipped: true,
1312
1317
  logs: [
1313
- `ticket-provider: ${names} not set; skipping ${provider} board creation (state stays "planned"). Set it in the environment, ${join16(action.repoPath, ".env")}, ${ticketProviderFleetEnvPath(env2)}, or ${ticketProviderSecretsPath(env2)} \u2014 or pass --board-id to link an existing board.`
1318
+ `ticket-provider: ${names} not set; skipping ${provider} board creation (state stays "planned"). Set it in the environment, ${join17(action.repoPath, ".env")}, ${ticketProviderFleetEnvPath(env2)}, or ${ticketProviderSecretsPath(env2)} \u2014 or pass --board-id to link an existing board.`
1314
1319
  ]
1315
1320
  };
1316
1321
  }
@@ -1324,12 +1329,12 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
1324
1329
  };
1325
1330
  }
1326
1331
  const redact = (text) => Object.values(values).reduce((acc, secret) => secret ? acc.split(secret).join("***") : acc, text);
1327
- const staging = mkdtempSync2(join16(tmpdir3(), "pjangler-tp-"));
1332
+ const staging = mkdtempSync3(join17(tmpdir3(), "pjangler-tp-"));
1328
1333
  try {
1329
- const providersDir = join16(staging, ".tp", "adapters");
1330
- mkdirSync7(providersDir, { recursive: true });
1331
- writeFileSync9(
1332
- join16(staging, ".project.json"),
1334
+ const providersDir = join17(staging, ".tp", "adapters");
1335
+ mkdirSync8(providersDir, { recursive: true });
1336
+ writeFileSync10(
1337
+ join17(staging, ".project.json"),
1333
1338
  `${JSON.stringify(
1334
1339
  {
1335
1340
  project_name: action.boardName,
@@ -1348,7 +1353,7 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
1348
1353
  `,
1349
1354
  "utf8"
1350
1355
  );
1351
- const staged = join16(providersDir, `${provider}.sh`);
1356
+ const staged = join17(providersDir, `${provider}.sh`);
1352
1357
  copyFileSync2(adapter, staged);
1353
1358
  const childEnv = { ...env2, ...values, TICKET_PROVIDER: provider };
1354
1359
  if (provider === "plane" && action.workspace) childEnv.PLANE_WORKSPACE = action.workspace;
@@ -1437,7 +1442,7 @@ function prospectiveRealPath(path) {
1437
1442
  while (!existsSync11(cursor)) {
1438
1443
  const parent = dirname13(cursor);
1439
1444
  if (parent === cursor) return resolve10(path);
1440
- suffix.unshift(basename7(cursor));
1445
+ suffix.unshift(basename8(cursor));
1441
1446
  cursor = parent;
1442
1447
  }
1443
1448
  return resolve10(realpathSync6(cursor), ...suffix);
@@ -1464,7 +1469,7 @@ function resolveAgentHooksLayer2(input, env2 = process.env) {
1464
1469
  const override = env2.PJ_AGENT_HOOKS_LAYER;
1465
1470
  if (override === "0" || override === "false") return false;
1466
1471
  if (override === "1" || override === "true") return true;
1467
- return !existsSync11(join16(homedir5(), ".agents", "hooks"));
1472
+ return !existsSync11(join17(homedir5(), ".agents", "hooks"));
1468
1473
  }
1469
1474
  function jsonStable(value) {
1470
1475
  return JSON.stringify(value, (_key, item) => isRecord2(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item);
@@ -1497,10 +1502,10 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1497
1502
  const expanded = expandHome(sourceSkill);
1498
1503
  const direct = resolve10(expanded);
1499
1504
  if (existsSync11(direct)) return direct;
1500
- const name = basename7(sourceSkill);
1505
+ const name = basename8(sourceSkill);
1501
1506
  const roots = sourceSkillRoots(env2);
1502
1507
  for (const root of roots) {
1503
- const candidate = join16(root, name);
1508
+ const candidate = join17(root, name);
1504
1509
  if (existsSync11(candidate)) return candidate;
1505
1510
  }
1506
1511
  const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
@@ -1510,8 +1515,8 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1510
1515
  function planProjectInit(input) {
1511
1516
  if (!input.name.trim()) throw new Error("Project name is required");
1512
1517
  const targetDir = resolve10(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
1513
- const localManifestPath = join16(targetDir, ".project.json");
1514
- const localManifestContent = existsSync11(localManifestPath) ? readFileSync14(localManifestPath, "utf8") : void 0;
1518
+ const localManifestPath = join17(targetDir, ".project.json");
1519
+ const localManifestContent = existsSync11(localManifestPath) ? readFileSync15(localManifestPath, "utf8") : void 0;
1515
1520
  const localManifest = localManifestContent === void 0 ? void 0 : JSON.parse(localManifestContent);
1516
1521
  const localId = localManifest?.project_id ?? localManifest?.project_slug;
1517
1522
  const slug = normalizeProjectId(input.projectSlug ?? localId ?? slugifyProjectName(input.name));
@@ -1539,7 +1544,7 @@ function planProjectInit(input) {
1539
1544
  const recordedTypeValue = existing?.template?.commonproject?.project_type;
1540
1545
  const recordedType = recordedTypeValue === void 0 ? void 0 : normalizeProjectType(recordedTypeValue, `Recorded project type of ${slug}`);
1541
1546
  const requestedType = input.projectType === void 0 ? void 0 : normalizeProjectType(input.projectType);
1542
- const freshRender = scaffold && localManifest === void 0 && !existsSync11(join16(targetDir, ".copier-answers.yml"));
1547
+ const freshRender = scaffold && localManifest === void 0 && !existsSync11(join17(targetDir, ".copier-answers.yml"));
1543
1548
  if (requestedType && !freshRender && requestedType !== (recordedType ?? "base")) {
1544
1549
  const adoptingPlugin = !scaffold && recordedType === void 0 && requestedType === "obsidian-plugin" && looksLikeObsidianPlugin(targetDir);
1545
1550
  if (!adoptingPlugin) {
@@ -1637,7 +1642,7 @@ function planProjectInit(input) {
1637
1642
  }));
1638
1643
  }
1639
1644
  actions.push(
1640
- { kind: "project.write-manifest", path: join16(targetDir, ".project.json"), manifest, expectedContent: localManifestContent },
1645
+ { kind: "project.write-manifest", path: join17(targetDir, ".project.json"), manifest, expectedContent: localManifestContent },
1641
1646
  {
1642
1647
  kind: "ticket-provider.create-or-link",
1643
1648
  enabled: boardEnabled,
@@ -1646,11 +1651,11 @@ function planProjectInit(input) {
1646
1651
  workspace: project.ticket_provider.workspace ?? "33god",
1647
1652
  identifier,
1648
1653
  repoPath: targetDir,
1649
- boardName: project.name,
1654
+ boardName: providerBoardName(project.ticket_provider.type, project.name),
1650
1655
  description: project.description || `Ticket board for ${project.slug}`,
1651
1656
  boardId: project.ticket_provider.board_id ?? "",
1652
1657
  state: project.ticket_provider.board_id ? "linked" : "planned",
1653
- reason: skipPlane ? "ticket-provider action disabled by skipPlane=true (--skip-board)" : project.ticket_provider.board_id ? "board already linked; no provider call" : !provisionTicketBoard ? "ticket-provider action requires explicit provisionTicketBoard=true" : `create or link the ${project.ticket_provider.type} board "${project.name}" (${identifier}) via the ticket-provider adapter`
1658
+ reason: skipPlane ? "ticket-provider action disabled by skipPlane=true (--skip-board)" : project.ticket_provider.board_id ? "board already linked; no provider call" : !provisionTicketBoard ? "ticket-provider action requires explicit provisionTicketBoard=true" : `create or link the ${project.ticket_provider.type} board "${providerBoardName(project.ticket_provider.type, project.name)}" (${identifier}) via the ticket-provider adapter`
1654
1659
  }
1655
1660
  );
1656
1661
  return {
@@ -1692,12 +1697,12 @@ function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__
1692
1697
  plan.manifest.ticket_provider = manifestProvider;
1693
1698
  action.boardId = boardId;
1694
1699
  action.state = manifestProvider.state;
1695
- const manifestPath = join16(action.repoPath, ".project.json");
1700
+ const manifestPath = join17(action.repoPath, ".project.json");
1696
1701
  let next;
1697
1702
  if (existsSync11(manifestPath)) {
1698
1703
  let existing = {};
1699
1704
  try {
1700
- const parsed = JSON.parse(readFileSync14(manifestPath, "utf8"));
1705
+ const parsed = JSON.parse(readFileSync15(manifestPath, "utf8"));
1701
1706
  if (isRecord2(parsed)) existing = parsed;
1702
1707
  } catch {
1703
1708
  existing = {};
@@ -1705,13 +1710,13 @@ function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__
1705
1710
  const existingProvider = isRecord2(existing.ticket_provider) ? existing.ticket_provider : {};
1706
1711
  next = { ...existing, ticket_provider: { ...existingProvider, ...manifestProvider } };
1707
1712
  } else {
1708
- mkdirSync7(dirname13(manifestPath), { recursive: true });
1713
+ mkdirSync8(dirname13(manifestPath), { recursive: true });
1709
1714
  next = plan.manifest;
1710
1715
  }
1711
1716
  const text = `${JSON.stringify(next, null, 2)}
1712
1717
  `;
1713
- if (!existsSync11(manifestPath) || readFileSync14(manifestPath, "utf8") !== text) {
1714
- writeFileSync9(manifestPath, text, "utf8");
1718
+ if (!existsSync11(manifestPath) || readFileSync15(manifestPath, "utf8") !== text) {
1719
+ writeFileSync10(manifestPath, text, "utf8");
1715
1720
  return [manifestPath];
1716
1721
  }
1717
1722
  return [];
@@ -1739,7 +1744,7 @@ async function executeProjectInitPlan(plan, options = {}) {
1739
1744
  logs.push(
1740
1745
  action.data.agent_hooks_layer === "false" ? "commonproject: agent-hooks layer skipped (global ~/.agents/hooks detected \u2014 no per-user CLI injection)" : "commonproject: agent-hooks layer included"
1741
1746
  );
1742
- mkdirSync7(dirname13(action.targetDir), { recursive: true });
1747
+ mkdirSync8(dirname13(action.targetDir), { recursive: true });
1743
1748
  const before = snapshotTree(action.targetDir);
1744
1749
  const copierExecutable = options.trustedCopier?.executable ?? action.command[0];
1745
1750
  const copierEnv = { ...process.env };
@@ -1771,22 +1776,22 @@ async function executeProjectInitPlan(plan, options = {}) {
1771
1776
  break;
1772
1777
  }
1773
1778
  const projectType = action.data.project_type ?? "base";
1774
- const missing = (PROJECT_TYPE_SENTINELS[projectType] ?? []).filter((rel) => !existsSync11(join16(action.targetDir, rel)));
1779
+ const missing = (PROJECT_TYPE_SENTINELS[projectType] ?? []).filter((rel) => !existsSync11(join17(action.targetDir, rel)));
1775
1780
  if (missing.length) {
1776
1781
  errors.push(`copier rendered no ${projectType} project: ${missing.join(", ")} missing from ${action.targetDir}`);
1777
1782
  break;
1778
1783
  }
1779
1784
  if (projectType !== "base") logs.push(`commonproject: rendered as ${projectType}`);
1780
1785
  } else if (action.kind === "project.write-manifest") {
1781
- if (action.expectedContent !== void 0 && (!existsSync11(action.path) || readFileSync14(action.path, "utf8") !== action.expectedContent)) {
1786
+ if (action.expectedContent !== void 0 && (!existsSync11(action.path) || readFileSync15(action.path, "utf8") !== action.expectedContent)) {
1782
1787
  errors.push(`Manifest changed after planning: ${action.path}; re-plan before applying`);
1783
1788
  break;
1784
1789
  }
1785
- mkdirSync7(dirname13(action.path), { recursive: true });
1790
+ mkdirSync8(dirname13(action.path), { recursive: true });
1786
1791
  let value = action.manifest;
1787
1792
  if (existsSync11(action.path)) {
1788
1793
  try {
1789
- const currentValue = JSON.parse(readFileSync14(action.path, "utf8"));
1794
+ const currentValue = JSON.parse(readFileSync15(action.path, "utf8"));
1790
1795
  if (isRecord2(currentValue)) {
1791
1796
  const currentNotebook = isRecord2(currentValue.notebook) ? currentValue.notebook : {};
1792
1797
  const desiredNotebook = isRecord2(value.notebook) ? value.notebook : {};
@@ -1814,9 +1819,9 @@ async function executeProjectInitPlan(plan, options = {}) {
1814
1819
  value.project_id = normalizeProjectId(value.project_id ?? action.manifest.project_id);
1815
1820
  const next = `${JSON.stringify(value, null, 2)}
1816
1821
  `;
1817
- const current = existsSync11(action.path) ? readFileSync14(action.path, "utf8") : void 0;
1822
+ const current = existsSync11(action.path) ? readFileSync15(action.path, "utf8") : void 0;
1818
1823
  if (current !== next) {
1819
- writeFileSync9(action.path, next, "utf8");
1824
+ writeFileSync10(action.path, next, "utf8");
1820
1825
  changedFiles.push(action.path);
1821
1826
  }
1822
1827
  changedFiles.push(...synchronizeCopierIdentity(action.path, action.manifest));
@@ -1897,7 +1902,7 @@ function getProject(registry, slug) {
1897
1902
  return project;
1898
1903
  }
1899
1904
  function buildCommonProjectCopierAction(input) {
1900
- const templateDir = join16(input.pjanglerRoot, "templates", "commonproject");
1905
+ const templateDir = join17(input.pjanglerRoot, "templates", "commonproject");
1901
1906
  const data = {
1902
1907
  project_name: input.projectName,
1903
1908
  project_description: input.projectDescription ?? "",
@@ -1928,7 +1933,7 @@ function buildCommonProjectCopierAction(input) {
1928
1933
  function resolvePjanglerRoot() {
1929
1934
  let dir = dirname13(fileURLToPath2(import.meta.url));
1930
1935
  while (dir !== dirname13(dir)) {
1931
- if (existsSync11(join16(dir, "package.json")) && existsSync11(join16(dir, "templates", "commonproject", "copier.yml"))) return dir;
1936
+ if (existsSync11(join17(dir, "package.json")) && existsSync11(join17(dir, "templates", "commonproject", "copier.yml"))) return dir;
1932
1937
  dir = dirname13(dir);
1933
1938
  }
1934
1939
  return resolve10(process.cwd());
@@ -2005,7 +2010,7 @@ function validateProjectRecord(project, key) {
2005
2010
  }
2006
2011
  function expandHome(path) {
2007
2012
  if (path === "~") return homedir5();
2008
- if (path.startsWith("~/")) return join16(homedir5(), path.slice(2));
2013
+ if (path.startsWith("~/")) return join17(homedir5(), path.slice(2));
2009
2014
  return path;
2010
2015
  }
2011
2016
  function isRecord2(value) {
@@ -2032,7 +2037,7 @@ var init_project = __esm({
2032
2037
  "obsidian-plugin": ["typescript", "javascript"]
2033
2038
  };
2034
2039
  PROJECT_TYPE_SENTINELS = {
2035
- "obsidian-plugin": ["manifest.json", join16("src", "main.ts")]
2040
+ "obsidian-plugin": ["manifest.json", join17("src", "main.ts")]
2036
2041
  };
2037
2042
  BOARD_URL_DEPRECATION_WARNING = "boardUrl is deprecated and ignored; board URLs are derived at runtime and are never persisted.";
2038
2043
  PROJECT_IDENTIFIER_SOURCES = ["provider", "proposed"];
@@ -2041,8 +2046,8 @@ var init_project = __esm({
2041
2046
  IDENTIFIER_REPAIR_COMMAND = "pj project identity --all --apply";
2042
2047
  DEFAULT_SOURCE_SKILL_ROOTS = [
2043
2048
  "/home/delorenj/code/skillex/all-skills",
2044
- join16(homedir5(), ".agents", "skills"),
2045
- join16(homedir5(), ".codex", "skills")
2049
+ join17(homedir5(), ".agents", "skills"),
2050
+ join17(homedir5(), ".codex", "skills")
2046
2051
  ];
2047
2052
  PROJECT_REGISTRY_OWNED_KEYS = [
2048
2053
  "name",
@@ -2074,7 +2079,7 @@ var init_project = __esm({
2074
2079
  import { randomUUID } from "node:crypto";
2075
2080
  import { isDeepStrictEqual } from "node:util";
2076
2081
  import { isIP as isIP2 } from "node:net";
2077
- import { closeSync as closeSync3, existsSync as existsSync12, fchmodSync as fchmodSync2, fsyncSync as fsyncSync2, lstatSync as lstatSync12, openSync as openSync3, readFileSync as readFileSync15, realpathSync as realpathSync7, renameSync as renameSync6, writeFileSync as writeFileSync10 } from "node:fs";
2082
+ import { closeSync as closeSync4, existsSync as existsSync12, fchmodSync as fchmodSync3, fsyncSync as fsyncSync2, lstatSync as lstatSync13, openSync as openSync4, readFileSync as readFileSync16, realpathSync as realpathSync7, renameSync as renameSync6, writeFileSync as writeFileSync11 } from "node:fs";
2078
2083
  import { dirname as dirname14, resolve as resolve11 } from "node:path";
2079
2084
  function isRecord3(value) {
2080
2085
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -2088,7 +2093,7 @@ function readManifest(repoPath) {
2088
2093
  if (!existsSync12(path)) return null;
2089
2094
  let parsed;
2090
2095
  try {
2091
- parsed = JSON.parse(readFileSync15(path, "utf8"));
2096
+ parsed = JSON.parse(readFileSync16(path, "utf8"));
2092
2097
  } catch {
2093
2098
  throw new NotebookError("INVALID_INPUT", `${path} is not valid JSON`);
2094
2099
  }
@@ -2330,7 +2335,7 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2330
2335
  const changed = [];
2331
2336
  const manifestPath = resolve11(resolved.project.repo_path, ".project.json");
2332
2337
  if (isRegistryServiceLocation(resolved.registry_path)) {
2333
- const before = existsSync12(manifestPath) ? readFileSync15(manifestPath, "utf8") : null;
2338
+ const before = existsSync12(manifestPath) ? readFileSync16(manifestPath, "utf8") : null;
2334
2339
  const declaredPolicy = resolved.manifest?.notebook?.policy ?? {};
2335
2340
  const nextProject = {
2336
2341
  ...resolved.project,
@@ -2342,10 +2347,10 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2342
2347
  resolved.registry = loadProjectRegistry(resolved.registry_path);
2343
2348
  resolved.project = registryProjectBySlug(resolved.registry, nextProject.slug);
2344
2349
  resolved.manifest = readManifest(nextProject.repo_path);
2345
- const after = existsSync12(manifestPath) ? readFileSync15(manifestPath, "utf8") : null;
2350
+ const after = existsSync12(manifestPath) ? readFileSync16(manifestPath, "utf8") : null;
2346
2351
  return before === after ? [] : [manifestPath];
2347
2352
  }
2348
- const manifestRaw = existsSync12(manifestPath) ? JSON.parse(readFileSync15(manifestPath, "utf8")) : {};
2353
+ const manifestRaw = existsSync12(manifestPath) ? JSON.parse(readFileSync16(manifestPath, "utf8")) : {};
2349
2354
  if (!isRecord3(manifestRaw)) throw new NotebookError("INVALID_INPUT", `${manifestPath} must contain a JSON object`);
2350
2355
  validateManifestNotebookSurface(manifestRaw.notebook);
2351
2356
  const manifestNotebook2 = isRecord3(manifestRaw.notebook) ? manifestRaw.notebook : {};
@@ -2378,29 +2383,29 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2378
2383
  };
2379
2384
  const manifestText = `${JSON.stringify(manifestNext, null, 2)}
2380
2385
  `;
2381
- if (!existsSync12(manifestPath) || readFileSync15(manifestPath, "utf8") !== manifestText) {
2386
+ if (!existsSync12(manifestPath) || readFileSync16(manifestPath, "utf8") !== manifestText) {
2382
2387
  let mode = 420;
2383
2388
  if (existsSync12(manifestPath)) {
2384
- const current2 = lstatSync12(manifestPath);
2389
+ const current2 = lstatSync13(manifestPath);
2385
2390
  if (!current2.isFile() || current2.isSymbolicLink()) throw new NotebookError("CONFLICT", `${manifestPath} must be a regular non-symlink file`);
2386
2391
  mode = current2.mode & 511;
2387
2392
  }
2388
2393
  const temp = resolve11(dirname14(manifestPath), `.${process.pid}.${randomUUID()}.project.json.tmp`);
2389
- const fd = openSync3(temp, "wx", mode);
2394
+ const fd = openSync4(temp, "wx", mode);
2390
2395
  try {
2391
- writeFileSync10(fd, manifestText, "utf8");
2392
- fchmodSync2(fd, mode);
2396
+ writeFileSync11(fd, manifestText, "utf8");
2397
+ fchmodSync3(fd, mode);
2393
2398
  fsyncSync2(fd);
2394
2399
  } finally {
2395
- closeSync3(fd);
2400
+ closeSync4(fd);
2396
2401
  }
2397
2402
  renameSync6(temp, manifestPath);
2398
2403
  try {
2399
- const directory = openSync3(dirname14(manifestPath), "r");
2404
+ const directory = openSync4(dirname14(manifestPath), "r");
2400
2405
  try {
2401
2406
  fsyncSync2(directory);
2402
2407
  } finally {
2403
- closeSync3(directory);
2408
+ closeSync4(directory);
2404
2409
  }
2405
2410
  } catch {
2406
2411
  }
@@ -2409,9 +2414,9 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
2409
2414
  const current = resolved.project.notebook;
2410
2415
  resolved.project.notebook = { ...isRecord3(current) ? current : { state: "planned" }, ...binding };
2411
2416
  const fileRegistry = !isRegistryServiceLocation(resolved.registry_path);
2412
- const registryBefore = fileRegistry && existsSync12(resolved.registry_path) ? readFileSync15(resolved.registry_path, "utf8") : null;
2417
+ const registryBefore = fileRegistry && existsSync12(resolved.registry_path) ? readFileSync16(resolved.registry_path, "utf8") : null;
2413
2418
  saveProjectRegistry(resolved.registry, resolved.registry_path);
2414
- const registryAfter = fileRegistry && existsSync12(resolved.registry_path) ? readFileSync15(resolved.registry_path, "utf8") : null;
2419
+ const registryAfter = fileRegistry && existsSync12(resolved.registry_path) ? readFileSync16(resolved.registry_path, "utf8") : null;
2415
2420
  if (registryBefore !== registryAfter) changed.push(resolved.registry_path);
2416
2421
  resolved.manifest = manifestNext;
2417
2422
  return changed;
@@ -2444,7 +2449,7 @@ var init_config = __esm({
2444
2449
  });
2445
2450
 
2446
2451
  // src/notebook/notes.ts
2447
- import { createHash as createHash9, randomUUID as randomUUID2 } from "node:crypto";
2452
+ import { createHash as createHash10, randomUUID as randomUUID2 } from "node:crypto";
2448
2453
  function canonicalJson2(value) {
2449
2454
  if (value === void 0 || typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") throw new NotebookError("INVALID_INPUT", "Canonical JSON value is not JSON-compatible");
2450
2455
  if (value === null || typeof value !== "object") {
@@ -2457,7 +2462,7 @@ function canonicalJson2(value) {
2457
2462
  return `{${Object.keys(record3).filter((key) => record3[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(record3[key])}`).join(",")}}`;
2458
2463
  }
2459
2464
  function sha256Hex(value) {
2460
- return createHash9("sha256").update(value).digest("hex");
2465
+ return createHash10("sha256").update(value).digest("hex");
2461
2466
  }
2462
2467
  function base64UrlEncode(value) {
2463
2468
  return Buffer.from(value, "utf8").toString("base64url");
@@ -2714,27 +2719,27 @@ var init_remote_mutation_schema = __esm({
2714
2719
  });
2715
2720
 
2716
2721
  // src/notebook/state.ts
2717
- import { createHash as createHash10, randomUUID as randomUUID3 } from "node:crypto";
2722
+ import { createHash as createHash11, randomUUID as randomUUID3 } from "node:crypto";
2718
2723
  import {
2719
- closeSync as closeSync4,
2720
- constants as constants2,
2724
+ closeSync as closeSync5,
2725
+ constants as constants3,
2721
2726
  existsSync as existsSync13,
2722
- fchmodSync as fchmodSync3,
2723
- fstatSync,
2727
+ fchmodSync as fchmodSync4,
2728
+ fstatSync as fstatSync2,
2724
2729
  fsyncSync as fsyncSync3,
2725
- lstatSync as lstatSync13,
2726
- mkdirSync as mkdirSync8,
2727
- openSync as openSync4,
2730
+ lstatSync as lstatSync14,
2731
+ mkdirSync as mkdirSync9,
2732
+ openSync as openSync5,
2728
2733
  readSync,
2729
- readdirSync as readdirSync9,
2734
+ readdirSync as readdirSync10,
2730
2735
  renameSync as renameSync7,
2731
- unlinkSync as unlinkSync7,
2732
- writeFileSync as writeFileSync11
2736
+ unlinkSync as unlinkSync8,
2737
+ writeFileSync as writeFileSync12
2733
2738
  } from "node:fs";
2734
2739
  import { homedir as homedir6 } from "node:os";
2735
- import { basename as basename8, dirname as dirname15, join as join17, parse, relative as relative12, resolve as resolve12, sep as sep4 } from "node:path";
2740
+ import { basename as basename9, dirname as dirname15, join as join18, parse, relative as relative12, resolve as resolve12, sep as sep4 } from "node:path";
2736
2741
  function notebookStateRoot(env2 = process.env) {
2737
- const base = env2.XDG_STATE_HOME || join17(env2.HOME || homedir6(), ".local", "state");
2742
+ const base = env2.XDG_STATE_HOME || join18(env2.HOME || homedir6(), ".local", "state");
2738
2743
  return resolve12(base, "pjangler", "notebook", NOTEBOOK_STATE_VERSION);
2739
2744
  }
2740
2745
  function assertDigest(value, label) {
@@ -2742,7 +2747,7 @@ function assertDigest(value, label) {
2742
2747
  }
2743
2748
  function projectStateDir(root, projectSlug) {
2744
2749
  if (!/^[a-z0-9][a-z0-9._-]{0,127}$/iu.test(projectSlug)) throw new NotebookError("INVALID_INPUT", "Invalid project slug for Notebook state");
2745
- return join17(resolve12(root), "projects", sha256Hex(`pjangler-project-state-v1\0${projectSlug}`));
2750
+ return join18(resolve12(root), "projects", sha256Hex(`pjangler-project-state-v1\0${projectSlug}`));
2746
2751
  }
2747
2752
  function assertContained(root, candidate) {
2748
2753
  const rel = relative12(resolve12(root), resolve12(candidate));
@@ -2755,33 +2760,33 @@ function openPinnedDirectory(path, root, create) {
2755
2760
  assertContained(absoluteRoot, absolute);
2756
2761
  if (!existsSync13("/proc/self/fd")) throw new NotebookError("INTERNAL_ERROR", "Descriptor-pinned Notebook state requires procfs");
2757
2762
  const parsed = parse(absolute);
2758
- let fd = openSync4(parsed.root, DIRECTORY_OPEN_FLAGS);
2763
+ let fd = openSync5(parsed.root, DIRECTORY_OPEN_FLAGS);
2759
2764
  let cursor = parsed.root;
2760
2765
  try {
2761
2766
  for (const part of absolute.slice(parsed.root.length).split(sep4).filter(Boolean)) {
2762
2767
  const child = `/proc/self/fd/${fd}/${part}`;
2763
- cursor = join17(cursor, part);
2768
+ cursor = join18(cursor, part);
2764
2769
  let childFd;
2765
2770
  try {
2766
- childFd = openSync4(child, DIRECTORY_OPEN_FLAGS);
2771
+ childFd = openSync5(child, DIRECTORY_OPEN_FLAGS);
2767
2772
  } catch (error) {
2768
2773
  if (!create || error.code !== "ENOENT") throw error;
2769
2774
  try {
2770
- mkdirSync8(child, { mode: 448 });
2775
+ mkdirSync9(child, { mode: 448 });
2771
2776
  } catch (mkdirError) {
2772
2777
  if (mkdirError.code !== "EEXIST") throw mkdirError;
2773
2778
  }
2774
- childFd = openSync4(child, DIRECTORY_OPEN_FLAGS);
2779
+ childFd = openSync5(child, DIRECTORY_OPEN_FLAGS);
2775
2780
  }
2776
- closeSync4(fd);
2781
+ closeSync5(fd);
2777
2782
  fd = childFd;
2778
- const stat = fstatSync(fd);
2783
+ const stat = fstatSync2(fd);
2779
2784
  if (!stat.isDirectory()) throw new NotebookError("INTERNAL_ERROR", "Notebook state path component is not a directory");
2780
2785
  if (cursor === absoluteRoot || relative12(absoluteRoot, cursor).startsWith("..") === false) assertOwned(stat);
2781
2786
  }
2782
2787
  return fd;
2783
2788
  } catch (error) {
2784
- closeSync4(fd);
2789
+ closeSync5(fd);
2785
2790
  if (error.code === "ELOOP" || error.code === "ENOTDIR") {
2786
2791
  throw new NotebookError("INTERNAL_ERROR", "Notebook state path contains a symlink component");
2787
2792
  }
@@ -2789,7 +2794,7 @@ function openPinnedDirectory(path, root, create) {
2789
2794
  }
2790
2795
  }
2791
2796
  function pinnedLeaf(parentFd, path) {
2792
- return `/proc/self/fd/${parentFd}/${basename8(path)}`;
2797
+ return `/proc/self/fd/${parentFd}/${basename9(path)}`;
2793
2798
  }
2794
2799
  function assertOwned(stat) {
2795
2800
  if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
@@ -2802,13 +2807,13 @@ function ensureDirectory(path, root) {
2802
2807
  assertContained(absoluteRoot, absolute);
2803
2808
  const fd = openPinnedDirectory(absolute, absoluteRoot, true);
2804
2809
  try {
2805
- const stat = fstatSync(fd);
2810
+ const stat = fstatSync2(fd);
2806
2811
  if (!stat.isDirectory()) throw new NotebookError("INTERNAL_ERROR", "Notebook state directory is not a real directory");
2807
2812
  assertOwned(stat);
2808
- fchmodSync3(fd, 448);
2813
+ fchmodSync4(fd, 448);
2809
2814
  fsyncSync3(fd);
2810
2815
  } finally {
2811
- closeSync4(fd);
2816
+ closeSync5(fd);
2812
2817
  }
2813
2818
  }
2814
2819
  function notebookStatePaths(root, projectSlug) {
@@ -2817,18 +2822,18 @@ function notebookStatePaths(root, projectSlug) {
2817
2822
  return {
2818
2823
  root: absoluteRoot,
2819
2824
  project,
2820
- baselines: join17(project, "baselines"),
2821
- claims: join17(project, "claims"),
2822
- receipts: join17(project, "receipts"),
2823
- refusals: join17(project, "refusals"),
2824
- journals: join17(project, "journals"),
2825
- locks: join17(project, "locks")
2825
+ baselines: join18(project, "baselines"),
2826
+ claims: join18(project, "claims"),
2827
+ receipts: join18(project, "receipts"),
2828
+ refusals: join18(project, "refusals"),
2829
+ journals: join18(project, "journals"),
2830
+ locks: join18(project, "locks")
2826
2831
  };
2827
2832
  }
2828
2833
  function ensureNotebookState(root, projectSlug) {
2829
2834
  const paths = notebookStatePaths(root, projectSlug);
2830
2835
  const { root: absoluteRoot, project } = paths;
2831
- for (const path of [absoluteRoot, join17(absoluteRoot, "projects"), project, paths.baselines, paths.claims, paths.receipts, paths.refusals, paths.journals, paths.locks]) {
2836
+ for (const path of [absoluteRoot, join18(absoluteRoot, "projects"), project, paths.baselines, paths.claims, paths.receipts, paths.refusals, paths.journals, paths.locks]) {
2832
2837
  ensureDirectory(path, absoluteRoot);
2833
2838
  }
2834
2839
  return paths;
@@ -2844,15 +2849,15 @@ function fsyncDirectory2(path) {
2844
2849
  fsyncSync3(fd);
2845
2850
  } catch {
2846
2851
  } finally {
2847
- if (fd !== void 0) closeSync4(fd);
2852
+ if (fd !== void 0) closeSync5(fd);
2848
2853
  }
2849
2854
  }
2850
2855
  function readStateDirectory(path, root) {
2851
2856
  const fd = openPinnedDirectory(path, root, false);
2852
2857
  try {
2853
- return readdirSync9(`/proc/self/fd/${fd}`, { withFileTypes: true });
2858
+ return readdirSync10(`/proc/self/fd/${fd}`, { withFileTypes: true });
2854
2859
  } finally {
2855
- closeSync4(fd);
2860
+ closeSync5(fd);
2856
2861
  }
2857
2862
  }
2858
2863
  function readNotebookStateDirectory(path, root) {
@@ -2866,22 +2871,22 @@ function unlinkStateFile(path, root, allowMissing = false) {
2866
2871
  let fileFd;
2867
2872
  try {
2868
2873
  try {
2869
- fileFd = openSync4(target, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2874
+ fileFd = openSync5(target, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
2870
2875
  } catch (error) {
2871
2876
  if (allowMissing && error.code === "ENOENT") return false;
2872
2877
  throw error;
2873
2878
  }
2874
- const stat = fstatSync(fileFd);
2879
+ const stat = fstatSync2(fileFd);
2875
2880
  if (!stat.isFile() || (stat.mode & 511) !== 384) {
2876
2881
  throw new NotebookError("CONFLICT", "Refusing to remove a suspect Notebook state entry; run pj notebook audit --json");
2877
2882
  }
2878
2883
  assertOwned(stat);
2879
- unlinkSync7(target);
2884
+ unlinkSync8(target);
2880
2885
  fsyncSync3(parentFd);
2881
2886
  return true;
2882
2887
  } finally {
2883
- if (fileFd !== void 0) closeSync4(fileFd);
2884
- closeSync4(parentFd);
2888
+ if (fileFd !== void 0) closeSync5(fileFd);
2889
+ closeSync5(parentFd);
2885
2890
  }
2886
2891
  }
2887
2892
  function renameStateFile(source, target, root) {
@@ -2891,12 +2896,12 @@ function renameStateFile(source, target, root) {
2891
2896
  const targetPath = pinnedLeaf(parentFd, target);
2892
2897
  let sourceFd;
2893
2898
  try {
2894
- sourceFd = openSync4(sourcePath, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2895
- const stat = fstatSync(sourceFd);
2899
+ sourceFd = openSync5(sourcePath, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
2900
+ const stat = fstatSync2(sourceFd);
2896
2901
  if (!stat.isFile() || (stat.mode & 511) !== 384) throw new NotebookError("CONFLICT", "Notebook state rename source has an integrity finding");
2897
2902
  assertOwned(stat);
2898
2903
  try {
2899
- lstatSync13(targetPath);
2904
+ lstatSync14(targetPath);
2900
2905
  throw new NotebookError("CONFLICT", "Notebook state rename target already exists");
2901
2906
  } catch (error) {
2902
2907
  if (error.code !== "ENOENT") throw error;
@@ -2904,8 +2909,8 @@ function renameStateFile(source, target, root) {
2904
2909
  renameSync7(sourcePath, targetPath);
2905
2910
  fsyncSync3(parentFd);
2906
2911
  } finally {
2907
- if (sourceFd !== void 0) closeSync4(sourceFd);
2908
- closeSync4(parentFd);
2912
+ if (sourceFd !== void 0) closeSync5(sourceFd);
2913
+ closeSync5(parentFd);
2909
2914
  }
2910
2915
  }
2911
2916
  function atomicWriteJson(path, value, root, afterParentPinned) {
@@ -2914,34 +2919,34 @@ function atomicWriteJson(path, value, root, afterParentPinned) {
2914
2919
  const text = jsonLine(value);
2915
2920
  const parentFd = openPinnedDirectory(dirname15(path), root, false);
2916
2921
  const target = pinnedLeaf(parentFd, path);
2917
- const tempName = `.${basename8(path)}.${process.pid}.${randomUUID3()}.tmp`;
2922
+ const tempName = `.${basename9(path)}.${process.pid}.${randomUUID3()}.tmp`;
2918
2923
  const temp = `/proc/self/fd/${parentFd}/${tempName}`;
2919
2924
  let existingIdentity = null;
2920
2925
  try {
2921
2926
  afterParentPinned?.();
2922
2927
  try {
2923
- const existingFd = openSync4(target, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2928
+ const existingFd = openSync5(target, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
2924
2929
  try {
2925
- const existing = fstatSync(existingFd);
2930
+ const existing = fstatSync2(existingFd);
2926
2931
  if (!existing.isFile()) throw new NotebookError("INTERNAL_ERROR", "Refusing to replace a non-regular Notebook state file");
2927
2932
  assertOwned(existing);
2928
2933
  if ((existing.mode & 511) !== 384) throw new NotebookError("CONFLICT", "Refusing to replace a Notebook state file with unsafe permissions");
2929
2934
  existingIdentity = { dev: existing.dev, ino: existing.ino };
2930
2935
  } finally {
2931
- closeSync4(existingFd);
2936
+ closeSync5(existingFd);
2932
2937
  }
2933
2938
  } catch (error) {
2934
2939
  if (error.code !== "ENOENT") throw error;
2935
2940
  }
2936
- const fd = openSync4(temp, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
2941
+ const fd = openSync5(temp, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | (constants3.O_NOFOLLOW ?? 0), 384);
2937
2942
  try {
2938
- writeFileSync11(fd, text, "utf8");
2943
+ writeFileSync12(fd, text, "utf8");
2939
2944
  fsyncSync3(fd);
2940
2945
  } finally {
2941
- closeSync4(fd);
2946
+ closeSync5(fd);
2942
2947
  }
2943
2948
  try {
2944
- const current = lstatSync13(target);
2949
+ const current = lstatSync14(target);
2945
2950
  if (!existingIdentity || !current.isFile() || current.isSymbolicLink() || current.dev !== existingIdentity.dev || current.ino !== existingIdentity.ino) {
2946
2951
  throw new NotebookError("CONFLICT", "Notebook state target changed during atomic update; preserving both entries for audit");
2947
2952
  }
@@ -2952,12 +2957,12 @@ function atomicWriteJson(path, value, root, afterParentPinned) {
2952
2957
  fsyncSync3(parentFd);
2953
2958
  } catch (error) {
2954
2959
  try {
2955
- unlinkSync7(temp);
2960
+ unlinkSync8(temp);
2956
2961
  } catch {
2957
2962
  }
2958
2963
  throw error;
2959
2964
  } finally {
2960
- closeSync4(parentFd);
2965
+ closeSync5(parentFd);
2961
2966
  }
2962
2967
  return Buffer.byteLength(text, "utf8");
2963
2968
  }
@@ -2967,22 +2972,22 @@ function exclusiveWrite(path, text, root) {
2967
2972
  const target = pinnedLeaf(parentFd, path);
2968
2973
  let fd;
2969
2974
  try {
2970
- fd = openSync4(target, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | (constants2.O_NOFOLLOW ?? 0), 384);
2975
+ fd = openSync5(target, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | (constants3.O_NOFOLLOW ?? 0), 384);
2971
2976
  } catch (error) {
2972
- closeSync4(parentFd);
2977
+ closeSync5(parentFd);
2973
2978
  if (error.code === "EEXIST") return false;
2974
2979
  throw error;
2975
2980
  }
2976
2981
  try {
2977
- writeFileSync11(fd, text, "utf8");
2982
+ writeFileSync12(fd, text, "utf8");
2978
2983
  fsyncSync3(fd);
2979
2984
  } finally {
2980
- closeSync4(fd);
2985
+ closeSync5(fd);
2981
2986
  }
2982
2987
  try {
2983
2988
  fsyncSync3(parentFd);
2984
2989
  } finally {
2985
- closeSync4(parentFd);
2990
+ closeSync5(parentFd);
2986
2991
  }
2987
2992
  return true;
2988
2993
  }
@@ -3126,13 +3131,13 @@ function safeReadJson(path, maxBytes) {
3126
3131
  }
3127
3132
  let fd;
3128
3133
  try {
3129
- fd = openSync4(pinnedLeaf(parentFd, path), constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
3134
+ fd = openSync5(pinnedLeaf(parentFd, path), constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
3130
3135
  } catch (error) {
3131
- closeSync4(parentFd);
3136
+ closeSync5(parentFd);
3132
3137
  return { reason: error.code === "ELOOP" ? "non-regular" : "unreadable", bytes: 0 };
3133
3138
  }
3134
3139
  try {
3135
- const stat = fstatSync(fd);
3140
+ const stat = fstatSync2(fd);
3136
3141
  if (!stat.isFile()) return { reason: "non-regular", bytes: 0 };
3137
3142
  if ((stat.mode & 511) !== 384) return { reason: "unsafe-permissions", bytes: stat.size };
3138
3143
  try {
@@ -3152,14 +3157,14 @@ function safeReadJson(path, maxBytes) {
3152
3157
  if (total > maxBytes) return { reason: "oversize", bytes: total };
3153
3158
  chunks.push(chunk.subarray(0, count));
3154
3159
  }
3155
- const after = fstatSync(fd);
3160
+ const after = fstatSync2(fd);
3156
3161
  if (after.size !== stat.size || total !== stat.size) return { reason: "unreadable", bytes: total };
3157
3162
  return { value: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total))), bytes: total };
3158
3163
  } catch (error) {
3159
3164
  return { reason: error instanceof SyntaxError || error instanceof TypeError ? "invalid-json" : "unreadable", bytes: 0 };
3160
3165
  } finally {
3161
- closeSync4(fd);
3162
- closeSync4(parentFd);
3166
+ closeSync5(fd);
3167
+ closeSync5(parentFd);
3163
3168
  }
3164
3169
  }
3165
3170
  function safeEntryId(kind, name) {
@@ -3176,10 +3181,10 @@ function baselineReceiptByteCeiling(limits) {
3176
3181
  function readSessionBaseline(root, projectSlug, sessionKey, limits) {
3177
3182
  assertDigest(sessionKey, "session key");
3178
3183
  const paths = ensureNotebookState(root, projectSlug);
3179
- return readBaseline(join17(paths.baselines, `${sessionKey}.json`), baselineReceiptByteCeiling(limits));
3184
+ return readBaseline(join18(paths.baselines, `${sessionKey}.json`), baselineReceiptByteCeiling(limits));
3180
3185
  }
3181
3186
  function acquireLock(paths, maxWaitMs) {
3182
- const lock = join17(paths.locks, "admission.lock");
3187
+ const lock = join18(paths.locks, "admission.lock");
3183
3188
  const deadline = Date.now() + Math.max(1, maxWaitMs);
3184
3189
  const token = randomUUID3();
3185
3190
  const record3 = () => ({
@@ -3209,7 +3214,7 @@ function acquireLock(paths, maxWaitMs) {
3209
3214
  throw new NotebookError("CONFLICT", "Notebook state lock has an integrity finding; preserve it and run pj notebook audit --json");
3210
3215
  }
3211
3216
  if (Date.now() >= expiresAt) {
3212
- const recovery = join17(paths.locks, `recovery-${heldToken}.lock`);
3217
+ const recovery = join18(paths.locks, `recovery-${heldToken}.lock`);
3213
3218
  const recoveryRecord = jsonLine({ schema_version: NOTEBOOK_SCHEMA_VERSION, stale_token: heldToken, recovery_token: token, expires_at: new Date(Date.now() + 5e3).toISOString() });
3214
3219
  if (exclusiveWrite(recovery, recoveryRecord, paths.root)) {
3215
3220
  try {
@@ -3217,7 +3222,7 @@ function acquireLock(paths, maxWaitMs) {
3217
3222
  const currentToken = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? verify.value.token : null;
3218
3223
  const currentExpiry = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? Date.parse(String(verify.value.expires_at ?? "")) : Number.NaN;
3219
3224
  if (currentToken === heldToken && Number.isFinite(currentExpiry) && Date.now() >= currentExpiry) {
3220
- const recovered = join17(paths.locks, `.recovered-${heldToken}-${randomUUID3()}.json`);
3225
+ const recovered = join18(paths.locks, `.recovered-${heldToken}-${randomUUID3()}.json`);
3221
3226
  renameStateFile(lock, recovered, paths.root);
3222
3227
  unlinkStateFile(recovered, paths.root);
3223
3228
  }
@@ -3297,7 +3302,7 @@ function scanAuxiliaryState(paths, limits) {
3297
3302
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
3298
3303
  continue;
3299
3304
  }
3300
- const read = safeReadJson(join17(specification.dir, entry.name), specification.maxBytes);
3305
+ const read = safeReadJson(join18(specification.dir, entry.name), specification.maxBytes);
3301
3306
  if (read.reason || read.value === void 0) {
3302
3307
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
3303
3308
  continue;
@@ -3322,7 +3327,7 @@ function scanAuxiliaryState(paths, limits) {
3322
3327
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
3323
3328
  continue;
3324
3329
  }
3325
- const read = safeReadJson(join17(paths.journals, entry.name), limits.receipt_max_bytes);
3330
+ const read = safeReadJson(join18(paths.journals, entry.name), limits.receipt_max_bytes);
3326
3331
  if (read.reason || read.value === void 0) {
3327
3332
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
3328
3333
  continue;
@@ -3351,7 +3356,7 @@ function scanAuxiliaryState(paths, limits) {
3351
3356
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
3352
3357
  continue;
3353
3358
  }
3354
- const read = safeReadJson(join17(paths.locks, entry.name), 8192);
3359
+ const read = safeReadJson(join18(paths.locks, entry.name), 8192);
3355
3360
  if (read.reason || read.value === void 0) {
3356
3361
  addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
3357
3362
  continue;
@@ -3390,7 +3395,7 @@ function scanReceipts(paths, limits) {
3390
3395
  }
3391
3396
  for (const entry of entries) {
3392
3397
  const entryId = safeEntryId("receipts", entry.name);
3393
- const path = join17(paths.receipts, entry.name);
3398
+ const path = join18(paths.receipts, entry.name);
3394
3399
  if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9]{64}\.json$/u.test(entry.name)) {
3395
3400
  addIntegrity({ entry_id: entryId, reason: "non-regular" });
3396
3401
  continue;
@@ -3420,7 +3425,7 @@ function scanBaselines(paths, nowMs, limits, referenced) {
3420
3425
  if (!existsSync13(paths.baselines)) return { current, stale };
3421
3426
  for (const entry of readStateDirectory(paths.baselines, paths.root)) {
3422
3427
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3423
- const baseline = readBaseline(join17(paths.baselines, entry.name), baselineReceiptByteCeiling(limits));
3428
+ const baseline = readBaseline(join18(paths.baselines, entry.name), baselineReceiptByteCeiling(limits));
3424
3429
  if (!baseline || referenced.has(baseline.session_key)) continue;
3425
3430
  const expires = Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3;
3426
3431
  if (nowMs >= expires) stale += 1;
@@ -3434,7 +3439,7 @@ function scanRefusals(paths, nowMs, limits, scan) {
3434
3439
  const entries = readStateDirectory(paths.refusals, paths.root).sort((a, b) => a.name.localeCompare(b.name, "en")).slice(0, limits.refusal_max_entries);
3435
3440
  for (const entry of entries) {
3436
3441
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3437
- const read = safeReadJson(join17(paths.refusals, entry.name), limits.receipt_max_bytes);
3442
+ const read = safeReadJson(join18(paths.refusals, entry.name), limits.receipt_max_bytes);
3438
3443
  const marker = read.value === void 0 ? null : parseRefusal(read.value);
3439
3444
  if (!marker || `${marker.session_key}.json` !== entry.name) continue;
3440
3445
  if (nowMs >= Date.parse(marker.baseline_created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
@@ -3517,7 +3522,7 @@ function listCaptureReceipts(root, projectSlug, limits, state) {
3517
3522
  function readCaptureReceipt(root, projectSlug, receiptId, limits) {
3518
3523
  if (!RECEIPT_ID_RE.test(receiptId)) throw new NotebookError("INVALID_INPUT", "Invalid receipt ID");
3519
3524
  const paths = notebookStatePaths(root, projectSlug);
3520
- const path = join17(paths.receipts, `${receiptId}.json`);
3525
+ const path = join18(paths.receipts, `${receiptId}.json`);
3521
3526
  if (!existsSync13(path)) throw new NotebookError("NOT_FOUND", `Capture receipt not found: ${receiptId}`);
3522
3527
  const read = safeReadJson(path, limits.receipt_max_bytes);
3523
3528
  const receipt = read.value === void 0 ? null : parseReceipt(read.value);
@@ -3530,7 +3535,7 @@ function writeReceipt(paths, receipt, limit) {
3530
3535
  const bytes = Buffer.byteLength(text, "utf8");
3531
3536
  if (bytes > limit) throw new NotebookError("CONFLICT", "Receipt transition exceeds its per-receipt ceiling");
3532
3537
  if (receipt.serialized_bytes === bytes) {
3533
- atomicWriteJson(join17(paths.receipts, `${receipt.receipt_id}.json`), receipt, paths.root);
3538
+ atomicWriteJson(join18(paths.receipts, `${receipt.receipt_id}.json`), receipt, paths.root);
3534
3539
  return receipt;
3535
3540
  }
3536
3541
  receipt.serialized_bytes = bytes;
@@ -3669,25 +3674,25 @@ function pruneNotebookState(root, projectSlug, limits, now = /* @__PURE__ */ new
3669
3674
  const successCutoff = now.getTime() - limits.receipt_succeeded_retention_days * 864e5;
3670
3675
  for (const receipt of scan.receipts) {
3671
3676
  if (receipt.state !== "succeeded" || Date.parse(receipt.updated_at) > successCutoff) continue;
3672
- const path = join17(paths.receipts, `${receipt.receipt_id}.json`);
3677
+ const path = join18(paths.receipts, `${receipt.receipt_id}.json`);
3673
3678
  unlinkStateFile(path, paths.root);
3674
- removed.push(safeEntryId("receipts", basename8(path)));
3679
+ removed.push(safeEntryId("receipts", basename9(path)));
3675
3680
  }
3676
3681
  const remaining = scanReceipts(paths, limits);
3677
3682
  const remainingAuxiliary = scanAuxiliaryState(paths, limits);
3678
3683
  const referenced = /* @__PURE__ */ new Set([...remaining.referencedSessions, ...remainingAuxiliary.referencedSessions]);
3679
3684
  for (const entry of readStateDirectory(paths.baselines, paths.root)) {
3680
3685
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
3681
- const path = join17(paths.baselines, entry.name);
3686
+ const path = join18(paths.baselines, entry.name);
3682
3687
  const baseline = readBaseline(path, baselineReceiptByteCeiling(limits));
3683
3688
  if (!baseline || referenced.has(baseline.session_key)) continue;
3684
3689
  if (now.getTime() < Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
3685
3690
  unlinkStateFile(path, paths.root);
3686
3691
  removed.push(safeEntryId("baselines", entry.name));
3687
- const claim = join17(paths.claims, `${baseline.session_key}.overview`);
3688
- if (unlinkStateFile(claim, paths.root, true)) removed.push(safeEntryId("claims", basename8(claim)));
3689
- const refusal = join17(paths.refusals, `${baseline.session_key}.json`);
3690
- if (unlinkStateFile(refusal, paths.root, true)) removed.push(safeEntryId("refusals", basename8(refusal)));
3692
+ const claim = join18(paths.claims, `${baseline.session_key}.overview`);
3693
+ if (unlinkStateFile(claim, paths.root, true)) removed.push(safeEntryId("claims", basename9(claim)));
3694
+ const refusal = join18(paths.refusals, `${baseline.session_key}.json`);
3695
+ if (unlinkStateFile(refusal, paths.root, true)) removed.push(safeEntryId("refusals", basename9(refusal)));
3691
3696
  }
3692
3697
  fsyncDirectory2(paths.project);
3693
3698
  return removed;
@@ -3733,7 +3738,7 @@ var init_state = __esm({
3733
3738
  "REMOTE_PROTOCOL_ERROR",
3734
3739
  "INTERNAL_ERROR"
3735
3740
  ]);
3736
- DIRECTORY_OPEN_FLAGS = constants2.O_RDONLY | (constants2.O_DIRECTORY ?? 0) | (constants2.O_NOFOLLOW ?? 0);
3741
+ DIRECTORY_OPEN_FLAGS = constants3.O_RDONLY | (constants3.O_DIRECTORY ?? 0) | (constants3.O_NOFOLLOW ?? 0);
3737
3742
  }
3738
3743
  });
3739
3744
 
@@ -4072,8 +4077,8 @@ var init_open_notebook_client = __esm({
4072
4077
 
4073
4078
  // src/notebook/git-evidence.ts
4074
4079
  import { spawnSync as spawnSync8 } from "node:child_process";
4075
- import { closeSync as closeSync5, constants as constants3, fstatSync as fstatSync2, lstatSync as lstatSync14, openSync as openSync5, readSync as readSync2, realpathSync as realpathSync8 } from "node:fs";
4076
- import { extname, join as join19, relative as relative13, resolve as resolve13, sep as sep5 } from "node:path";
4080
+ import { closeSync as closeSync6, constants as constants4, fstatSync as fstatSync3, lstatSync as lstatSync15, openSync as openSync6, readSync as readSync2, realpathSync as realpathSync8 } from "node:fs";
4081
+ import { extname, join as join20, relative as relative13, resolve as resolve13, sep as sep5 } from "node:path";
4077
4082
  function git(repo, args, maxBuffer = 4 * 1024 * 1024, timeout = 5e3) {
4078
4083
  const result2 = spawnSync8("git", args, { cwd: repo, encoding: "utf8", maxBuffer, timeout, shell: false });
4079
4084
  return { ok: result2.status === 0, stdout: result2.stdout ?? "" };
@@ -4151,9 +4156,9 @@ function hasSymlinkComponent(root, candidate) {
4151
4156
  const rel = relative13(root, candidate);
4152
4157
  let cursor = root;
4153
4158
  for (const part of rel.split(sep5).filter(Boolean)) {
4154
- cursor = join19(cursor, part);
4159
+ cursor = join20(cursor, part);
4155
4160
  try {
4156
- if (lstatSync14(cursor).isSymbolicLink()) return true;
4161
+ if (lstatSync15(cursor).isSymbolicLink()) return true;
4157
4162
  } catch {
4158
4163
  return true;
4159
4164
  }
@@ -4165,12 +4170,12 @@ function readSafeEvidenceText(repoPath, relativePath2, maxBytes) {
4165
4170
  if (!safe || hasSymlinkComponent(safe.root, safe.candidate)) return { status: "excluded", reason: "unsafe-path" };
4166
4171
  let fd;
4167
4172
  try {
4168
- fd = openSync5(safe.candidate, constants3.O_RDONLY | (constants3.O_NOFOLLOW ?? 0));
4173
+ fd = openSync6(safe.candidate, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
4169
4174
  } catch {
4170
4175
  return { status: "excluded", reason: "not-regular" };
4171
4176
  }
4172
4177
  try {
4173
- const before = fstatSync2(fd);
4178
+ const before = fstatSync3(fd);
4174
4179
  if (!before.isFile()) return { status: "excluded", reason: "not-regular" };
4175
4180
  if (before.size > maxBytes) return { status: "excluded", reason: "oversize" };
4176
4181
  let physical;
@@ -4191,7 +4196,7 @@ function readSafeEvidenceText(repoPath, relativePath2, maxBytes) {
4191
4196
  if (total > maxBytes) return { status: "excluded", reason: "oversize" };
4192
4197
  chunks.push(chunk.subarray(0, count));
4193
4198
  }
4194
- const after = fstatSync2(fd);
4199
+ const after = fstatSync3(fd);
4195
4200
  if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || total !== before.size) return { status: "excluded", reason: "changed-during-read" };
4196
4201
  const bytes = Buffer.concat(chunks, total);
4197
4202
  if (bytes.includes(0)) return { status: "excluded", reason: "binary" };
@@ -4204,7 +4209,7 @@ function readSafeEvidenceText(repoPath, relativePath2, maxBytes) {
4204
4209
  if (looksSecret(relativePath2, content)) return { status: "excluded", reason: "secret-like" };
4205
4210
  return { status: "present", content, content_sha256: sha256Hex(bytes), bytes: total };
4206
4211
  } finally {
4207
- closeSync5(fd);
4212
+ closeSync6(fd);
4208
4213
  }
4209
4214
  }
4210
4215
  function looksGenerated(path) {
@@ -4294,10 +4299,10 @@ var init_git_evidence = __esm({
4294
4299
 
4295
4300
  // src/notebook/remote-mutation-journal.ts
4296
4301
  import { randomUUID as randomUUID4 } from "node:crypto";
4297
- import { join as join20 } from "node:path";
4302
+ import { join as join21 } from "node:path";
4298
4303
  function journalPath(root, projectSlug, operationId) {
4299
4304
  if (!/^[a-f0-9-]{16,64}$/iu.test(operationId)) throw new NotebookError("INVALID_INPUT", "Invalid remote mutation operation ID");
4300
- return join20(ensureNotebookState(root, projectSlug).journals, `${operationId}.json`);
4305
+ return join21(ensureNotebookState(root, projectSlug).journals, `${operationId}.json`);
4301
4306
  }
4302
4307
  function remoteMutationJournalPath(root, projectSlug, operationId) {
4303
4308
  return journalPath(root, projectSlug, operationId);
@@ -4317,7 +4322,7 @@ function listRemoteMutationJournals(root, projectSlug) {
4317
4322
  if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9-]{16,64}\.json$/iu.test(entry.name)) {
4318
4323
  throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
4319
4324
  }
4320
- const read = readNotebookStateJson(join20(paths.journals, entry.name), paths.root, 65536);
4325
+ const read = readNotebookStateJson(join21(paths.journals, entry.name), paths.root, 65536);
4321
4326
  const journal = read.value === void 0 ? null : parseRemoteMutationJournal(read.value);
4322
4327
  if (!journal || `${journal.operation_id}.json` !== entry.name) {
4323
4328
  throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
@@ -4671,7 +4676,7 @@ var init_reconcile = __esm({
4671
4676
 
4672
4677
  // src/notebook/entropy.ts
4673
4678
  import { existsSync as existsSync16 } from "node:fs";
4674
- import { createHash as createHash13 } from "node:crypto";
4679
+ import { createHash as createHash14 } from "node:crypto";
4675
4680
  import { resolve as resolve16 } from "node:path";
4676
4681
  import { spawnSync as spawnSync11 } from "node:child_process";
4677
4682
  function listAllTrackedDocuments(config) {
@@ -4726,7 +4731,7 @@ async function compactSessionCaptures(input) {
4726
4731
  const toCompact = sessionHeaders.slice(maxRecent);
4727
4732
  const notesToCompact = typeof input.client.hydrateNotes === "function" ? await input.client.hydrateNotes(toCompact) : toCompact;
4728
4733
  const archiveTitle = "Session Captures Archive (Prior Sessions)";
4729
- const slugHash = createHash13("sha256").update(`session-capture-archive:${input.projectSlug}`).digest("hex").slice(0, 32);
4734
+ const slugHash = createHash14("sha256").update(`session-capture-archive:${input.projectSlug}`).digest("hex").slice(0, 32);
4730
4735
  const archiveLogicalId = `user-note:v1:${slugHash}`;
4731
4736
  let existingArchiveContent = "";
4732
4737
  let existingArchiveId = null;
@@ -5294,7 +5299,7 @@ var init_capture = __esm({
5294
5299
 
5295
5300
  // src/mcp-server.ts
5296
5301
  import { existsSync as existsSync22, statSync as statSync8 } from "node:fs";
5297
- import { dirname as dirname20, join as join31, resolve as resolve22 } from "node:path";
5302
+ import { dirname as dirname20, join as join32, resolve as resolve22 } from "node:path";
5298
5303
  import { fileURLToPath as fileURLToPath6 } from "node:url";
5299
5304
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5300
5305
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -5360,6 +5365,15 @@ function isHostScoped(finding2) {
5360
5365
  function gatesProject(finding2) {
5361
5366
  return finding2.status === "fail" && !isHostScoped(finding2);
5362
5367
  }
5368
+ var FINDING_DETAIL_LIMIT = 20;
5369
+ function describeFinding(finding2) {
5370
+ const details = (finding2.details ?? []).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
5371
+ if (details.length === 0) return `${finding2.id}: ${finding2.summary}`;
5372
+ const shown = details.slice(0, FINDING_DETAIL_LIMIT).map((line) => ` - ${line.replace(/\r?\n/g, "\n ")}`);
5373
+ const omitted = details.length - shown.length;
5374
+ if (omitted > 0) shown.push(` - (+${omitted} more; run pj audit --rules ${finding2.id} for the full list)`);
5375
+ return [`${finding2.id}: ${finding2.summary}`, ...shown].join("\n");
5376
+ }
5363
5377
 
5364
5378
  // src/recipes/Recipe.ts
5365
5379
  function commandStatus(result2, dryRun) {
@@ -5418,7 +5432,7 @@ var Recipe = class {
5418
5432
  }
5419
5433
  if (!finding2.fixable) {
5420
5434
  phases.push({ id: check.id, status: "failed", changedFiles: [], message: finding2.summary });
5421
- errors.push(`${check.id}: ${finding2.summary}`);
5435
+ errors.push(describeFinding({ id: check.id, summary: finding2.summary, details: finding2.details }));
5422
5436
  break;
5423
5437
  }
5424
5438
  const migrated = await check.migrate(ctx, finding2);
@@ -5428,7 +5442,7 @@ var Recipe = class {
5428
5442
  logs.push(`${check.id}: ${migrated.summary}`);
5429
5443
  changedFiles.push(...actualChanges);
5430
5444
  if (status === "failed") {
5431
- errors.push(`${check.id}: ${migrated.summary}`);
5445
+ errors.push(describeFinding({ id: check.id, summary: migrated.summary, details: migrated.details }));
5432
5446
  break;
5433
5447
  }
5434
5448
  if (!ctx.dryRun) {
@@ -6868,7 +6882,11 @@ function attestBmadInstallerFiles(inventory, base, files, label) {
6868
6882
 
6869
6883
  // src/parity/skill-roots.ts
6870
6884
  var CANONICAL_CLI_SKILLS_ALIAS = "../.agents/skills";
6871
- var SUPPORTED_SKILLS_ALIASES = SUPPORTED_CLIS.map((cli) => cli.skillsRoot);
6885
+ var SKILLEX_ONLY_ALIASES_AFTER = { ".kimi-code/skills": ".kimi/skills" };
6886
+ var SUPPORTED_SKILLS_ALIASES = SUPPORTED_CLIS.flatMap((cli) => {
6887
+ const follower = SKILLEX_ONLY_ALIASES_AFTER[cli.skillsRoot];
6888
+ return follower ? [cli.skillsRoot, follower] : [cli.skillsRoot];
6889
+ });
6872
6890
  var SKILL_ROOTS_QUARANTINE = ".pjangler-quarantine";
6873
6891
  function lstatOrUndefined(path) {
6874
6892
  try {
@@ -7274,20 +7292,20 @@ function planSkillRoots(repoRoot, options = {}) {
7274
7292
  for (const name of readdirSync6(real).sort()) {
7275
7293
  const path = join7(real, name);
7276
7294
  const label = `${alias}/${name}`;
7277
- const entryStat = lstatSync6(path);
7295
+ const entryStat2 = lstatSync6(path);
7278
7296
  const rows = tracked.get(name) ?? [];
7279
- const candidate = { alias, name, path, label, kind: entryStat.isSymbolicLink() ? "link" : entryStat.isDirectory() ? "directory" : "file", tracked: rows.map((row) => row.path) };
7297
+ const candidate = { alias, name, path, label, kind: entryStat2.isSymbolicLink() ? "link" : entryStat2.isDirectory() ? "directory" : "file", tracked: rows.map((row) => row.path) };
7280
7298
  if (name === ".git") {
7281
7299
  candidate.staticBlock = `${label} is git metadata; refusing to relocate another repository's work tree`;
7282
- } else if (entryStat.isSymbolicLink()) {
7300
+ } else if (entryStat2.isSymbolicLink()) {
7283
7301
  candidate.text = readlinkSync5(path);
7284
7302
  candidate.resolution = resolveIn(diskLayout, path);
7285
7303
  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`;
7286
- } else if (!entryStat.isDirectory() && !entryStat.isFile()) {
7304
+ } else if (!entryStat2.isDirectory() && !entryStat2.isFile()) {
7287
7305
  candidate.staticBlock = `${label} is a special file; refusing to relocate it`;
7288
7306
  } else {
7289
7307
  candidate.staticBlock = entryHazard(path, label);
7290
- if (!candidate.staticBlock && rootDevice !== void 0 && entryStat.dev !== rootDevice) candidate.staticBlock = `${label} is on a different filesystem than .agents/skills; refusing a copying move`;
7308
+ if (!candidate.staticBlock && rootDevice !== void 0 && entryStat2.dev !== rootDevice) candidate.staticBlock = `${label} is on a different filesystem than .agents/skills; refusing a copying move`;
7291
7309
  const linkRow = rows.find((row) => row.mode === "120000");
7292
7310
  const gitlink = rows.find((row) => row.mode === "160000");
7293
7311
  if (!candidate.staticBlock && gitlink) candidate.staticBlock = `${label} holds a tracked submodule (${gitlink.path}); refusing to relocate it`;
@@ -7975,7 +7993,7 @@ function readHandoffRecord(ctx) {
7975
7993
  }
7976
7994
  function writeHandoffRecord(ctx, names) {
7977
7995
  const path = handoffRecordPath(ctx);
7978
- mkdirSync5(dirname6(path), { recursive: true });
7996
+ mkdirSync5(dirname6(path), { recursive: true, mode: 448 });
7979
7997
  const temporary = `${path}.${process.pid}.tmp`;
7980
7998
  writeFileSync5(temporary, `${JSON.stringify({ project: realpathOf(ctx.repoRoot) ?? resolve4(ctx.repoRoot), excluded: [...new Set(names)].sort() }, null, 2)}
7981
7999
  `);
@@ -9194,14 +9212,324 @@ function createG33CompanionCheck() {
9194
9212
  }
9195
9213
 
9196
9214
  // src/parity/rules.ts
9197
- import { existsSync as existsSync8, lstatSync as lstatSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync10, readlinkSync as readlinkSync8, readdirSync as readdirSync7, realpathSync as realpathSync4, renameSync as renameSync4, symlinkSync as symlinkSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8, chmodSync as chmodSync3, copyFileSync, rmSync as rmSync5 } from "node:fs";
9198
- import { basename as basename4, dirname as dirname8, join as join11, relative as relative9, resolve as resolve6 } from "node:path";
9215
+ import { existsSync as existsSync8, lstatSync as lstatSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync11, readlinkSync as readlinkSync8, readdirSync as readdirSync8, realpathSync as realpathSync4, renameSync as renameSync4, symlinkSync as symlinkSync3, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9, chmodSync as chmodSync3, copyFileSync, rmSync as rmSync5 } from "node:fs";
9216
+
9217
+ // src/bmad/setup-scripts.ts
9218
+ import { createHash as createHash8 } from "node:crypto";
9219
+ import { closeSync as closeSync2, fchmodSync, fstatSync, linkSync, lstatSync as lstatSync9, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync9, readdirSync as readdirSync7, rmdirSync as rmdirSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7, constants } from "node:fs";
9220
+ import { basename as basename4, join as join10 } from "node:path";
9221
+ function entryStat(path) {
9222
+ try {
9223
+ return lstatSync9(path);
9224
+ } catch (error) {
9225
+ if (error.code === "ENOENT") return void 0;
9226
+ throw error;
9227
+ }
9228
+ }
9229
+ var RUNTIME_SCRIPTS = ["config_utils.py", "memlog.py", "render_skill.py", "resolve_config.py", "resolve_customization.py"];
9230
+ function manifestRows(text) {
9231
+ const rows = [];
9232
+ let row = [], field2 = "", quoted = false, closed = false, inQuotes = false;
9233
+ for (let i = 0; i < text.length; i++) {
9234
+ const c = text[i];
9235
+ if (inQuotes) {
9236
+ if (c === '"' && text[i + 1] === '"') {
9237
+ field2 += '"';
9238
+ i++;
9239
+ } else if (c === '"') {
9240
+ inQuotes = false;
9241
+ closed = true;
9242
+ } else field2 += c;
9243
+ } else if (c === '"' && !field2 && !closed) {
9244
+ inQuotes = true;
9245
+ quoted = true;
9246
+ } else if (c === ",") {
9247
+ row.push(field2);
9248
+ field2 = "";
9249
+ quoted = closed = false;
9250
+ } else if (c === "\n" || c === "\r") {
9251
+ if (c === "\r" && text[i + 1] === "\n") i++;
9252
+ row.push(field2);
9253
+ if (row.length !== 1 || row[0]) rows.push(row);
9254
+ row = [];
9255
+ field2 = "";
9256
+ quoted = closed = false;
9257
+ } else if (closed || c === '"' || quoted && c === "\r") throw new Error("Malformed BMAD file manifest CSV");
9258
+ else field2 += c;
9259
+ }
9260
+ if (inQuotes) throw new Error("Unterminated BMAD file manifest CSV field");
9261
+ if (row.length || field2 || closed) {
9262
+ row.push(field2);
9263
+ rows.push(row);
9264
+ }
9265
+ if (rows.some((r) => r.length !== 5) || rows[0]?.join("\0") !== "type\0name\0module\0path\0hash") {
9266
+ throw new Error("Invalid BMAD file manifest columns");
9267
+ }
9268
+ return rows.slice(1);
9269
+ }
9270
+ function sameIdentity(a, b) {
9271
+ return !!a && a.dev === b.dev && a.ino === b.ino && a.isDirectory() && !a.isSymbolicLink();
9272
+ }
9273
+ var DeclinableError = class extends Error {
9274
+ constructor(message, kind) {
9275
+ super(message);
9276
+ this.kind = kind;
9277
+ }
9278
+ };
9279
+ var FIELD_SEP = String.fromCharCode(0);
9280
+ var MANIFEST_UNUSABLE = "Installed BMAD file manifest is missing or not a real file; cannot attest recovered scripts";
9281
+ function inspectBmadSetupScripts(repoRoot, version, installedVersion) {
9282
+ const bmadDir = join10(repoRoot, "_bmad");
9283
+ const dest = join10(bmadDir, "scripts");
9284
+ try {
9285
+ const bmad = entryStat(bmadDir);
9286
+ if (!bmad) return { state: "decline", why: "_bmad is absent", missing: [...RUNTIME_SCRIPTS] };
9287
+ if (!bmad.isDirectory() || bmad.isSymbolicLink()) return { state: "unsafe", error: "_bmad is not a real directory" };
9288
+ const destStat = entryStat(dest);
9289
+ if (destStat && (!destStat.isDirectory() || destStat.isSymbolicLink())) return { state: "unsafe", error: "_bmad/scripts is not a real directory" };
9290
+ const missing = [];
9291
+ for (const name of RUNTIME_SCRIPTS) {
9292
+ const existing = destStat ? entryStat(join10(dest, name)) : void 0;
9293
+ if (!existing) missing.push(name);
9294
+ else if (!existing.isFile() || existing.isSymbolicLink()) return { state: "unsafe", error: `Existing _bmad/scripts/${name} is not a regular file; refusing overwrite` };
9295
+ }
9296
+ if (!missing.length) return { state: "present" };
9297
+ const configDir = entryStat(join10(bmadDir, "_config"));
9298
+ if (configDir && (!configDir.isDirectory() || configDir.isSymbolicLink())) return { state: "unsafe", error: MANIFEST_UNUSABLE };
9299
+ const manifestPath = join10(bmadDir, "_config", "files-manifest.csv");
9300
+ const manifestFile = configDir ? entryStat(manifestPath) : void 0;
9301
+ if (!manifestFile) return { state: "decline", why: "_bmad/_config/files-manifest.csv is absent, so recovered scripts cannot be attested", missing };
9302
+ if (!manifestFile.isFile() || manifestFile.isSymbolicLink()) return { state: "unsafe", error: MANIFEST_UNUSABLE };
9303
+ const installed = installedVersion?.();
9304
+ if (installed && installed !== version) {
9305
+ return { state: "decline", why: `this install is bmad-method ${installed}, not the pinned ${version} that recovery restores from`, missing };
9306
+ }
9307
+ const rows = manifestRows(readFileSync9(manifestPath, "utf8"));
9308
+ const unattested = missing.filter((name) => !rows.some((row) => row[3] === `scripts/${name}`));
9309
+ if (unattested.length) {
9310
+ return { state: "decline", why: `_bmad/_config/files-manifest.csv has no row for ${unattested.map((name) => `scripts/${name}`).join(", ")}, so recovered scripts cannot be attested`, missing };
9311
+ }
9312
+ return { state: "missing", missing };
9313
+ } catch (error) {
9314
+ return { state: "unsafe", error: error instanceof Error ? error.message : String(error) };
9315
+ }
9316
+ }
9317
+ function planBmadSetupScripts(repoRoot, pkg, version) {
9318
+ const dest = join10(repoRoot, "_bmad", "scripts");
9319
+ if (!pkg.root || !pkg.entry || pkg.version !== version) return { files: [], declinable: "package", error: `Pinned bmad-method@${version} package could not be resolved: ${pkg.reason ?? "version mismatch"}` };
9320
+ let fingerprint2;
9321
+ try {
9322
+ fingerprint2 = installerFingerprint(pkg.root);
9323
+ } catch (error) {
9324
+ return { files: [], declinable: "package", error: `Could not fingerprint pinned BMAD installer: ${String(error)}` };
9325
+ }
9326
+ if (VERIFIED_BMAD_INSTALLERS[fingerprint2] !== version) return { files: [], declinable: "package", error: `Unverified BMAD installer fingerprint ${fingerprint2}; refusing script recovery` };
9327
+ const source = join10(pkg.root, "src", "scripts");
9328
+ try {
9329
+ const bmad = lstatSync9(join10(repoRoot, "_bmad"));
9330
+ if (!bmad.isDirectory() || bmad.isSymbolicLink()) throw new Error("_bmad is not a real directory");
9331
+ const destStat = entryStat(dest);
9332
+ if (destStat && (!destStat.isDirectory() || destStat.isSymbolicLink())) throw new Error("_bmad/scripts is not a real directory");
9333
+ const names = readdirSync7(source).filter((name) => !name.startsWith(".") && !name.endsWith(".pyc") && name !== "tests" && name !== "__pycache__" && name !== ".pytest_cache").sort();
9334
+ if (names.join(FIELD_SEP) !== [...RUNTIME_SCRIPTS].sort().join(FIELD_SEP)) throw new DeclinableError("Pinned installer script inventory changed; refusing recovery", "package");
9335
+ const missing = [];
9336
+ for (const name of names) {
9337
+ const existing = entryStat(join10(dest, name));
9338
+ if (!existing) missing.push(name);
9339
+ else if (!existing.isFile() || existing.isSymbolicLink()) throw new Error(`Existing _bmad/scripts/${name} is not a regular file; refusing overwrite`);
9340
+ }
9341
+ const files = [];
9342
+ if (!missing.length) return { files, source };
9343
+ const manifestPath = join10(repoRoot, "_bmad", "_config", "files-manifest.csv");
9344
+ const manifestDir = entryStat(join10(repoRoot, "_bmad", "_config"));
9345
+ const manifestFile = entryStat(manifestPath);
9346
+ if (!manifestDir?.isDirectory() || manifestDir.isSymbolicLink() || !manifestFile?.isFile() || manifestFile.isSymbolicLink()) {
9347
+ throw new Error(MANIFEST_UNUSABLE);
9348
+ }
9349
+ const rows = manifestRows(readFileSync9(manifestPath, "utf8"));
9350
+ for (const name of missing) {
9351
+ const src = join10(source, name);
9352
+ if (!lstatSync9(src).isFile() || lstatSync9(src).isSymbolicLink()) throw new DeclinableError(`Installer script is not a regular file: ${name}`, "package");
9353
+ const matching = rows.filter((row) => row[3] === `scripts/${name}`);
9354
+ const expectedHash = createHash8("sha256").update(readFileSync9(src)).digest("hex");
9355
+ if (matching.length !== 1 || matching[0].join(FIELD_SEP) !== ["py", name.slice(0, -3), "scripts", `scripts/${name}`, expectedHash].join(FIELD_SEP)) {
9356
+ throw new DeclinableError(`Installed file manifest does not attest pinned script ${name}`, "attest");
9357
+ }
9358
+ files.push(join10(dest, name));
9359
+ }
9360
+ return { files, source };
9361
+ } catch (error) {
9362
+ return { files: [], ...error instanceof DeclinableError ? { declinable: error.kind } : {}, error: `BMAD script recovery refused: ${error instanceof Error ? error.message : String(error)}` };
9363
+ }
9364
+ }
9365
+ function applyBmadSetupScripts(repoRoot, pkg, version, afterPublish) {
9366
+ const plan = planBmadSetupScripts(repoRoot, pkg, version);
9367
+ if (plan.error || !plan.files.length) return { files: [], ...plan.error ? { error: plan.error } : {} };
9368
+ const parent = join10(repoRoot, "_bmad");
9369
+ const dest = join10(parent, "scripts");
9370
+ const expectedParent = entryStat(parent);
9371
+ const expectedDir = entryStat(dest);
9372
+ let stage;
9373
+ let stageFd;
9374
+ let parentFd;
9375
+ const created = [];
9376
+ const stagedIdentities = /* @__PURE__ */ new Map();
9377
+ const published = /* @__PURE__ */ new Map();
9378
+ let ambiguousPublication;
9379
+ const matchesPayload = (path, owned) => {
9380
+ const stat = entryStat(path);
9381
+ return !!stat?.isFile() && !stat.isSymbolicLink() && stat.dev === owned.dev && stat.ino === owned.ino && (stat.mode & 4095) === owned.mode && readFileSync9(path).equals(owned.bytes);
9382
+ };
9383
+ let madeDir = false;
9384
+ let dirFd;
9385
+ let result2 = { files: [] };
9386
+ try {
9387
+ parentFd = openSync2(parent, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
9388
+ const pinnedParent = fstatSync(parentFd);
9389
+ if (!sameIdentity(expectedParent, pinnedParent) || !sameIdentity(entryStat(parent), pinnedParent)) throw new Error("BMAD parent directory changed before staging");
9390
+ const anchoredParent = `/proc/self/fd/${parentFd}`;
9391
+ stage = mkdtempSync2(join10(anchoredParent, ".scripts-stage-"));
9392
+ stageFd = openSync2(stage, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
9393
+ const pinnedStage = fstatSync(stageFd);
9394
+ const stageName = basename4(stage);
9395
+ const anchoredStage = `/proc/self/fd/${stageFd}`;
9396
+ if (!sameIdentity(entryStat(join10(anchoredParent, stageName)), pinnedStage)) throw new Error("Staging directory changed during creation");
9397
+ for (const path of plan.files) {
9398
+ const name = basename4(path);
9399
+ const src = join10(plan.source, name);
9400
+ const staged = join10(anchoredStage, name);
9401
+ const fileFd = openSync2(staged, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
9402
+ try {
9403
+ const stat = fstatSync(fileFd);
9404
+ const bytes = readFileSync9(src);
9405
+ const mode = lstatSync9(src).mode & 4095;
9406
+ stagedIdentities.set(name, { dev: stat.dev, ino: stat.ino, bytes, mode });
9407
+ writeFileSync7(fileFd, bytes);
9408
+ fchmodSync(fileFd, mode);
9409
+ } finally {
9410
+ closeSync2(fileFd);
9411
+ }
9412
+ }
9413
+ if (!sameIdentity(entryStat(parent), pinnedParent)) throw new Error("BMAD parent directory changed while staging");
9414
+ const current = planBmadSetupScripts(repoRoot, pkg, version);
9415
+ if (current.error || current.files.join("\0") !== plan.files.join("\0")) throw new Error(current.error ?? "Script destination changed while staging");
9416
+ const anchoredDest = join10(anchoredParent, "scripts");
9417
+ if (!entryStat(anchoredDest)) {
9418
+ mkdirSync6(anchoredDest);
9419
+ madeDir = true;
9420
+ }
9421
+ dirFd = openSync2(anchoredDest, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
9422
+ const pinned = fstatSync(dirFd);
9423
+ if (expectedDir && !sameIdentity(expectedDir, pinned)) throw new Error("Script directory changed while staging");
9424
+ if (!sameIdentity(entryStat(parent), pinnedParent) || !sameIdentity(entryStat(dest), pinned)) throw new Error("Script or parent directory changed during publication");
9425
+ const anchored = `/proc/self/fd/${dirFd}`;
9426
+ for (const path of plan.files) {
9427
+ const name = basename4(path);
9428
+ const staged = join10(anchoredStage, name);
9429
+ const output = join10(anchored, name);
9430
+ const owned = stagedIdentities.get(name);
9431
+ if (!sameIdentity(entryStat(parent), pinnedParent) || !sameIdentity(entryStat(dest), pinned)) throw new Error("Script or parent directory changed during publication");
9432
+ if (!matchesPayload(staged, owned)) throw new Error(`Staged script changed before publication: ${name}`);
9433
+ if (entryStat(output)) {
9434
+ ambiguousPublication = path;
9435
+ throw new Error(`Script destination appeared before publication: ${name}`);
9436
+ }
9437
+ try {
9438
+ linkSync(staged, output);
9439
+ } catch (error) {
9440
+ if (entryStat(output)) ambiguousPublication = path;
9441
+ throw error;
9442
+ }
9443
+ published.set(path, owned);
9444
+ created.push(path);
9445
+ if (!matchesPayload(output, owned)) throw new Error(`Published script changed during publication: ${name}`);
9446
+ afterPublish?.(created.length);
9447
+ }
9448
+ if (!sameIdentity(entryStat(parent), pinnedParent) || !sameIdentity(entryStat(dest), pinned)) throw new Error("Script or parent directory changed during publication");
9449
+ for (const path of created) {
9450
+ if (!matchesPayload(join10(anchored, basename4(path)), stagedIdentities.get(basename4(path)))) throw new Error(`Published script changed before completion: ${basename4(path)}`);
9451
+ }
9452
+ result2 = { files: created };
9453
+ } catch (error) {
9454
+ const recoveryErrors = [];
9455
+ for (const [path, owned] of [...published].reverse()) {
9456
+ try {
9457
+ const anchoredPath = join10(`/proc/self/fd/${dirFd}`, basename4(path));
9458
+ const stat = entryStat(anchoredPath);
9459
+ if (matchesPayload(anchoredPath, owned)) unlinkSync5(anchoredPath);
9460
+ else if (stat) recoveryErrors.push(`Concurrent replacement or mutation retained: ${path}`);
9461
+ else recoveryErrors.push(`Published file absent from pinned directory; ownership unresolved: ${path}`);
9462
+ } catch (recoveryError) {
9463
+ recoveryErrors.push(`Could not restore ${path}: ${String(recoveryError)}`);
9464
+ }
9465
+ }
9466
+ if (ambiguousPublication) recoveryErrors.push(`Publication not confirmed; destination retained or ownership unresolved: ${ambiguousPublication}`);
9467
+ if (madeDir && dirFd !== void 0 && parentFd !== void 0) {
9468
+ try {
9469
+ const pinned = fstatSync(dirFd);
9470
+ const anchoredDest = join10(`/proc/self/fd/${parentFd}`, "scripts");
9471
+ if (sameIdentity(entryStat(anchoredDest), pinned) && readdirSync7(anchoredDest).length === 0) rmdirSync3(anchoredDest);
9472
+ else recoveryErrors.push(`Created script directory moved or retained: ${dest}`);
9473
+ } catch (recoveryError) {
9474
+ recoveryErrors.push(`Could not remove empty script directory: ${String(recoveryError)}`);
9475
+ }
9476
+ }
9477
+ result2 = { files: [], error: `BMAD script recovery failed: ${error instanceof Error ? error.message : String(error)}${recoveryErrors.length ? `; recovery incomplete: ${recoveryErrors.join("; ")}` : ""}` };
9478
+ } finally {
9479
+ const cleanupErrors = [];
9480
+ if (stageFd !== void 0) {
9481
+ try {
9482
+ const pinnedStage = fstatSync(stageFd);
9483
+ const anchoredStage = `/proc/self/fd/${stageFd}`;
9484
+ for (const [name, owned] of stagedIdentities) {
9485
+ const staged = join10(anchoredStage, name);
9486
+ const stat = entryStat(staged);
9487
+ if (stat?.isFile() && !stat.isSymbolicLink() && stat.dev === owned.dev && stat.ino === owned.ino) unlinkSync5(staged);
9488
+ else cleanupErrors.push(`Staged file replaced, modified or absent: ${name}`);
9489
+ }
9490
+ const stagePath = join10(`/proc/self/fd/${parentFd}`, basename4(stage));
9491
+ if (sameIdentity(entryStat(stagePath), pinnedStage) && readdirSync7(anchoredStage).length === 0) rmdirSync3(stagePath);
9492
+ else cleanupErrors.push(`Staging directory moved or contains foreign entries: ${join10(parent, basename4(stage))}`);
9493
+ } catch (cleanupError) {
9494
+ cleanupErrors.push(`Could not clean owned staging directory: ${String(cleanupError)}`);
9495
+ }
9496
+ closeSync2(stageFd);
9497
+ } else if (stage) cleanupErrors.push(`Staging directory identity unavailable: ${join10(parent, basename4(stage))}`);
9498
+ if (cleanupErrors.length && result2.files.length) {
9499
+ for (const [path, owned] of [...published].reverse()) {
9500
+ try {
9501
+ const output = join10(`/proc/self/fd/${dirFd}`, basename4(path));
9502
+ if (matchesPayload(output, owned)) unlinkSync5(output);
9503
+ else cleanupErrors.push(`Concurrent replacement or mutation retained: ${path}`);
9504
+ } catch (error) {
9505
+ cleanupErrors.push(`Could not restore ${path}: ${String(error)}`);
9506
+ }
9507
+ }
9508
+ if (madeDir && dirFd !== void 0 && parentFd !== void 0) {
9509
+ try {
9510
+ const anchoredDest = join10(`/proc/self/fd/${parentFd}`, "scripts");
9511
+ if (sameIdentity(entryStat(anchoredDest), fstatSync(dirFd)) && readdirSync7(anchoredDest).length === 0) rmdirSync3(anchoredDest);
9512
+ else cleanupErrors.push(`Created script directory moved or retained: ${dest}`);
9513
+ } catch (error) {
9514
+ cleanupErrors.push(`Could not remove empty script directory: ${String(error)}`);
9515
+ }
9516
+ }
9517
+ }
9518
+ if (dirFd !== void 0) closeSync2(dirFd);
9519
+ if (parentFd !== void 0) closeSync2(parentFd);
9520
+ if (cleanupErrors.length) result2 = { files: [], error: `${result2.error ? `${result2.error}; ` : "BMAD script staging cleanup incomplete: "}recovery incomplete: ${cleanupErrors.join("; ")}` };
9521
+ }
9522
+ return result2;
9523
+ }
9524
+
9525
+ // src/parity/rules.ts
9526
+ import { basename as basename5, dirname as dirname8, join as join12, relative as relative9, resolve as resolve6 } from "node:path";
9199
9527
  import { spawnSync as spawnSync5 } from "node:child_process";
9200
9528
  import YAML4 from "yaml";
9201
9529
 
9202
9530
  // src/bmad/reserved-modules.ts
9203
- import { existsSync as existsSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
9204
- import { join as join10 } from "node:path";
9531
+ import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "node:fs";
9532
+ import { join as join11 } from "node:path";
9205
9533
  import YAML3 from "yaml";
9206
9534
  var BMAD_RESERVED_DIR_NAMES = ["custom", "scripts", "_config", "_memory", "memory", "render"];
9207
9535
  var indentOf = (line) => line.length - line.trimStart().length;
@@ -9258,19 +9586,19 @@ function stripReservedModuleEntries(raw) {
9258
9586
  return { text: lines.filter((_, index) => !drop.has(index)).join(""), evicted };
9259
9587
  }
9260
9588
  function evictReservedModuleEntries(repoRoot, dryRun) {
9261
- const manifestPath = join10(repoRoot, "_bmad", "_config", "manifest.yaml");
9589
+ const manifestPath = join11(repoRoot, "_bmad", "_config", "manifest.yaml");
9262
9590
  const none = { evicted: [], changedFiles: [], details: [] };
9263
9591
  if (!existsSync7(manifestPath)) return none;
9264
9592
  let original;
9265
9593
  try {
9266
- original = readFileSync9(manifestPath, "utf8");
9594
+ original = readFileSync10(manifestPath, "utf8");
9267
9595
  } catch {
9268
9596
  return none;
9269
9597
  }
9270
9598
  const stripped = stripReservedModuleEntries(original);
9271
9599
  if (!stripped) return none;
9272
9600
  const names = stripped.evicted.map((name) => `"${name}"`).join(", ");
9273
- if (!dryRun) writeFileSync7(manifestPath, stripped.text, "utf8");
9601
+ if (!dryRun) writeFileSync8(manifestPath, stripped.text, "utf8");
9274
9602
  return {
9275
9603
  evicted: stripped.evicted,
9276
9604
  changedFiles: [manifestPath],
@@ -9984,17 +10312,17 @@ function normalizeNewlines(value) {
9984
10312
  return value.replace(/\r\n/g, "\n");
9985
10313
  }
9986
10314
  function readText(path) {
9987
- return normalizeNewlines(readFileSync10(path, "utf8"));
10315
+ return normalizeNewlines(readFileSync11(path, "utf8"));
9988
10316
  }
9989
10317
  function safeReadText2(path) {
9990
10318
  return existsSync8(path) ? readText(path) : null;
9991
10319
  }
9992
10320
  function ensureParent(path) {
9993
- mkdirSync6(dirname8(path), { recursive: true });
10321
+ mkdirSync7(dirname8(path), { recursive: true });
9994
10322
  }
9995
10323
  function writeText(path, content) {
9996
10324
  ensureParent(path);
9997
- writeFileSync8(path, content);
10325
+ writeFileSync9(path, content);
9998
10326
  }
9999
10327
  function tryParseJson(text) {
10000
10328
  if (!text) return null;
@@ -10020,12 +10348,12 @@ function readSymlinkTarget(path) {
10020
10348
  }
10021
10349
  function ensureSymlink(path, target, dryRun) {
10022
10350
  if (existsSync8(path)) {
10023
- const stat = lstatSync9(path);
10351
+ const stat = lstatSync10(path);
10024
10352
  if (stat.isSymbolicLink()) {
10025
10353
  const current = readSymlinkTarget(path);
10026
10354
  if (current === target) return { changed: false };
10027
10355
  if (!dryRun) {
10028
- unlinkSync5(path);
10356
+ unlinkSync6(path);
10029
10357
  symlinkSync3(target, path);
10030
10358
  }
10031
10359
  return { changed: true };
@@ -10036,12 +10364,12 @@ function ensureSymlink(path, target, dryRun) {
10036
10364
  return { changed: true };
10037
10365
  }
10038
10366
  function bootstrapAgentsFile(repoRoot, dryRun) {
10039
- const agentsPath = join11(repoRoot, "AGENTS.md");
10367
+ const agentsPath = join12(repoRoot, "AGENTS.md");
10040
10368
  if (existsSync8(agentsPath)) return { changedFiles: [], details: [] };
10041
10369
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
10042
- const source = join11(repoRoot, file);
10370
+ const source = join12(repoRoot, file);
10043
10371
  if (!existsSync8(source)) continue;
10044
- const stat = lstatSync9(source);
10372
+ const stat = lstatSync10(source);
10045
10373
  if (stat.isSymbolicLink()) continue;
10046
10374
  if (stat.isFile()) {
10047
10375
  if (!dryRun) renameSync4(source, agentsPath);
@@ -10049,9 +10377,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
10049
10377
  }
10050
10378
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
10051
10379
  }
10052
- const readmePath = join11(repoRoot, "README.md");
10380
+ const readmePath = join12(repoRoot, "README.md");
10053
10381
  if (existsSync8(readmePath)) {
10054
- const stat = lstatSync9(readmePath);
10382
+ const stat = lstatSync10(readmePath);
10055
10383
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
10056
10384
  if (!dryRun) copyFileSync(readmePath, agentsPath);
10057
10385
  return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
@@ -10089,11 +10417,11 @@ function yamlGet(text, keyPath) {
10089
10417
  return "";
10090
10418
  }
10091
10419
  function discoverRoles(repoRoot) {
10092
- const rolesDir = join11(repoRoot, "agents", "hermes");
10420
+ const rolesDir = join12(repoRoot, "agents", "hermes");
10093
10421
  if (!existsSync8(rolesDir)) return [];
10094
- return readdirSync7(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
10095
- const roleDir = join11(rolesDir, entry.name);
10096
- const roleYamlPath = join11(roleDir, "role.yaml");
10422
+ return readdirSync8(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
10423
+ const roleDir = join12(rolesDir, entry.name);
10424
+ const roleYamlPath = join12(roleDir, "role.yaml");
10097
10425
  if (!existsSync8(roleYamlPath)) return null;
10098
10426
  const text = readText(roleYamlPath);
10099
10427
  const runtimeRepoRaw = yamlGet(text, "runtime.github_repo");
@@ -10126,26 +10454,26 @@ function discoverRoles(repoRoot) {
10126
10454
  }).filter((value) => Boolean(value));
10127
10455
  }
10128
10456
  function templateScript(ctx, name) {
10129
- const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
10457
+ const source = join12(ctx.pjanglerRoot, ".mise", "scripts", name);
10130
10458
  return existsSync8(source) ? readText(source) : void 0;
10131
10459
  }
10132
10460
  function templateVersioningScript(ctx) {
10133
10461
  return templateScript(ctx, "versioning.sh");
10134
10462
  }
10135
10463
  function templateLinkAgentfilesScript(ctx) {
10136
- const source = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".mise", "scripts", "link-agentfiles.sh");
10464
+ const source = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", ".mise", "scripts", "link-agentfiles.sh");
10137
10465
  return existsSync8(source) ? readText(source) : templateScript(ctx, "link-agentfiles.sh");
10138
10466
  }
10139
10467
  function templateMaterializeEnvScript(ctx) {
10140
- const source = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", MATERIALIZE_ENV_SCRIPT_REL);
10468
+ const source = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", MATERIALIZE_ENV_SCRIPT_REL);
10141
10469
  return existsSync8(source) ? readText(source) : void 0;
10142
10470
  }
10143
10471
  function resolveAgentHooksLayer(ctx) {
10144
10472
  const override = process.env.PJ_AGENT_HOOKS_LAYER;
10145
10473
  if (override === "0" || override === "false") return false;
10146
10474
  if (override === "1" || override === "true") return true;
10147
- if (existsSync8(join11(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
10148
- return !existsSync8(join11(ctx.homeDir, ".agents", "hooks"));
10475
+ if (existsSync8(join12(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
10476
+ return !existsSync8(join12(ctx.homeDir, ".agents", "hooks"));
10149
10477
  }
10150
10478
  function evaluateMiseConditionals(template, agentHooksLayer) {
10151
10479
  const out = [];
@@ -10171,13 +10499,13 @@ function evaluateMiseConditionals(template, agentHooksLayer) {
10171
10499
  }
10172
10500
  function renderGeneratedProjectMiseToml(ctx, template) {
10173
10501
  const project = readProjectJson(ctx);
10174
- const projectName = String(project?.project_name ?? basename4(ctx.repoRoot) ?? "project");
10502
+ const projectName = String(project?.project_name ?? basename5(ctx.repoRoot) ?? "project");
10175
10503
  return evaluateMiseConditionals(template, resolveAgentHooksLayer(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
10176
10504
  }
10177
10505
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
10178
- const targetPath = join11(ctx.repoRoot, "mise.toml");
10506
+ const targetPath = join12(ctx.repoRoot, "mise.toml");
10179
10507
  if (existsSync8(targetPath)) return null;
10180
- const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
10508
+ const sourcePath = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
10181
10509
  if (!existsSync8(sourcePath)) return null;
10182
10510
  const rendered = renderGeneratedProjectMiseToml(ctx, readText(sourcePath));
10183
10511
  changedFiles.push(targetPath);
@@ -10185,12 +10513,12 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
10185
10513
  return rendered;
10186
10514
  }
10187
10515
  function templateCommonProjectText(ctx, rel) {
10188
- const path = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
10516
+ const path = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
10189
10517
  return existsSync8(path) ? readText(path) : void 0;
10190
10518
  }
10191
10519
  function lstatIfPresent(path) {
10192
10520
  try {
10193
- return lstatSync9(path);
10521
+ return lstatSync10(path);
10194
10522
  } catch (error) {
10195
10523
  if (error.code === "ENOENT") return void 0;
10196
10524
  throw error;
@@ -10198,7 +10526,7 @@ function lstatIfPresent(path) {
10198
10526
  }
10199
10527
  function templateVersionFilesConf(ctx, repoRoot) {
10200
10528
  if (isObsidianPluginRepo(ctx, repoRoot)) return OBSIDIAN_VERSION_FILES_CONF;
10201
- const packageJson = join11(repoRoot, "package.json");
10529
+ const packageJson = join12(repoRoot, "package.json");
10202
10530
  return existsSync8(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";
10203
10531
  }
10204
10532
  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";
@@ -10208,7 +10536,7 @@ function recordedProjectType(ctx) {
10208
10536
  function isObsidianPluginRepo(ctx, repoRoot) {
10209
10537
  const recorded = recordedProjectType(ctx);
10210
10538
  if (recorded !== void 0) return recorded === "obsidian-plugin";
10211
- const manifest = record2(tryParseJson(safeReadText2(join11(repoRoot, "manifest.json"))));
10539
+ const manifest = record2(tryParseJson(safeReadText2(join12(repoRoot, "manifest.json"))));
10212
10540
  return typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
10213
10541
  }
10214
10542
  function unsupportedVersionTypes(conf, script) {
@@ -10669,7 +10997,7 @@ function describeRewrite(before, after) {
10669
10997
  return difference ? `migrate would still rewrite ${difference} in mise.toml` : "migrate would still re-lay out mise.toml (its meaning is unchanged)";
10670
10998
  }
10671
10999
  function readProjectJson(ctx) {
10672
- return tryParseJson(safeReadText2(join11(ctx.repoRoot, ".project.json")));
11000
+ return tryParseJson(safeReadText2(join12(ctx.repoRoot, ".project.json")));
10673
11001
  }
10674
11002
  function readDeclaredAgents(ctx) {
10675
11003
  const project = readProjectJson(ctx);
@@ -10686,7 +11014,7 @@ function readDeclaredAgents(ctx) {
10686
11014
  });
10687
11015
  }
10688
11016
  function readRoleYamlAt(roleDir) {
10689
- const roleYamlPath = join11(roleDir, "role.yaml");
11017
+ const roleYamlPath = join12(roleDir, "role.yaml");
10690
11018
  if (!existsSync8(roleYamlPath)) return null;
10691
11019
  const text = readText(roleYamlPath);
10692
11020
  return {
@@ -10698,7 +11026,7 @@ function readRoleYamlAt(roleDir) {
10698
11026
  }
10699
11027
  function declaredRoleIsUnprovisioned(repoRoot, roleDir) {
10700
11028
  if (!roleDir) return true;
10701
- return !existsSync8(join11(resolve6(repoRoot, roleDir), "role.yaml"));
11029
+ return !existsSync8(join12(resolve6(repoRoot, roleDir), "role.yaml"));
10702
11030
  }
10703
11031
  function validateDeclaredAgent(ctx, declared) {
10704
11032
  const details = [];
@@ -10723,11 +11051,11 @@ function validateDeclaredAgent(ctx, declared) {
10723
11051
  details.push(`agents.${declared.agentId} should map to agent_id ${roleYaml.agentId}`);
10724
11052
  }
10725
11053
  if (roleYaml.providerName) {
10726
- const dispatcher = join11(roleDir, ".scripts", "lib", "ticket-provider.sh");
11054
+ const dispatcher = join12(roleDir, ".scripts", "lib", "ticket-provider.sh");
10727
11055
  if (!existsSync8(dispatcher)) {
10728
11056
  details.push(`agents.${declared.agentId} provider dispatcher ${relative9(ctx.repoRoot, dispatcher)} missing`);
10729
11057
  }
10730
- const provider = join11(roleDir, ".scripts", "providers", `${roleYaml.providerName}.sh`);
11058
+ const provider = join12(roleDir, ".scripts", "providers", `${roleYaml.providerName}.sh`);
10731
11059
  if (!existsSync8(provider)) {
10732
11060
  details.push(`agents.${declared.agentId} provider script ${relative9(ctx.repoRoot, provider)} missing`);
10733
11061
  }
@@ -10743,7 +11071,7 @@ function repoPathNamesRoot(value, repoRoot, homeDir) {
10743
11071
  function canonicalProjectJson(ctx) {
10744
11072
  const roles = discoverRoles(ctx.repoRoot);
10745
11073
  const existing = readProjectJson(ctx) ?? {};
10746
- const slug = normalizeProjectId(existing.project_id ?? existing.project_slug ?? slugifyRepoName(basename4(ctx.repoRoot)));
11074
+ const slug = normalizeProjectId(existing.project_id ?? existing.project_slug ?? slugifyRepoName(basename5(ctx.repoRoot)));
10747
11075
  const firstRole = roles[0];
10748
11076
  const ticketProvider = {
10749
11077
  ...existing.ticket_provider,
@@ -10813,8 +11141,8 @@ function canonicalProjectJson(ctx) {
10813
11141
  };
10814
11142
  }
10815
11143
  function projectJsonFinding(ctx) {
10816
- const projectPath = join11(ctx.repoRoot, ".project.json");
10817
- const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
11144
+ const projectPath = join12(ctx.repoRoot, ".project.json");
11145
+ const planeJsonPath = join12(ctx.repoRoot, ".plane.json");
10818
11146
  const details = [];
10819
11147
  const data = readProjectJson(ctx);
10820
11148
  const roles = discoverRoles(ctx.repoRoot);
@@ -10885,7 +11213,7 @@ var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
10885
11213
  var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
10886
11214
  var BMAD_INSTALL_TOOLS = SUPPORTED_BMAD_TOOLS;
10887
11215
  function manifestBmadModules(repoRoot) {
10888
- const manifestPath = join11(repoRoot, "_bmad", "_config", "manifest.yaml");
11216
+ const manifestPath = join12(repoRoot, "_bmad", "_config", "manifest.yaml");
10889
11217
  const raw = safeReadText2(manifestPath);
10890
11218
  if (raw === null) return { status: "absent" };
10891
11219
  try {
@@ -10913,7 +11241,7 @@ function manifestBmadModules(repoRoot) {
10913
11241
  }
10914
11242
  }
10915
11243
  function configuredBmadModuleSelection(repoRoot) {
10916
- const raw = safeReadText2(join11(repoRoot, "_bmad", "config.toml"));
11244
+ const raw = safeReadText2(join12(repoRoot, "_bmad", "config.toml"));
10917
11245
  if (raw === null) return void 0;
10918
11246
  const modules = [...raw.matchAll(/^\[modules\.([A-Za-z0-9][A-Za-z0-9_-]*)\]\s*$/gm)].map((match) => match[1]);
10919
11247
  return { modules: Array.from(new Set(modules)).filter((name) => name !== "g33"), hasG33: modules.includes("g33") };
@@ -10924,24 +11252,60 @@ function selectedBmadModules(repoRoot) {
10924
11252
  if (manifest.status === "invalid") throw new Error(manifest.error);
10925
11253
  return configuredBmadModuleSelection(repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
10926
11254
  }
10927
- function requiredBmadSentinels(repoRoot, modules = selectedBmadModules(repoRoot)) {
10928
- return [
10929
- join11("core", "config.yaml"),
10930
- join11("config.toml"),
10931
- join11("_config", "manifest.yaml"),
10932
- ...modules.map((module) => join11(module, "config.yaml"))
10933
- ];
11255
+ var BMAD_RUNTIME_SCRIPT_SENTINELS = ["resolve_config.py", "config_utils.py"];
11256
+ function consolidatedBmadConfig(repoRoot) {
11257
+ const path = join12(repoRoot, "_bmad", "config.yaml");
11258
+ if (!existsSync8(path)) return { status: "absent" };
11259
+ let parsed;
11260
+ try {
11261
+ parsed = YAML4.parse(readText(path));
11262
+ } catch (error) {
11263
+ return { status: "unusable", reason: `could not be parsed: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
11264
+ }
11265
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
11266
+ return { status: "unusable", reason: "is not a YAML mapping" };
11267
+ }
11268
+ return { status: "mapping", sections: new Set(Object.keys(parsed)) };
11269
+ }
11270
+ function bmadSentinelGaps(repoRoot, modules = selectedBmadModules(repoRoot)) {
11271
+ const bmad = join12(repoRoot, "_bmad");
11272
+ const gaps = [];
11273
+ const absent = (...segments) => !existsSync8(join12(bmad, ...segments));
11274
+ if (absent("config.toml")) gaps.push({ path: "config.toml", detail: "_bmad/config.toml" });
11275
+ for (const script of BMAD_RUNTIME_SCRIPT_SENTINELS) {
11276
+ if (absent("scripts", script)) gaps.push({ path: join12("scripts", script), detail: `_bmad/scripts/${script}` });
11277
+ }
11278
+ if (absent("_config", "manifest.yaml")) {
11279
+ gaps.push({
11280
+ path: join12("_config", "manifest.yaml"),
11281
+ detail: existsSync8(bmad) ? "_bmad/_config/manifest.yaml (the installer's install record). BMB setup's `--also-remove _config` cleanup is the likely cause: it deletes all of _bmad/_config. Restore it with: pj migrate bmad.scaffold" : "_bmad/_config/manifest.yaml"
11282
+ });
11283
+ }
11284
+ let consolidated;
11285
+ for (const module of modules) {
11286
+ if (!absent(module, "config.yaml")) continue;
11287
+ consolidated ??= consolidatedBmadConfig(repoRoot);
11288
+ if (consolidated.status === "mapping" && consolidated.sections.has(module)) continue;
11289
+ const why = consolidated.status === "absent" ? "" : consolidated.status === "unusable" ? ` (and _bmad/config.yaml ${consolidated.reason}, so it cannot supply a ${module} section)` : ` (and _bmad/config.yaml has no ${module} section)`;
11290
+ gaps.push({ path: join12(module, "config.yaml"), detail: `_bmad/${module}/config.yaml${why}` });
11291
+ }
11292
+ return gaps;
11293
+ }
11294
+ function bmadScaffoldIssues(repoRoot, modules) {
11295
+ const gaps = bmadSentinelGaps(repoRoot, modules);
11296
+ const nameIssues = bmadProjectNameIssues(repoRoot);
11297
+ return { gaps, nameIssues, details: [...gaps.map((gap) => gap.detail), ...nameIssues.details] };
10934
11298
  }
10935
11299
  function canonicalBmadProjectName(repoRoot) {
10936
11300
  const project = readProjectJson({ repoRoot });
10937
11301
  const declared = typeof project?.project_name === "string" ? project.project_name.trim() : "";
10938
- return declared || basename4(repoRoot);
11302
+ return declared || basename5(repoRoot);
10939
11303
  }
10940
11304
  function bmadProjectNameIssues(repoRoot) {
10941
11305
  const expected = canonicalBmadProjectName(repoRoot);
10942
11306
  const paths = [];
10943
11307
  const details = [];
10944
- const configToml = join11(repoRoot, "_bmad", "config.toml");
11308
+ const configToml = join12(repoRoot, "_bmad", "config.toml");
10945
11309
  const tomlText = safeReadText2(configToml);
10946
11310
  const tomlMatch = tomlText?.match(/^project_name\s*=\s*"((?:\\.|[^"\\])*)"\s*$/m);
10947
11311
  let tomlName;
@@ -10956,16 +11320,16 @@ function bmadProjectNameIssues(repoRoot) {
10956
11320
  paths.push(configToml);
10957
11321
  details.push(`_bmad/config.toml project_name must be ${JSON.stringify(expected)}`);
10958
11322
  }
10959
- const bmadRoot = join11(repoRoot, "_bmad");
11323
+ const bmadRoot = join12(repoRoot, "_bmad");
10960
11324
  if (existsSync8(bmadRoot)) {
10961
11325
  let declared;
10962
11326
  try {
10963
11327
  declared = selectedBmadModules(repoRoot);
10964
11328
  } catch {
10965
- declared = readdirSync7(bmadRoot);
11329
+ declared = readdirSync8(bmadRoot);
10966
11330
  }
10967
11331
  for (const name of new Set(declared)) {
10968
- const configPath = join11(bmadRoot, name, "config.yaml");
11332
+ const configPath = join12(bmadRoot, name, "config.yaml");
10969
11333
  const raw = safeReadText2(configPath);
10970
11334
  if (raw === null) continue;
10971
11335
  let actual;
@@ -10985,7 +11349,7 @@ function bmadProjectNameIssues(repoRoot) {
10985
11349
  function isCurrentActivation(path, activations) {
10986
11350
  if (activations.owned.has(path)) return true;
10987
11351
  try {
10988
- if (activations.owned.has(join11(realpathSync4(dirname8(path)), basename4(path)))) return true;
11352
+ if (activations.owned.has(join12(realpathSync4(dirname8(path)), basename5(path)))) return true;
10989
11353
  return activations.targets.has(realpathSync4(path));
10990
11354
  } catch {
10991
11355
  return false;
@@ -10998,26 +11362,26 @@ async function skillActivations(ctx) {
10998
11362
  function evictLegacyBmadPackState(ctx, changedFiles, activations) {
10999
11363
  const details = [];
11000
11364
  const skillDirs = [
11001
- join11(ctx.repoRoot, ".agents", "skills"),
11002
- ...SUPPORTED_CLI_ROOTS.map((root) => join11(ctx.repoRoot, root, "skills"))
11365
+ join12(ctx.repoRoot, ".agents", "skills"),
11366
+ ...SUPPORTED_CLI_ROOTS.map((root) => join12(ctx.repoRoot, root, "skills"))
11003
11367
  ];
11004
11368
  for (const dir of skillDirs) {
11005
11369
  const dirStat = lstatIfPresent(dir);
11006
11370
  if (!dirStat || dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
11007
11371
  let names;
11008
11372
  try {
11009
- names = readdirSync7(dir);
11373
+ names = readdirSync8(dir);
11010
11374
  } catch {
11011
11375
  continue;
11012
11376
  }
11013
11377
  for (const name of names) {
11014
11378
  if (!name.startsWith(BMAD_SKILL_NAME_PREFIX)) continue;
11015
- const path = join11(dir, name);
11379
+ const path = join12(dir, name);
11016
11380
  if (!lstatIfPresent(path)?.isSymbolicLink()) continue;
11017
11381
  if (isCurrentActivation(path, activations)) continue;
11018
11382
  changedFiles.push(path);
11019
11383
  details.push(`removed retired BMAD pack symlink ${relative9(ctx.repoRoot, path)}`);
11020
- if (!ctx.dryRun) unlinkSync5(path);
11384
+ if (!ctx.dryRun) unlinkSync6(path);
11021
11385
  }
11022
11386
  }
11023
11387
  return details;
@@ -11030,10 +11394,11 @@ function bmadInstallerInvocation(version = BMAD_INSTALLER_VERSION) {
11030
11394
  prefixArgs: ["-y", `${BMAD_NPM_PACKAGE}@${version}`]
11031
11395
  };
11032
11396
  }
11033
- function resolveBmadInstallerPackage(version = BMAD_INSTALLER_VERSION) {
11397
+ function resolveBmadInstallerPackage(version = BMAD_INSTALLER_VERSION, options = {}) {
11398
+ const timeoutMs = options.timeoutMs ?? 18e4;
11034
11399
  const packageAt = (dir) => {
11035
11400
  try {
11036
- const parsed = JSON.parse(readFileSync10(join11(dir, "package.json"), "utf8"));
11401
+ const parsed = JSON.parse(readFileSync11(join12(dir, "package.json"), "utf8"));
11037
11402
  return parsed.name === BMAD_NPM_PACKAGE ? typeof parsed.version === "string" ? parsed.version : "unknown" : void 0;
11038
11403
  } catch {
11039
11404
  return void 0;
@@ -11065,12 +11430,13 @@ function resolveBmadInstallerPackage(version = BMAD_INSTALLER_VERSION) {
11065
11430
  `node -e "process.stdout.write(require('fs').realpathSync(process.argv[1]))" "$(command -v ${BMAD_NPM_PACKAGE})"`
11066
11431
  ], {
11067
11432
  encoding: "utf8",
11068
- timeout: 18e4,
11433
+ timeout: timeoutMs,
11069
11434
  env: { ...process.env, npm_config_update_notifier: "false", npm_config_fund: "false" }
11070
11435
  });
11071
11436
  const entry = probe.stdout?.trim().split("\n").pop()?.trim();
11072
11437
  if (probe.status !== 0 || !entry) {
11073
- const detail = String(probe.stderr || probe.error?.message || `exit ${probe.status}`).split("\n").filter((line) => line.trim() && !line.startsWith("npm notice")).slice(-3).join(" ");
11438
+ const timedOut = probe.error?.code === "ETIMEDOUT";
11439
+ const detail = timedOut ? `timed out after ${Math.round(timeoutMs / 1e3)}s` : String(probe.stderr || probe.error?.message || `exit ${probe.status}`).split("\n").filter((line) => line.trim() && !line.startsWith("npm notice")).slice(-3).join(" ");
11074
11440
  return { reason: `could not resolve ${BMAD_NPM_PACKAGE}@${version}: ${detail || "no installer path reported"}` };
11075
11441
  }
11076
11442
  const root = dirname8(dirname8(dirname8(entry)));
@@ -11078,6 +11444,34 @@ function resolveBmadInstallerPackage(version = BMAD_INSTALLER_VERSION) {
11078
11444
  if (!found) return { reason: `${entry} is not inside a ${BMAD_NPM_PACKAGE} package` };
11079
11445
  return { root, entry, version: found };
11080
11446
  }
11447
+ var BMAD_SETUP_SCRIPTS_PROBE_TIMEOUT_MS = 1e4;
11448
+ var BMAD_SETUP_SCRIPTS_REMEDY = "re-run the BMAD installer (npx bmad-method install) or upgrade via `pj migrate bmad.version`";
11449
+ function bmadSetupScriptsState(repoRoot) {
11450
+ const inspection = inspectBmadSetupScripts(repoRoot, BMAD_INSTALLER_VERSION, () => readInstalledBmadVersion(repoRoot));
11451
+ const decline = (why, missing, remedy2) => ({
11452
+ kind: "decline",
11453
+ summary: `Setup script recovery unavailable: ${why}${missing.length ? ` (missing from _bmad/scripts: ${missing.join(", ")})` : ""}; ${remedy2}`
11454
+ });
11455
+ switch (inspection.state) {
11456
+ case "present":
11457
+ return { kind: "present" };
11458
+ case "unsafe":
11459
+ return { kind: "refuse", error: `BMAD script recovery refused: ${inspection.error}` };
11460
+ case "decline":
11461
+ return decline(inspection.why, inspection.missing, BMAD_SETUP_SCRIPTS_REMEDY);
11462
+ case "missing":
11463
+ break;
11464
+ }
11465
+ const override = Boolean(process.env.PJ_BMAD_INSTALLER?.trim());
11466
+ const remedy = override ? "unset PJ_BMAD_INSTALLER to enable recovery from the pinned package" : `retry when npm is reachable, or ${BMAD_SETUP_SCRIPTS_REMEDY}`;
11467
+ const pkg = resolveBmadInstallerPackage(BMAD_INSTALLER_VERSION, { timeoutMs: BMAD_SETUP_SCRIPTS_PROBE_TIMEOUT_MS });
11468
+ if (!pkg.root) return decline(pkg.reason ?? "installer is not a bmad-method package", inspection.missing, remedy);
11469
+ const plan = planBmadSetupScripts(repoRoot, pkg, BMAD_INSTALLER_VERSION);
11470
+ if (plan.error) {
11471
+ return plan.declinable ? decline(plan.error.replace(/^BMAD script recovery refused: /, ""), inspection.missing, plan.declinable === "attest" ? BMAD_SETUP_SCRIPTS_REMEDY : remedy) : { kind: "refuse", error: plan.error };
11472
+ }
11473
+ return plan.files.length ? { kind: "missing", pkg, files: plan.files } : { kind: "present" };
11474
+ }
11081
11475
  function bmadInstallerArgs(repoRoot, modules = selectedBmadModules(repoRoot), extraArgs = []) {
11082
11476
  const installerModules = modules.length ? modules.join(",") : "core";
11083
11477
  return [
@@ -11096,7 +11490,7 @@ function bmadInstallerArgs(repoRoot, modules = selectedBmadModules(repoRoot), ex
11096
11490
  }
11097
11491
  function bmadUpgradeChannelArgs(repoRoot, version) {
11098
11492
  if (!/^\d+\.\d+\.\d+$/.test(version)) return [];
11099
- const raw = safeReadText2(join11(repoRoot, "_bmad", "_config", "manifest.yaml"));
11493
+ const raw = safeReadText2(join12(repoRoot, "_bmad", "_config", "manifest.yaml"));
11100
11494
  if (!raw) return [];
11101
11495
  let modules;
11102
11496
  try {
@@ -11166,7 +11560,7 @@ function bmadChannelTarget(distTags) {
11166
11560
  return next ? { version: next, channel: BMAD_TARGET_CHANNEL } : void 0;
11167
11561
  }
11168
11562
  function readInstalledBmadVersion(repoRoot) {
11169
- const raw = safeReadText2(join11(repoRoot, "_bmad", "_config", "manifest.yaml"));
11563
+ const raw = safeReadText2(join12(repoRoot, "_bmad", "_config", "manifest.yaml"));
11170
11564
  if (!raw) return void 0;
11171
11565
  try {
11172
11566
  const parsed = YAML4.parse(raw);
@@ -11177,8 +11571,8 @@ function readInstalledBmadVersion(repoRoot) {
11177
11571
  }
11178
11572
  }
11179
11573
  function bmadCachePath(homeDir) {
11180
- const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join11(homeDir, ".cache");
11181
- return join11(cacheRoot, "pjangler", "bmad-dist-tags.json");
11574
+ const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join12(homeDir, ".cache");
11575
+ return join12(cacheRoot, "pjangler", "bmad-dist-tags.json");
11182
11576
  }
11183
11577
  function readBmadDistTagsCache(homeDir) {
11184
11578
  const raw = safeReadText2(bmadCachePath(homeDir));
@@ -11220,8 +11614,8 @@ function resolveBmadDistTags(homeDir) {
11220
11614
  if (fetched) {
11221
11615
  try {
11222
11616
  const path = bmadCachePath(homeDir);
11223
- mkdirSync6(dirname8(path), { recursive: true });
11224
- writeFileSync8(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
11617
+ mkdirSync7(dirname8(path), { recursive: true });
11618
+ writeFileSync9(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
11225
11619
  } catch {
11226
11620
  }
11227
11621
  return { distTags: fetched, stale: false };
@@ -11345,8 +11739,8 @@ var UNSUPPORTED_BMAD_ROOTS = {
11345
11739
  ".zencoder": "zencoder"
11346
11740
  };
11347
11741
  function unsupportedRootAttestation(repoRoot, rootName) {
11348
- const root = join11(repoRoot, rootName);
11349
- const stat = lstatSync9(root);
11742
+ const root = join12(repoRoot, rootName);
11743
+ const stat = lstatSync10(root);
11350
11744
  if (!stat.isDirectory() || stat.isSymbolicLink()) return { safe: false, reason: `${rootName} is not a regular generated directory` };
11351
11745
  const installerTool = UNSUPPORTED_BMAD_ROOTS[rootName];
11352
11746
  if (!installedBmadTools(repoRoot).has(installerTool)) {
@@ -11365,7 +11759,7 @@ var OPTIONAL_TEMPLATE_SCRIPTS = [
11365
11759
  function optionalTemplateScriptIssues(ctx) {
11366
11760
  const issues = [];
11367
11761
  for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
11368
- const path = join11(ctx.repoRoot, rel);
11762
+ const path = join12(ctx.repoRoot, rel);
11369
11763
  if (!existsSync8(path)) continue;
11370
11764
  const expected = templateCommonProjectText(ctx, rel);
11371
11765
  if (expected === void 0) continue;
@@ -11376,7 +11770,7 @@ function optionalTemplateScriptIssues(ctx) {
11376
11770
  function refreshOptionalTemplateScripts(ctx) {
11377
11771
  const changed = [];
11378
11772
  for (const rel of OPTIONAL_TEMPLATE_SCRIPTS) {
11379
- const path = join11(ctx.repoRoot, rel);
11773
+ const path = join12(ctx.repoRoot, rel);
11380
11774
  if (!existsSync8(path)) continue;
11381
11775
  const expected = templateCommonProjectText(ctx, rel);
11382
11776
  if (expected === void 0 || safeReadText2(path) === expected) continue;
@@ -11394,13 +11788,13 @@ function createMiseChecks() {
11394
11788
  id: "mise.config-root",
11395
11789
  title: "mise config_root + AGENTS link hooks",
11396
11790
  audit: (ctx) => {
11397
- const misePath = join11(ctx.repoRoot, "mise.toml");
11791
+ const misePath = join12(ctx.repoRoot, "mise.toml");
11398
11792
  if (!existsSync8(misePath)) {
11399
11793
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
11400
11794
  }
11401
11795
  const text = readText(misePath);
11402
11796
  const details = [];
11403
- const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
11797
+ const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
11404
11798
  if (!existsSync8(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
11405
11799
  const toml = [];
11406
11800
  const manual = [];
@@ -11448,7 +11842,7 @@ function createMiseChecks() {
11448
11842
  };
11449
11843
  },
11450
11844
  migrate: (ctx, finding2) => {
11451
- const path = join11(ctx.repoRoot, "mise.toml");
11845
+ const path = join12(ctx.repoRoot, "mise.toml");
11452
11846
  const changedFiles = [];
11453
11847
  const details = [];
11454
11848
  if (!existsSync8(path)) {
@@ -11467,7 +11861,7 @@ function createMiseChecks() {
11467
11861
  if (!ctx.dryRun) writeText(path, plan.text);
11468
11862
  text = plan.text;
11469
11863
  }
11470
- const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
11864
+ const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
11471
11865
  const expectedScript = templateLinkAgentfilesScript(ctx);
11472
11866
  if (expectedScript === void 0) {
11473
11867
  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: [] };
@@ -11505,9 +11899,9 @@ function createMiseChecks() {
11505
11899
  title: "managed mise versioning block",
11506
11900
  audit: (ctx) => {
11507
11901
  const details = [];
11508
- const misePath = join11(ctx.repoRoot, "mise.toml");
11509
- const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
11510
- const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
11902
+ const misePath = join12(ctx.repoRoot, "mise.toml");
11903
+ const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
11904
+ const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
11511
11905
  const text = safeReadText2(misePath);
11512
11906
  if (!text?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
11513
11907
  if (!existsSync8(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
@@ -11533,7 +11927,7 @@ function createMiseChecks() {
11533
11927
  migrate: (ctx, finding2) => {
11534
11928
  const changedFiles = [];
11535
11929
  const details = [];
11536
- const misePath = join11(ctx.repoRoot, "mise.toml");
11930
+ const misePath = join12(ctx.repoRoot, "mise.toml");
11537
11931
  if (!existsSync8(misePath)) {
11538
11932
  if (ensureMiseTomlFromTemplate(ctx, changedFiles) === null) {
11539
11933
  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: [] };
@@ -11551,7 +11945,7 @@ function createMiseChecks() {
11551
11945
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
11552
11946
  if (!ctx.dryRun) writeText(misePath, versioningPlan.text);
11553
11947
  }
11554
- const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
11948
+ const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
11555
11949
  const expectedScript = templateVersioningScript(ctx);
11556
11950
  if (expectedScript === void 0) {
11557
11951
  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: [] };
@@ -11563,7 +11957,7 @@ function createMiseChecks() {
11563
11957
  chmodSync3(versioningPath, 493);
11564
11958
  }
11565
11959
  }
11566
- const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
11960
+ const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
11567
11961
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
11568
11962
  if (safeReadText2(manifestPath) !== expectedManifest) {
11569
11963
  changedFiles.push(manifestPath);
@@ -11625,7 +12019,7 @@ var RETIRED_SKILL_SCRIPTS = [
11625
12019
  ".mise/scripts/unlink-project-skills-from-clis.sh"
11626
12020
  ];
11627
12021
  function retiredSkillsScripts(ctx) {
11628
- return RETIRED_SKILL_SCRIPTS.map((path) => join11(ctx.repoRoot, path)).filter((path) => Boolean(lstatIfPresent(path)));
12022
+ return RETIRED_SKILL_SCRIPTS.map((path) => join12(ctx.repoRoot, path)).filter((path) => Boolean(lstatIfPresent(path)));
11629
12023
  }
11630
12024
  function skillsWiringFindings(text) {
11631
12025
  if (text === null) return { fixable: [], manual: [] };
@@ -11676,7 +12070,7 @@ function createAgentHooksChecks() {
11676
12070
  title: "Skillex project skills",
11677
12071
  audit: async (ctx) => {
11678
12072
  const finding2 = await auditProjectSkills(ctx);
11679
- const mise = safeReadText2(join11(ctx.repoRoot, "mise.toml"));
12073
+ const mise = safeReadText2(join12(ctx.repoRoot, "mise.toml"));
11680
12074
  const registryField = await legacyRegistryField(ctx);
11681
12075
  const registryDrop = registryField ? await dropLegacyRegistryField({ ...ctx, dryRun: true }) : void 0;
11682
12076
  const wiring = [...skillsWiringIssues(mise), ...registryField && !registryDrop?.blocked ? [`Drop the legacy "registry" field (${JSON.stringify(registryField.value)}) from .agents/skills.json; it pins resolution to the never-fetched ~/.agents/.cache registry clone`] : []];
@@ -11697,7 +12091,7 @@ function createAgentHooksChecks() {
11697
12091
  const changedFiles = [];
11698
12092
  const details = [];
11699
12093
  const result2 = (status, summary) => ({ id: finding2.id, title: finding2.title, status, summary, changedFiles: [...new Set(changedFiles)], details });
11700
- let currentMise = safeReadText2(join11(ctx.repoRoot, "mise.toml"));
12094
+ let currentMise = safeReadText2(join12(ctx.repoRoot, "mise.toml"));
11701
12095
  if (currentMise === null) {
11702
12096
  const template = templateCommonProjectText(ctx, "mise.toml.jinja");
11703
12097
  if (template === void 0) return result2("blocked", "Generated-project mise template is unavailable");
@@ -11712,9 +12106,9 @@ function createAgentHooksChecks() {
11712
12106
  const stat = lstatIfPresent(path);
11713
12107
  if (!stat || stat.isDirectory()) continue;
11714
12108
  changedFiles.push(path);
11715
- if (!ctx.dryRun) unlinkSync5(path);
12109
+ if (!ctx.dryRun) unlinkSync6(path);
11716
12110
  }
11717
- const misePath = join11(ctx.repoRoot, "mise.toml");
12111
+ const misePath = join12(ctx.repoRoot, "mise.toml");
11718
12112
  const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
11719
12113
  if (safeReadText2(misePath) !== nextMise) {
11720
12114
  changedFiles.push(misePath);
@@ -11745,9 +12139,9 @@ function createAgentHooksChecks() {
11745
12139
  id: "sot.agent-symlinks",
11746
12140
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
11747
12141
  audit: (ctx) => {
11748
- const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
12142
+ const agentsPath = join12(ctx.repoRoot, "AGENTS.md");
11749
12143
  if (!existsSync8(agentsPath)) {
11750
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync8(join11(ctx.repoRoot, file)));
12144
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync8(join12(ctx.repoRoot, file)));
11751
12145
  if (fallbackSources.length === 0) {
11752
12146
  return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
11753
12147
  }
@@ -11762,7 +12156,7 @@ function createAgentHooksChecks() {
11762
12156
  }
11763
12157
  const details = [];
11764
12158
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
11765
- const full = join11(ctx.repoRoot, file);
12159
+ const full = join12(ctx.repoRoot, file);
11766
12160
  const target = readSymlinkTarget(full);
11767
12161
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
11768
12162
  }
@@ -11786,7 +12180,7 @@ function createAgentHooksChecks() {
11786
12180
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
11787
12181
  }
11788
12182
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
11789
- const full = join11(ctx.repoRoot, file);
12183
+ const full = join12(ctx.repoRoot, file);
11790
12184
  const result2 = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
11791
12185
  if (result2.blocked) blockedDetails.push(result2.blocked);
11792
12186
  if (result2.changed) changedFiles.push(full);
@@ -11813,7 +12207,7 @@ function createProjectJsonChecks() {
11813
12207
  const changedFiles = [];
11814
12208
  const blockedDetails = [];
11815
12209
  const droppedDetails = [];
11816
- const path = join11(ctx.repoRoot, ".project.json");
12210
+ const path = join12(ctx.repoRoot, ".project.json");
11817
12211
  const existing = readProjectJson(ctx) ?? {};
11818
12212
  const canonical = canonicalProjectJson(ctx);
11819
12213
  const preservedDetails = [];
@@ -11845,7 +12239,7 @@ function createProjectJsonChecks() {
11845
12239
  changedFiles.push(path);
11846
12240
  if (!ctx.dryRun) writeText(path, expected);
11847
12241
  }
11848
- const planeJson = join11(ctx.repoRoot, ".plane.json");
12242
+ const planeJson = join12(ctx.repoRoot, ".plane.json");
11849
12243
  if (existsSync8(planeJson)) {
11850
12244
  const backup = `${planeJson}.migrated-backup`;
11851
12245
  if (existsSync8(backup)) {
@@ -11877,10 +12271,10 @@ function createMiseOpInjectChecks() {
11877
12271
  const details = [];
11878
12272
  let envOpNeedsHands = false;
11879
12273
  const optedOut = envMaterializationOptedOut(ctx);
11880
- const envOpPath = join11(ctx.repoRoot, ".env.op");
12274
+ const envOpPath = join12(ctx.repoRoot, ".env.op");
11881
12275
  const envOpExists = existsSync8(envOpPath);
11882
12276
  const envOp = envOpExists ? readText(envOpPath) : void 0;
11883
- const gitignore = safeReadText2(join11(ctx.repoRoot, ".gitignore"));
12277
+ const gitignore = safeReadText2(join12(ctx.repoRoot, ".gitignore"));
11884
12278
  if (!envOpExists) {
11885
12279
  details.push(".env.op missing");
11886
12280
  } else if (!envOp?.trim()) {
@@ -11900,7 +12294,7 @@ function createMiseOpInjectChecks() {
11900
12294
  if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
11901
12295
  if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
11902
12296
  if (!gitignore?.includes("!.env.op")) details.push(".gitignore should unignore .env.op");
11903
- const misePath = join11(ctx.repoRoot, "mise.toml");
12297
+ const misePath = join12(ctx.repoRoot, "mise.toml");
11904
12298
  const miseText = safeReadText2(misePath);
11905
12299
  if (!miseText) {
11906
12300
  details.push("mise.toml missing for .env materialization hook");
@@ -11923,7 +12317,7 @@ function createMiseOpInjectChecks() {
11923
12317
  else if (plan.text !== miseText && !mise.length) mise.push(describeRewrite(miseText, plan.text));
11924
12318
  details.push(...mise);
11925
12319
  }
11926
- const materializePath = join11(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
12320
+ const materializePath = join12(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
11927
12321
  const expectedMaterializer = templateMaterializeEnvScript(ctx);
11928
12322
  if (optedOut) {
11929
12323
  if (existsSync8(materializePath)) details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is present, but .project.json declares secrets.materialize_env: false`);
@@ -11931,7 +12325,7 @@ function createMiseOpInjectChecks() {
11931
12325
  details.push("pjangler package is missing the managed materialize-env.sh source");
11932
12326
  } else if (safeReadText2(materializePath) !== expectedMaterializer) {
11933
12327
  details.push(`${MATERIALIZE_ENV_SCRIPT_REL} missing or drifted`);
11934
- } else if ((lstatSync9(materializePath).mode & 73) === 0) {
12328
+ } else if ((lstatSync10(materializePath).mode & 73) === 0) {
11935
12329
  details.push(`${MATERIALIZE_ENV_SCRIPT_REL} is not executable`);
11936
12330
  }
11937
12331
  return {
@@ -11947,8 +12341,8 @@ function createMiseOpInjectChecks() {
11947
12341
  const changedFiles = [];
11948
12342
  const details = [];
11949
12343
  let envOpNeedsHands = false;
11950
- const envOpPath = join11(ctx.repoRoot, ".env.op");
11951
- const canonicalEnvOpPath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op");
12344
+ const envOpPath = join12(ctx.repoRoot, ".env.op");
12345
+ const canonicalEnvOpPath = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op");
11952
12346
  if (!existsSync8(canonicalEnvOpPath)) {
11953
12347
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing the neutral .env.op template", changedFiles: [], details: [] };
11954
12348
  }
@@ -11977,7 +12371,7 @@ function createMiseOpInjectChecks() {
11977
12371
  }
11978
12372
  }
11979
12373
  }
11980
- const gitignorePath = join11(ctx.repoRoot, ".gitignore");
12374
+ const gitignorePath = join12(ctx.repoRoot, ".gitignore");
11981
12375
  const gitignore = safeReadText2(gitignorePath) ?? "";
11982
12376
  const requiredBlock = `# Secrets \u2014 .env is materialized from .env.op by \`op inject\` on mise enter,
11983
12377
  # staged through a mktemp file and moved into place only on success.
@@ -11990,7 +12384,7 @@ function createMiseOpInjectChecks() {
11990
12384
  changedFiles.push(gitignorePath);
11991
12385
  if (!ctx.dryRun) writeText(gitignorePath, `${gitignore.replace(/\s*$/, "")}${gitignore.trim() ? "\n\n" : ""}${requiredBlock}`);
11992
12386
  }
11993
- const misePath = join11(ctx.repoRoot, "mise.toml");
12387
+ const misePath = join12(ctx.repoRoot, "mise.toml");
11994
12388
  let currentMise = safeReadText2(misePath);
11995
12389
  if (currentMise === null) {
11996
12390
  const initialized = ensureMiseTomlFromTemplate(ctx, changedFiles);
@@ -12007,7 +12401,7 @@ function createMiseOpInjectChecks() {
12007
12401
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
12008
12402
  if (!ctx.dryRun) writeText(misePath, opPlan.text);
12009
12403
  }
12010
- const materializePath = join11(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
12404
+ const materializePath = join12(ctx.repoRoot, MATERIALIZE_ENV_SCRIPT_REL);
12011
12405
  const expectedMaterializer = templateMaterializeEnvScript(ctx);
12012
12406
  if (optedOut) {
12013
12407
  if (existsSync8(materializePath)) {
@@ -12016,7 +12410,7 @@ function createMiseOpInjectChecks() {
12016
12410
  }
12017
12411
  } else if (!expectedMaterializer) {
12018
12412
  return { id: finding2.id, title: finding2.title, status: "blocked", summary: "pjangler package is missing materialize-env.sh", changedFiles: [], details: [] };
12019
- } else if (safeReadText2(materializePath) !== expectedMaterializer || existsSync8(materializePath) && (lstatSync9(materializePath).mode & 73) === 0) {
12413
+ } else if (safeReadText2(materializePath) !== expectedMaterializer || existsSync8(materializePath) && (lstatSync10(materializePath).mode & 73) === 0) {
12020
12414
  changedFiles.push(materializePath);
12021
12415
  if (!ctx.dryRun) {
12022
12416
  writeText(materializePath, expectedMaterializer);
@@ -12078,7 +12472,7 @@ function createProjectProvenanceChecks() {
12078
12472
  title: ".copier-answers.yml provenance + drift report",
12079
12473
  audit: (ctx) => {
12080
12474
  const details = [];
12081
- const path = join11(ctx.repoRoot, ".copier-answers.yml");
12475
+ const path = join12(ctx.repoRoot, ".copier-answers.yml");
12082
12476
  const text = safeReadText2(path);
12083
12477
  const project = readProjectJson(ctx);
12084
12478
  if (!text) {
@@ -12106,11 +12500,11 @@ function createProjectProvenanceChecks() {
12106
12500
  migrate: (ctx, finding2) => {
12107
12501
  const changedFiles = [];
12108
12502
  const project = canonicalProjectJson(ctx);
12109
- const path = join11(ctx.repoRoot, ".copier-answers.yml");
12503
+ const path = join12(ctx.repoRoot, ".copier-answers.yml");
12110
12504
  const current = safeReadText2(path);
12111
12505
  const existing = current ? YAML4.parseDocument(current) : void 0;
12112
12506
  const document = existing && !existing.errors.length && YAML4.isMap(existing.contents) ? existing : new YAML4.Document({});
12113
- document.set("_src_path", join11(ctx.pjanglerRoot, "templates", "commonproject"));
12507
+ document.set("_src_path", join12(ctx.pjanglerRoot, "templates", "commonproject"));
12114
12508
  document.set("project_description", String(project.project_description));
12115
12509
  document.set("project_name", String(project.project_name));
12116
12510
  document.set("ticket_provider", String(project.ticket_provider?.type ?? "plane"));
@@ -12135,9 +12529,9 @@ ${body}`;
12135
12529
  }
12136
12530
  function supportedCliProjectionIssues(repoRoot, plan) {
12137
12531
  const issues = [];
12138
- const managedSkills = join11(repoRoot, ".agents", "skills");
12532
+ const managedSkills = join12(repoRoot, ".agents", "skills");
12139
12533
  for (const rootName of SUPPORTED_CLI_ROOTS) {
12140
- const root = join11(repoRoot, rootName);
12534
+ const root = join12(repoRoot, rootName);
12141
12535
  const rootStat = lstatIfPresent(root);
12142
12536
  if (!rootStat) {
12143
12537
  issues.push(`${rootName} missing`);
@@ -12147,7 +12541,7 @@ function supportedCliProjectionIssues(repoRoot, plan) {
12147
12541
  issues.push(`${rootName} must be a real configuration directory`);
12148
12542
  continue;
12149
12543
  }
12150
- const skills = join11(root, "skills");
12544
+ const skills = join12(root, "skills");
12151
12545
  const skillsStat = lstatIfPresent(skills);
12152
12546
  if (!skillsStat) {
12153
12547
  issues.push(`${rootName}/skills missing`);
@@ -12184,9 +12578,9 @@ function supportedCliProjectionIssues(repoRoot, plan) {
12184
12578
  issues.push(`${rootName}/skills is not a directory`);
12185
12579
  continue;
12186
12580
  }
12187
- const hasGeneratedSkill = existsSync8(projectedSkills) && readdirSync7(projectedSkills).some((name) => {
12188
- const skillFile = join11(projectedSkills, name, "SKILL.md");
12189
- return existsSync8(skillFile) && lstatSync9(skillFile).isFile();
12581
+ const hasGeneratedSkill = existsSync8(projectedSkills) && readdirSync8(projectedSkills).some((name) => {
12582
+ const skillFile = join12(projectedSkills, name, "SKILL.md");
12583
+ return existsSync8(skillFile) && lstatSync10(skillFile).isFile();
12190
12584
  });
12191
12585
  if (!hasGeneratedSkill) issues.push(`${rootName}/skills contains no BMAD skill configuration`);
12192
12586
  }
@@ -12212,7 +12606,7 @@ var LEGACY_CLI_GITIGNORE_LINES = /* @__PURE__ */ new Set([
12212
12606
  "# some CLIs get a real directory here and some get a symlink."
12213
12607
  ]);
12214
12608
  function supportedCliGitignoreIssues(repoRoot) {
12215
- const lines = (safeReadText2(join11(repoRoot, ".gitignore")) ?? "").split(/\r?\n/);
12609
+ const lines = (safeReadText2(join12(repoRoot, ".gitignore")) ?? "").split(/\r?\n/);
12216
12610
  const legacy = lines.filter((line) => LEGACY_CLI_GITIGNORE_LINES.has(line.trim()));
12217
12611
  return [
12218
12612
  ...legacy.length ? [`.gitignore contains ${legacy.length} legacy client-root override line(s); only .agents is canonical`] : [],
@@ -12220,7 +12614,7 @@ function supportedCliGitignoreIssues(repoRoot) {
12220
12614
  ];
12221
12615
  }
12222
12616
  function ensureSupportedCliGitignore(ctx) {
12223
- const path = join11(ctx.repoRoot, ".gitignore");
12617
+ const path = join12(ctx.repoRoot, ".gitignore");
12224
12618
  const current = safeReadText2(path) ?? "";
12225
12619
  const kept = current.split(/\r?\n/).filter((line) => {
12226
12620
  const trimmed2 = line.trim();
@@ -12262,6 +12656,24 @@ function withInstallerSkillHandoff(check) {
12262
12656
  }
12263
12657
  };
12264
12658
  }
12659
+ function withScaffoldPostcondition(check) {
12660
+ if (check.id !== "bmad.scaffold") return check;
12661
+ return {
12662
+ ...check,
12663
+ migrate: async (ctx, finding2) => {
12664
+ const result2 = await check.migrate(ctx, finding2);
12665
+ if (ctx.dryRun || result2.status !== "applied" && result2.status !== "noop") return result2;
12666
+ const after = await check.audit(ctx);
12667
+ if (after.status === "pass" || after.status === "skip") return result2;
12668
+ return {
12669
+ ...result2,
12670
+ status: "partial",
12671
+ summary: after.summary,
12672
+ details: [...result2.details, `still failing after the install: ${after.summary}`, ...after.details]
12673
+ };
12674
+ }
12675
+ };
12676
+ }
12265
12677
  function createBmadChecks() {
12266
12678
  return [
12267
12679
  {
@@ -12279,15 +12691,8 @@ function createBmadChecks() {
12279
12691
  fixable: false
12280
12692
  };
12281
12693
  }
12282
- const targetRoot = join11(ctx.repoRoot, "_bmad");
12283
12694
  const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModuleSelection(ctx.repoRoot)?.modules ?? [...DEFAULT_BMAD_MODULES];
12284
- const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
12285
- const missing = sentinels.filter((file) => !existsSync8(join11(targetRoot, file)));
12286
- const projectNameIssues = bmadProjectNameIssues(ctx.repoRoot);
12287
- const details = [
12288
- ...missing.map((file) => `_bmad/${file}`),
12289
- ...projectNameIssues.details
12290
- ];
12695
+ const { details } = bmadScaffoldIssues(ctx.repoRoot, selectedModules);
12291
12696
  return {
12292
12697
  id: "bmad.scaffold",
12293
12698
  title: "BMAD modules/docs scaffold",
@@ -12326,7 +12731,7 @@ function createBmadChecks() {
12326
12731
  bmadCompanion: preservation
12327
12732
  };
12328
12733
  if (ctx.dryRun) {
12329
- const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
12734
+ const issues = bmadScaffoldIssues(ctx.repoRoot, selectedModules);
12330
12735
  const companion = installedBmad ? await runG33PreUpdateCompanion(ctx.repoRoot) : await runG33Companion(ctx.repoRoot, true, "bmad-install");
12331
12736
  if (installedBmad && ["unavailable", "conflict", "error"].includes(companion.status)) {
12332
12737
  return {
@@ -12339,8 +12744,8 @@ function createBmadChecks() {
12339
12744
  bmadCompanion: companion
12340
12745
  };
12341
12746
  }
12342
- changedFiles.push(...sentinels.map((file) => join11(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync8(path)));
12343
- changedFiles.push(...bmadProjectNameIssues(ctx.repoRoot).paths);
12747
+ changedFiles.push(...issues.gaps.map((gap) => join12(ctx.repoRoot, "_bmad", gap.path)));
12748
+ changedFiles.push(...issues.nameIssues.paths);
12344
12749
  const reservedPreview = installedBmad ? evictReservedModuleEntries(ctx.repoRoot, true) : void 0;
12345
12750
  changedFiles.push(...reservedPreview?.changedFiles ?? []);
12346
12751
  return {
@@ -12358,9 +12763,10 @@ function createBmadChecks() {
12358
12763
  };
12359
12764
  }
12360
12765
  if (installedBmad && g33CompanionEnabled(ctx.repoRoot)) {
12766
+ const before2 = bmadScaffoldIssues(ctx.repoRoot, selectedModules);
12361
12767
  const expectedChangedPaths2 = [
12362
- ...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) => join11(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync8(path)),
12363
- ...bmadProjectNameIssues(ctx.repoRoot).paths
12768
+ ...before2.gaps.map((gap) => join12(ctx.repoRoot, "_bmad", gap.path)),
12769
+ ...before2.nameIssues.paths
12364
12770
  ];
12365
12771
  const pkg = resolveBmadInstallerPackage(BMAD_INSTALLER_VERSION);
12366
12772
  const scope = deriveBmadUpdateScope(ctx.repoRoot, { tools: BMAD_INSTALL_TOOLS, installer: pkg, companionSurfaces: G33_OWNED_SURFACES });
@@ -12401,16 +12807,17 @@ function createBmadChecks() {
12401
12807
  bmadCompanion: update.companion
12402
12808
  };
12403
12809
  }
12810
+ const before = bmadScaffoldIssues(ctx.repoRoot, selectedModules);
12404
12811
  const expectedChangedPaths = [
12405
- ...requiredBmadSentinels(ctx.repoRoot, selectedModules).map((file) => join11(ctx.repoRoot, "_bmad", file)).filter((path) => !existsSync8(path)),
12406
- ...bmadProjectNameIssues(ctx.repoRoot).paths
12812
+ ...before.gaps.map((gap) => join12(ctx.repoRoot, "_bmad", gap.path)),
12813
+ ...before.nameIssues.paths
12407
12814
  ];
12408
12815
  const evicted = evictLegacyBmadPackState(ctx, changedFiles, await skillActivations(ctx));
12409
12816
  const reserved = installedBmad ? evictReservedModuleEntries(ctx.repoRoot, false) : void 0;
12410
12817
  if (reserved) evicted.unshift(...reserved.details);
12411
12818
  const install = await runBmadInstall(ctx.repoRoot, selectedModules, BMAD_INSTALLER_VERSION, channelArgs);
12412
12819
  if (!install.ok && reserved?.original !== void 0) {
12413
- writeFileSync8(reserved.changedFiles[0], reserved.original, "utf8");
12820
+ writeFileSync9(reserved.changedFiles[0], reserved.original, "utf8");
12414
12821
  evicted.push("restored _bmad/_config/manifest.yaml after the failed install");
12415
12822
  }
12416
12823
  if (!install.ok) {
@@ -12435,6 +12842,58 @@ function createBmadChecks() {
12435
12842
  };
12436
12843
  }
12437
12844
  },
12845
+ {
12846
+ id: "bmad.setup-scripts",
12847
+ title: "BMAD ignored installer setup scripts",
12848
+ audit: (ctx) => {
12849
+ if (!isInstalledBmadProject(ctx.repoRoot)) return {
12850
+ id: "bmad.setup-scripts",
12851
+ title: "BMAD ignored installer setup scripts",
12852
+ status: "skip",
12853
+ summary: "No installed BMAD project",
12854
+ details: [],
12855
+ fixable: false
12856
+ };
12857
+ const state = bmadSetupScriptsState(ctx.repoRoot);
12858
+ const id = "bmad.setup-scripts", title = "BMAD ignored installer setup scripts";
12859
+ switch (state.kind) {
12860
+ case "present":
12861
+ return { id, title, status: "pass", summary: "BMAD installer scripts are present", details: [], fixable: false };
12862
+ case "decline":
12863
+ return { id, title, status: "skip", summary: state.summary, details: [], fixable: false };
12864
+ case "refuse":
12865
+ return { id, title, status: "fail", summary: state.error, details: [state.error], fixable: false };
12866
+ case "missing":
12867
+ return {
12868
+ id,
12869
+ title,
12870
+ status: "fail",
12871
+ summary: "Ignored BMAD installer scripts are missing",
12872
+ details: state.files.map((path) => relative9(ctx.repoRoot, path)),
12873
+ fixable: true
12874
+ };
12875
+ }
12876
+ },
12877
+ migrate: (ctx, finding2) => {
12878
+ const title = finding2.title, id = finding2.id;
12879
+ if (finding2.status === "skip") return { id, title, status: "skipped", summary: finding2.summary, details: [], changedFiles: [] };
12880
+ const state = bmadSetupScriptsState(ctx.repoRoot);
12881
+ if (state.kind === "decline") return { id, title, status: "skipped", summary: state.summary, details: [], changedFiles: [] };
12882
+ if (state.kind === "refuse") return { id, title, status: "blocked", summary: state.error, details: [state.error], changedFiles: [] };
12883
+ if (state.kind === "present") return { id, title, status: "noop", summary: "BMAD installer scripts already present", details: [], changedFiles: [] };
12884
+ const { pkg, files } = state;
12885
+ if (ctx.dryRun) return {
12886
+ id,
12887
+ title,
12888
+ status: "applied",
12889
+ summary: "Would restore ignored scripts from verified pinned bmad-method package",
12890
+ details: [`bmad-method@${BMAD_INSTALLER_VERSION} (${pkg.root})`],
12891
+ changedFiles: files
12892
+ };
12893
+ const applied = applyBmadSetupScripts(ctx.repoRoot, pkg, BMAD_INSTALLER_VERSION);
12894
+ return applied.error ? { id, title, status: "blocked", summary: applied.error, details: [applied.error], changedFiles: [] } : { id, title, status: "applied", summary: "Restored ignored BMAD installer scripts", details: [`bmad-method@${BMAD_INSTALLER_VERSION} (${pkg.root})`], changedFiles: applied.files };
12895
+ }
12896
+ },
12438
12897
  {
12439
12898
  id: "bmad.version",
12440
12899
  title: "BMAD version currency",
@@ -12445,7 +12904,7 @@ function createBmadChecks() {
12445
12904
  id: "bmad.version",
12446
12905
  title: "BMAD version currency",
12447
12906
  status: "skip",
12448
- summary: existsSync8(join11(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
12907
+ summary: existsSync8(join12(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
12449
12908
  details: [],
12450
12909
  fixable: false
12451
12910
  };
@@ -12533,7 +12992,7 @@ function createBmadChecks() {
12533
12992
  }
12534
12993
  const installed = readInstalledBmadVersion(ctx.repoRoot);
12535
12994
  const available = ctx.bmadVersionPin?.trim() ?? bmadChannelTarget(resolveBmadDistTags(ctx.homeDir)?.distTags)?.version;
12536
- const manifestPath = join11(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
12995
+ const manifestPath = join12(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
12537
12996
  const manifestSelection = manifestBmadModules(ctx.repoRoot);
12538
12997
  if (manifestSelection.status === "invalid") {
12539
12998
  return {
@@ -12631,7 +13090,7 @@ function createBmadChecks() {
12631
13090
  const reserved = evictReservedModuleEntries(ctx.repoRoot, false);
12632
13091
  const install = await runBmadInstall(ctx.repoRoot, selectedModules, available ?? BMAD_TARGET_CHANNEL, channelArgs);
12633
13092
  if (!install.ok) {
12634
- if (reserved.original !== void 0) writeFileSync8(reserved.changedFiles[0], reserved.original, "utf8");
13093
+ if (reserved.original !== void 0) writeFileSync9(reserved.changedFiles[0], reserved.original, "utf8");
12635
13094
  return {
12636
13095
  id: finding2.id,
12637
13096
  title: finding2.title,
@@ -12666,7 +13125,7 @@ function createBmadChecks() {
12666
13125
  title: "Supported BMAD CLI projection roots",
12667
13126
  audit: (ctx) => {
12668
13127
  const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
12669
- const present = unsupportedNames.filter((name) => existsSync8(join11(ctx.repoRoot, name)));
13128
+ const present = unsupportedNames.filter((name) => existsSync8(join12(ctx.repoRoot, name)));
12670
13129
  const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
12671
13130
  const plan = planSkillRoots(ctx.repoRoot);
12672
13131
  const supportedIssues = supportedCliProjectionIssues(ctx.repoRoot, plan);
@@ -12688,7 +13147,7 @@ function createBmadChecks() {
12688
13147
  },
12689
13148
  migrate: (ctx, finding2) => {
12690
13149
  const unsupportedNames = Object.keys(UNSUPPORTED_BMAD_ROOTS);
12691
- const present = unsupportedNames.filter((name) => existsSync8(join11(ctx.repoRoot, name)));
13150
+ const present = unsupportedNames.filter((name) => existsSync8(join12(ctx.repoRoot, name)));
12692
13151
  const attestations = present.map((name) => ({ name, ...unsupportedRootAttestation(ctx.repoRoot, name) }));
12693
13152
  const blocked = attestations.filter((entry) => !entry.safe);
12694
13153
  if (blocked.length) {
@@ -12713,7 +13172,7 @@ function createBmadChecks() {
12713
13172
  };
12714
13173
  }
12715
13174
  const gitignoreChanges = ensureSupportedCliGitignore(ctx);
12716
- const removedRoots = attestations.map((entry) => join11(ctx.repoRoot, entry.name));
13175
+ const removedRoots = attestations.map((entry) => join12(ctx.repoRoot, entry.name));
12717
13176
  if (!ctx.dryRun) for (const path of removedRoots) rmSync5(path, { recursive: true, force: true });
12718
13177
  const changedFiles = [.../* @__PURE__ */ new Set([...projectionResult.changedFiles, ...gitignoreChanges, ...removedRoots])].sort();
12719
13178
  return {
@@ -12726,7 +13185,7 @@ function createBmadChecks() {
12726
13185
  };
12727
13186
  }
12728
13187
  }
12729
- ].map(withInstallerSkillHandoff);
13188
+ ].map(withInstallerSkillHandoff).map(withScaffoldPostcondition);
12730
13189
  }
12731
13190
  function createProjectMomoChecks() {
12732
13191
  return [
@@ -13335,8 +13794,8 @@ var NodeRecipe = class extends Recipe {
13335
13794
  // src/recipes/ProjectRecipe.ts
13336
13795
  init_project();
13337
13796
  import { spawnSync as spawnSync13 } from "node:child_process";
13338
- import { existsSync as existsSync18, lstatSync as lstatSync16, readFileSync as readFileSync20, rmSync as rmSync8 } from "node:fs";
13339
- import { dirname as dirname17, isAbsolute as isAbsolute10, join as join23, relative as relativePath, resolve as resolve18 } from "node:path";
13797
+ import { existsSync as existsSync18, lstatSync as lstatSync17, readFileSync as readFileSync21, rmSync as rmSync8 } from "node:fs";
13798
+ import { dirname as dirname17, isAbsolute as isAbsolute10, join as join24, relative as relativePath, resolve as resolve18 } from "node:path";
13340
13799
 
13341
13800
  // src/project/ticketScope.ts
13342
13801
  function ticketScope(ticket) {
@@ -13353,8 +13812,8 @@ init_tree_diff();
13353
13812
 
13354
13813
  // src/recipes/NotebookRecipe.ts
13355
13814
  init_project();
13356
- import { existsSync as existsSync17, readFileSync as readFileSync19 } from "node:fs";
13357
- import { join as join22 } from "node:path";
13815
+ import { existsSync as existsSync17, readFileSync as readFileSync20 } from "node:fs";
13816
+ import { join as join23 } from "node:path";
13358
13817
 
13359
13818
  // src/notebook/checks.ts
13360
13819
  init_config();
@@ -13362,8 +13821,8 @@ init_notes();
13362
13821
  init_output();
13363
13822
  init_state();
13364
13823
  import { spawnSync as spawnSync7 } from "node:child_process";
13365
- import { existsSync as existsSync14, readFileSync as readFileSync17 } from "node:fs";
13366
- import { join as join18 } from "node:path";
13824
+ import { existsSync as existsSync14, readFileSync as readFileSync18 } from "node:fs";
13825
+ import { join as join19 } from "node:path";
13367
13826
  var NOTEBOOK_RULE_IDS = [
13368
13827
  "notebook.configuration",
13369
13828
  "notebook.binding",
@@ -13380,10 +13839,10 @@ function result(check, status, summary, changedFiles = [], details = []) {
13380
13839
  return { id: check.id, title: check.title, status, summary, changedFiles, details };
13381
13840
  }
13382
13841
  function manifestNotebook(repo) {
13383
- const path = join18(repo, ".project.json");
13842
+ const path = join19(repo, ".project.json");
13384
13843
  if (!existsSync14(path)) return null;
13385
13844
  try {
13386
- const parsed = JSON.parse(readFileSync17(path, "utf8"));
13845
+ const parsed = JSON.parse(readFileSync18(path, "utf8"));
13387
13846
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
13388
13847
  const notebook = parsed.notebook;
13389
13848
  return notebook && typeof notebook === "object" && !Array.isArray(notebook) ? notebook : null;
@@ -13612,7 +14071,7 @@ function captureAudit(ctx) {
13612
14071
  function overviewMigrate(ctx, current) {
13613
14072
  if (ctx.dryRun) return result(current, "applied", "Would reconcile drifted Project Overview note");
13614
14073
  if (!ctx.live) return result(current, "blocked", "Reconciling the remote Overview note requires --live");
13615
- const pjanglerEntry = ctx.pjanglerRoot ? join18(ctx.pjanglerRoot, "dist", "index.js") : null;
14074
+ const pjanglerEntry = ctx.pjanglerRoot ? join19(ctx.pjanglerRoot, "dist", "index.js") : null;
13616
14075
  const entry = pjanglerEntry && existsSync14(pjanglerEntry) ? pjanglerEntry : "pj";
13617
14076
  const cmd = entry === "pj" ? ["pj"] : [process.execPath, entry];
13618
14077
  const spawned = spawnSync7(cmd[0], [...cmd.slice(1), "notebook", "overview", ctx.repoRoot], {
@@ -13664,7 +14123,7 @@ import { resolve as resolve17 } from "node:path";
13664
14123
 
13665
14124
  // src/notebook/journal-key.ts
13666
14125
  init_types();
13667
- import { createHash as createHash11 } from "node:crypto";
14126
+ import { createHash as createHash12 } from "node:crypto";
13668
14127
  function validateJournalPeriodKey(period, key) {
13669
14128
  if (period === "monthly") {
13670
14129
  if (!/^\d{4}-(?:0[1-9]|1[0-2])$/u.test(key)) throw new NotebookError("INVALID_INPUT", "Monthly period key must be YYYY-MM");
@@ -13683,7 +14142,7 @@ function validateJournalPeriodKey(period, key) {
13683
14142
  }
13684
14143
  function journalNoteOperationId(period, key) {
13685
14144
  validateJournalPeriodKey(period, key);
13686
- const bytes = createHash11("sha256").update(`pjangler-dev-journal-v1\0infra\0${period}\0${key}`).digest().subarray(0, 16);
14145
+ const bytes = createHash12("sha256").update(`pjangler-dev-journal-v1\0infra\0${period}\0${key}`).digest().subarray(0, 16);
13687
14146
  bytes[6] = bytes[6] & 15 | 80;
13688
14147
  bytes[8] = bytes[8] & 63 | 128;
13689
14148
  const hex = bytes.toString("hex");
@@ -13839,15 +14298,15 @@ init_notes();
13839
14298
  init_state();
13840
14299
  init_types();
13841
14300
  import { spawn, spawnSync as spawnSync10 } from "node:child_process";
13842
- import { createHash as createHash12, randomUUID as randomUUID5 } from "node:crypto";
13843
- import { chmodSync as chmodSync6, closeSync as closeSync6, constants as constants4, copyFileSync as copyFileSync3, existsSync as existsSync15, fstatSync as fstatSync3, fsyncSync as fsyncSync4, lstatSync as lstatSync15, mkdirSync as mkdirSync9, openSync as openSync6, readFileSync as readFileSync18, readSync as readSync3, readdirSync as readdirSync10, realpathSync as realpathSync10, renameSync as renameSync8, rmSync as rmSync7, symlinkSync as symlinkSync4, unlinkSync as unlinkSync8, writeFileSync as writeFileSync12 } from "node:fs";
13844
- import { basename as basename9, dirname as dirname16, isAbsolute as isAbsolute8, join as join21, parse as parse3, relative as relative15, resolve as resolve15, sep as sep7 } from "node:path";
14301
+ import { createHash as createHash13, randomUUID as randomUUID5 } from "node:crypto";
14302
+ import { chmodSync as chmodSync6, closeSync as closeSync7, constants as constants5, copyFileSync as copyFileSync3, existsSync as existsSync15, fstatSync as fstatSync4, fsyncSync as fsyncSync4, lstatSync as lstatSync16, mkdirSync as mkdirSync10, openSync as openSync7, readFileSync as readFileSync19, readSync as readSync3, readdirSync as readdirSync11, realpathSync as realpathSync10, renameSync as renameSync8, rmSync as rmSync7, symlinkSync as symlinkSync4, unlinkSync as unlinkSync9, writeFileSync as writeFileSync13 } from "node:fs";
14303
+ import { basename as basename10, dirname as dirname16, isAbsolute as isAbsolute8, join as join22, parse as parse3, relative as relative15, resolve as resolve15, sep as sep7 } from "node:path";
13845
14304
  import { fileURLToPath as fileURLToPath3 } from "node:url";
13846
14305
  function bundledSkillCandidates() {
13847
14306
  const candidates = [];
13848
14307
  let cursor = dirname16(fileURLToPath3(import.meta.url));
13849
14308
  for (let depth = 0; depth < 8; depth++) {
13850
- candidates.push(join21(cursor, "dist", "assets", "project-notebook-skill"), join21(cursor, "assets", "project-notebook-skill"));
14309
+ candidates.push(join22(cursor, "dist", "assets", "project-notebook-skill"), join22(cursor, "assets", "project-notebook-skill"));
13851
14310
  const parent = dirname16(cursor);
13852
14311
  if (parent === cursor) break;
13853
14312
  cursor = parent;
@@ -13865,14 +14324,14 @@ function assertOwnedSkillTree(source) {
13865
14324
  assertNoSymlinkComponents(source);
13866
14325
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
13867
14326
  const walk = (directory) => {
13868
- const directoryStat = lstatSync15(directory);
14327
+ const directoryStat = lstatSync16(directory);
13869
14328
  if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-directory component");
13870
14329
  if (uid !== void 0 && directoryStat.uid !== uid) throw new NotebookError("CONFLICT", "Project Notebook skill source is not owned by the current user");
13871
14330
  if (directoryStat.mode & 3586) throw new NotebookError("CONFLICT", "Project Notebook skill source has unsafe directory mode bits");
13872
- for (const entry of readdirSync10(directory, { withFileTypes: true })) {
14331
+ for (const entry of readdirSync11(directory, { withFileTypes: true })) {
13873
14332
  if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
13874
- const path = join21(directory, entry.name);
13875
- const stat = lstatSync15(path);
14333
+ const path = join22(directory, entry.name);
14334
+ const stat = lstatSync16(path);
13876
14335
  if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a symlink");
13877
14336
  if (entry.isDirectory()) walk(path);
13878
14337
  else if (!entry.isFile()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-regular entry");
@@ -13887,18 +14346,18 @@ function assertOwnedSkillTree(source) {
13887
14346
  function enumerateSkillPayload(source) {
13888
14347
  const result2 = [];
13889
14348
  const walk = (directory) => {
13890
- for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
14349
+ for (const entry of readdirSync11(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
13891
14350
  if (entry.isDirectory() && (entry.name === "__pycache__" || entry.name === ".git" || entry.name === "node_modules")) continue;
13892
- const path = join21(directory, entry.name);
14351
+ const path = join22(directory, entry.name);
13893
14352
  const rel = relative15(source, path).split(sep7).join("/");
13894
14353
  if (!rel.includes("/") && (rel === "export-manifest.json" || rel === "SHA256SUMS" || rel === ".source.yaml")) continue;
13895
14354
  if (!safeSkillRelativePath(rel)) throw new NotebookError("CONFLICT", `Project Notebook skill export path is unsafe: ${rel}`);
13896
- const stat = lstatSync15(path);
14355
+ const stat = lstatSync16(path);
13897
14356
  if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a symlink");
13898
14357
  if (entry.isDirectory()) walk(path);
13899
14358
  else if (entry.isFile()) {
13900
14359
  const expectedMode = rel.endsWith(".sh") || rel.startsWith("scripts/") ? "0755" : "0644";
13901
- result2.push({ path: rel, sha256: createHash12("sha256").update(readFileSync18(path)).digest("hex"), mode: expectedMode });
14360
+ result2.push({ path: rel, sha256: createHash13("sha256").update(readFileSync19(path)).digest("hex"), mode: expectedMode });
13902
14361
  } else throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
13903
14362
  }
13904
14363
  };
@@ -13906,9 +14365,9 @@ function enumerateSkillPayload(source) {
13906
14365
  return result2.sort((a, b) => a.path.localeCompare(b.path, "en"));
13907
14366
  }
13908
14367
  function parsePackedManifest(source) {
13909
- const manifestPath = join21(source, "export-manifest.json");
14368
+ const manifestPath = join22(source, "export-manifest.json");
13910
14369
  if (!existsSync15(manifestPath)) return null;
13911
- const value = JSON.parse(readFileSync18(manifestPath, "utf8"));
14370
+ const value = JSON.parse(readFileSync19(manifestPath, "utf8"));
13912
14371
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is invalid");
13913
14372
  const manifest = value;
13914
14373
  if (manifest.schema_version !== 1 || manifest.skill !== "project-notebook" || !Array.isArray(manifest.files)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is incompatible");
@@ -13917,10 +14376,10 @@ function parsePackedManifest(source) {
13917
14376
  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");
13918
14377
  if (!safeSkillRelativePath(entry.path) || paths.has(entry.path)) throw new NotebookError("CONFLICT", "Project Notebook skill export path is unsafe or duplicated");
13919
14378
  paths.add(entry.path);
13920
- const path = join21(source, ...entry.path.split("/"));
13921
- const stat = lstatSync15(path);
14379
+ const path = join22(source, ...entry.path.split("/"));
14380
+ const stat = lstatSync16(path);
13922
14381
  if (!stat.isFile() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
13923
- const actual = createHash12("sha256").update(readFileSync18(path)).digest("hex");
14382
+ const actual = createHash13("sha256").update(readFileSync19(path)).digest("hex");
13924
14383
  if (actual !== entry.sha256) throw new NotebookError("CONFLICT", `Project Notebook skill digest mismatch: ${entry.path}`);
13925
14384
  const actualMode = stat.mode & 511;
13926
14385
  const executable = entry.mode === "0755";
@@ -13932,16 +14391,16 @@ function parsePackedManifest(source) {
13932
14391
  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");
13933
14392
  const sums = `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
13934
14393
  `;
13935
- if (!existsSync15(join21(source, "SHA256SUMS")) || readFileSync18(join21(source, "SHA256SUMS"), "utf8") !== sums) throw new NotebookError("CONFLICT", "Project Notebook skill SHA256SUMS is missing or stale");
14394
+ if (!existsSync15(join22(source, "SHA256SUMS")) || readFileSync19(join22(source, "SHA256SUMS"), "utf8") !== sums) throw new NotebookError("CONFLICT", "Project Notebook skill SHA256SUMS is missing or stale");
13936
14395
  return manifest;
13937
14396
  }
13938
14397
  function expectedPackedSkill() {
13939
14398
  for (const candidate of bundledSkillCandidates()) {
13940
- if (!existsSync15(join21(candidate, "export-manifest.json"))) continue;
14399
+ if (!existsSync15(join22(candidate, "export-manifest.json"))) continue;
13941
14400
  assertOwnedSkillTree(candidate);
13942
14401
  const manifest = parsePackedManifest(candidate);
13943
14402
  if (!manifest) continue;
13944
- const digest = createHash12("sha256").update(JSON.stringify(manifest)).digest("hex");
14403
+ const digest = createHash13("sha256").update(JSON.stringify(manifest)).digest("hex");
13945
14404
  return { source: candidate, manifest, digest };
13946
14405
  }
13947
14406
  throw new NotebookError("NOT_CONFIGURED", "PJ\xE1ngler package has no verified Project Notebook skill export");
@@ -14004,13 +14463,13 @@ function verifyProjectNotebookSkillExport(source) {
14004
14463
  function resolveProjectNotebookSkillSource(env2 = process.env) {
14005
14464
  if (env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT) {
14006
14465
  const explicit = resolve15(env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT);
14007
- if (!existsSync15(join21(explicit, "SKILL.md"))) throw new NotebookError("NOT_CONFIGURED", "Configured Project Notebook skill source is unavailable");
14466
+ if (!existsSync15(join22(explicit, "SKILL.md"))) throw new NotebookError("NOT_CONFIGURED", "Configured Project Notebook skill source is unavailable");
14008
14467
  verifyProjectNotebookSkillExport(explicit);
14009
14468
  return explicit;
14010
14469
  }
14011
14470
  if (env2.PJ_SKILLS_REGISTRY_ROOT) {
14012
14471
  const canonical = resolve15(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
14013
- if (existsSync15(join21(canonical, "SKILL.md"))) {
14472
+ if (existsSync15(join22(canonical, "SKILL.md"))) {
14014
14473
  verifyProjectNotebookSkillExport(canonical);
14015
14474
  return canonical;
14016
14475
  }
@@ -14025,7 +14484,7 @@ var REPAIR_COMMAND = "pj notebook skill --apply";
14025
14484
  function probeCanonicalSkillexRootProjection(skillsRoot, link) {
14026
14485
  let rootLink;
14027
14486
  try {
14028
- rootLink = lstatSync15(skillsRoot);
14487
+ rootLink = lstatSync16(skillsRoot);
14029
14488
  } catch (error) {
14030
14489
  if (error.code === "ENOENT") return { state: "absent" };
14031
14490
  throw error;
@@ -14038,15 +14497,15 @@ function probeCanonicalSkillexRootProjection(skillsRoot, link) {
14038
14497
  if (uid !== void 0 && rootLink.uid !== uid) throw new Error("owner");
14039
14498
  const globalRoot = realpathSync10(skillsRoot);
14040
14499
  assertNoSymlinkComponents(globalRoot);
14041
- const globalStat = lstatSync15(globalRoot);
14500
+ const globalStat = lstatSync16(globalRoot);
14042
14501
  if (!globalStat.isDirectory() || globalStat.isSymbolicLink()) throw new Error("root-type");
14043
14502
  if (uid !== void 0 && globalStat.uid !== uid) throw new Error("root-owner");
14044
14503
  if (globalStat.mode & 3586) throw new Error("root-mode");
14045
14504
  const skillSetsRoot = dirname16(globalRoot);
14046
- if (basename9(globalRoot) !== "global" || basename9(skillSetsRoot) !== "skill-sets") throw new Error("layout");
14505
+ if (basename10(globalRoot) !== "global" || basename10(skillSetsRoot) !== "skill-sets") throw new Error("layout");
14047
14506
  const checkoutRoot = dirname16(skillSetsRoot);
14048
- const expectedSource = join21(checkoutRoot, "all-skills", "project-notebook");
14049
- const linkStat = lstatSync15(link);
14507
+ const expectedSource = join22(checkoutRoot, "all-skills", "project-notebook");
14508
+ const linkStat = lstatSync16(link);
14050
14509
  if (!linkStat.isSymbolicLink()) throw new Error("projection-type");
14051
14510
  if (uid !== void 0 && linkStat.uid !== uid) throw new Error("projection-owner");
14052
14511
  const projectedSource = realpathSync10(link);
@@ -14080,44 +14539,44 @@ function installPackagedProjectNotebookSkill(input = {}) {
14080
14539
  const source = input.source ?? resolveProjectNotebookSkillSource(env2);
14081
14540
  if (!source) throw new NotebookError("NOT_CONFIGURED", "Project Notebook skill source is unavailable");
14082
14541
  const manifest = verifyProjectNotebookSkillExport(source);
14083
- const digest = createHash12("sha256").update(JSON.stringify(manifest)).digest("hex");
14542
+ const digest = createHash13("sha256").update(JSON.stringify(manifest)).digest("hex");
14084
14543
  const home = env2.HOME;
14085
14544
  if (!home || !resolve15(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to install the Project Notebook skill");
14086
- const skillsRoot = join21(home, ".agents", "skills");
14087
- const link = join21(skillsRoot, "project-notebook");
14545
+ const skillsRoot = join22(home, ".agents", "skills");
14546
+ const link = join22(skillsRoot, "project-notebook");
14088
14547
  const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
14089
14548
  if (probe.state === "adopted") return { installed: false, path: link, digest };
14090
14549
  if (probe.state === "declined") return { installed: false, path: link, digest, blocked: probe.block };
14091
- const dataRoot = resolve15(env2.XDG_DATA_HOME || join21(home, ".local", "share"), "pjangler", "skills", "project-notebook");
14092
- const payload = join21(dataRoot, `${PJANGLER_VERSION}-${digest}`);
14550
+ const dataRoot = resolve15(env2.XDG_DATA_HOME || join22(home, ".local", "share"), "pjangler", "skills", "project-notebook");
14551
+ const payload = join22(dataRoot, `${PJANGLER_VERSION}-${digest}`);
14093
14552
  assertNoSymlinkComponents(dirname16(dataRoot), true);
14094
- mkdirSync9(dataRoot, { recursive: true, mode: 448 });
14553
+ mkdirSync10(dataRoot, { recursive: true, mode: 448 });
14095
14554
  assertNoSymlinkComponents(dataRoot);
14096
- const dataStat = lstatSync15(dataRoot);
14555
+ const dataStat = lstatSync16(dataRoot);
14097
14556
  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");
14098
14557
  chmodSync6(dataRoot, 448);
14099
14558
  if (!existsSync15(payload)) {
14100
- const staging = join21(dataRoot, `.staging-${randomUUID5()}`);
14101
- mkdirSync9(staging, { recursive: false, mode: 448 });
14559
+ const staging = join22(dataRoot, `.staging-${randomUUID5()}`);
14560
+ mkdirSync10(staging, { recursive: false, mode: 448 });
14102
14561
  try {
14103
14562
  for (const entry of manifest.files) {
14104
- const destination = join21(staging, ...entry.path.split("/"));
14105
- mkdirSync9(dirname16(destination), { recursive: true, mode: 493 });
14106
- copyFileSync3(join21(source, ...entry.path.split("/")), destination);
14563
+ const destination = join22(staging, ...entry.path.split("/"));
14564
+ mkdirSync10(dirname16(destination), { recursive: true, mode: 493 });
14565
+ copyFileSync3(join22(source, ...entry.path.split("/")), destination);
14107
14566
  const mode = entry.mode === "0755" ? 493 : 420;
14108
14567
  chmodSync6(destination, mode);
14109
- const fd = openSync6(destination, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
14568
+ const fd = openSync7(destination, constants5.O_RDONLY | (constants5.O_NOFOLLOW ?? 0));
14110
14569
  try {
14111
- const current = fstatSync3(fd);
14570
+ const current = fstatSync4(fd);
14112
14571
  if (!current.isFile()) throw new NotebookError("CONFLICT", `Installed skill entry is not regular: ${entry.path}`);
14113
14572
  fsyncSync4(fd);
14114
14573
  } finally {
14115
- closeSync6(fd);
14574
+ closeSync7(fd);
14116
14575
  }
14117
14576
  }
14118
- writeFileSync12(join21(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
14577
+ writeFileSync13(join22(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
14119
14578
  `, { mode: 420, flag: "wx" });
14120
- writeFileSync12(join21(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
14579
+ writeFileSync13(join22(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
14121
14580
  `, { mode: 420, flag: "wx" });
14122
14581
  verifyProjectNotebookSkillExport(staging);
14123
14582
  renameSync8(staging, payload);
@@ -14125,17 +14584,17 @@ function installPackagedProjectNotebookSkill(input = {}) {
14125
14584
  if (existsSync15(staging)) rmSync7(staging, { recursive: true, force: true });
14126
14585
  }
14127
14586
  } else {
14128
- const stat = lstatSync15(payload);
14587
+ const stat = lstatSync16(payload);
14129
14588
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Installed Project Notebook payload is not a real directory");
14130
14589
  verifyProjectNotebookSkillExport(payload);
14131
14590
  }
14132
14591
  assertNoSymlinkComponents(skillsRoot, true);
14133
- mkdirSync9(skillsRoot, { recursive: true, mode: 448 });
14592
+ mkdirSync10(skillsRoot, { recursive: true, mode: 448 });
14134
14593
  assertNoSymlinkComponents(skillsRoot);
14135
14594
  let linkExists = existsSync15(link);
14136
14595
  if (!linkExists) {
14137
14596
  try {
14138
- lstatSync15(link);
14597
+ lstatSync16(link);
14139
14598
  linkExists = true;
14140
14599
  } catch {
14141
14600
  }
@@ -14152,7 +14611,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
14152
14611
  repair: REPAIR_COMMAND
14153
14612
  }
14154
14613
  });
14155
- const stat = lstatSync15(link);
14614
+ const stat = lstatSync16(link);
14156
14615
  if (!stat.isSymbolicLink()) return foreign(["the path is a real file or directory, not a PJ\xE1ngler-owned link"]);
14157
14616
  const target = realpathSync10(link);
14158
14617
  if (target !== realpathSync10(payload) && !isVerifiedCanonicalSkillexProjection(target)) {
@@ -14180,15 +14639,15 @@ function collectSupersededPayloads(dataRoot, keep) {
14180
14639
  const removed = [];
14181
14640
  let entries;
14182
14641
  try {
14183
- entries = readdirSync10(dataRoot);
14642
+ entries = readdirSync11(dataRoot);
14184
14643
  } catch {
14185
14644
  return removed;
14186
14645
  }
14187
14646
  for (const name of entries) {
14188
14647
  if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?-[0-9a-f]{64}$/u.test(name)) continue;
14189
- const candidate = join21(dataRoot, name);
14648
+ const candidate = join22(dataRoot, name);
14190
14649
  try {
14191
- const stat = lstatSync15(candidate);
14650
+ const stat = lstatSync16(candidate);
14192
14651
  if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
14193
14652
  if (kept.has(realpathSync10(candidate))) continue;
14194
14653
  rmSync7(candidate, { recursive: true, force: true });
@@ -14202,8 +14661,8 @@ function repairProjectNotebookSkillProjection(input = {}) {
14202
14661
  const env2 = input.env ?? process.env;
14203
14662
  const home = env2.HOME;
14204
14663
  if (!home || !resolve15(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to repair the Project Notebook skill projection");
14205
- const skillsRoot = join21(home, ".agents", "skills");
14206
- const link = join21(skillsRoot, "project-notebook");
14664
+ const skillsRoot = join22(home, ".agents", "skills");
14665
+ const link = join22(skillsRoot, "project-notebook");
14207
14666
  const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
14208
14667
  if (probe.state === "adopted") return { status: "clean", summary: "Canonical Skillex projection already matches the version-pinned export", source: probe.source, drift: [], changed_files: [] };
14209
14668
  if (probe.state !== "declined") {
@@ -14216,8 +14675,8 @@ function repairProjectNotebookSkillProjection(input = {}) {
14216
14675
  assertOwnedSkillTree(source);
14217
14676
  const present = [];
14218
14677
  const walk = (directory) => {
14219
- for (const entry of readdirSync10(directory, { withFileTypes: true })) {
14220
- const path = join21(directory, entry.name);
14678
+ for (const entry of readdirSync11(directory, { withFileTypes: true })) {
14679
+ const path = join22(directory, entry.name);
14221
14680
  const rel = relative15(source, path).split(sep7).join("/");
14222
14681
  if (entry.isDirectory()) {
14223
14682
  walk(path);
@@ -14231,11 +14690,11 @@ function repairProjectNotebookSkillProjection(input = {}) {
14231
14690
  walk(source);
14232
14691
  const stale = present.filter((rel) => !wanted.has(rel)).sort();
14233
14692
  const changed = expected.manifest.files.filter((entry) => {
14234
- const path = join21(source, ...entry.path.split("/"));
14693
+ const path = join22(source, ...entry.path.split("/"));
14235
14694
  if (!existsSync15(path)) return true;
14236
- const stat = lstatSync15(path);
14695
+ const stat = lstatSync16(path);
14237
14696
  if (!stat.isFile()) return true;
14238
- if (createHash12("sha256").update(readFileSync18(path)).digest("hex") !== entry.sha256) return true;
14697
+ if (createHash13("sha256").update(readFileSync19(path)).digest("hex") !== entry.sha256) return true;
14239
14698
  const mode = stat.mode & 511;
14240
14699
  const executable = entry.mode === "0755";
14241
14700
  return executable && (mode & 64) === 0 || !executable && (mode & 73) !== 0;
@@ -14250,15 +14709,15 @@ function repairProjectNotebookSkillProjection(input = {}) {
14250
14709
  summary: `Would restore ${affected.length} file(s) in ${source} from the version-pinned export`,
14251
14710
  source,
14252
14711
  drift: probe.block.details,
14253
- changed_files: affected.map((rel) => join21(source, ...rel.split("/")))
14712
+ changed_files: affected.map((rel) => join22(source, ...rel.split("/")))
14254
14713
  };
14255
14714
  }
14256
- for (const rel of stale) rmSync7(join21(source, ...rel.split("/")), { force: true });
14715
+ for (const rel of stale) rmSync7(join22(source, ...rel.split("/")), { force: true });
14257
14716
  for (const rel of changed) {
14258
- const destination = join21(source, ...rel.split("/"));
14717
+ const destination = join22(source, ...rel.split("/"));
14259
14718
  const entry = expected.manifest.files.find((item) => item.path === rel);
14260
- mkdirSync9(dirname16(destination), { recursive: true, mode: 493 });
14261
- copyFileSync3(join21(expected.source, ...rel.split("/")), destination);
14719
+ mkdirSync10(dirname16(destination), { recursive: true, mode: 493 });
14720
+ copyFileSync3(join22(expected.source, ...rel.split("/")), destination);
14262
14721
  chmodSync6(destination, entry.mode === "0755" ? 493 : 420);
14263
14722
  }
14264
14723
  verifyProjectNotebookSkillExport(source);
@@ -14267,21 +14726,21 @@ function repairProjectNotebookSkillProjection(input = {}) {
14267
14726
  summary: `Restored ${affected.length} file(s) in ${source} from the version-pinned export; commit the change in the Skillex checkout`,
14268
14727
  source,
14269
14728
  drift: probe.block.details,
14270
- changed_files: affected.map((rel) => join21(source, ...rel.split("/")))
14729
+ changed_files: affected.map((rel) => join22(source, ...rel.split("/")))
14271
14730
  };
14272
14731
  }
14273
14732
  function inspectProjectNotebookIntegration(env2 = process.env) {
14274
14733
  const home = env2.HOME;
14275
14734
  if (!home) return { skill_installed: false, hooks_projected: false, details: ["HOME is unavailable"] };
14276
- const skillsRoot = join21(home, ".agents", "skills");
14277
- const link = join21(skillsRoot, "project-notebook");
14735
+ const skillsRoot = join22(home, ".agents", "skills");
14736
+ const link = join22(skillsRoot, "project-notebook");
14278
14737
  try {
14279
14738
  const expected = expectedPackedSkill();
14280
- const dataRoot = resolve15(env2.XDG_DATA_HOME || join21(home, ".local", "share"), "pjangler", "skills", "project-notebook");
14281
- const expectedPayload = join21(dataRoot, `${PJANGLER_VERSION}-${expected.digest}`);
14739
+ const dataRoot = resolve15(env2.XDG_DATA_HOME || join22(home, ".local", "share"), "pjangler", "skills", "project-notebook");
14740
+ const expectedPayload = join22(dataRoot, `${PJANGLER_VERSION}-${expected.digest}`);
14282
14741
  const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
14283
14742
  if (probe.state === "declined") return { skill_installed: false, hooks_projected: false, details: probe.block.details, blocked: probe.block };
14284
- const stat = lstatSync15(link);
14743
+ const stat = lstatSync16(link);
14285
14744
  if (!stat.isSymbolicLink()) {
14286
14745
  return {
14287
14746
  skill_installed: false,
@@ -14318,8 +14777,8 @@ function inspectProjectNotebookIntegration(env2 = process.env) {
14318
14777
  }
14319
14778
  }
14320
14779
  function projectorArguments(source, command, input) {
14321
- const script = join21(source, "scripts", "project-hooks.py");
14322
- const args = [script, command, "--master", join21(source, "hooks", "hooks.master.json"), "--fragment", join21(source, "hooks", "claude.settings.json"), "--target", input.target];
14780
+ const script = join22(source, "scripts", "project-hooks.py");
14781
+ const args = [script, command, "--master", join22(source, "hooks", "hooks.master.json"), "--fragment", join22(source, "hooks", "claude.settings.json"), "--target", input.target];
14323
14782
  if (command === "check") args.push("--json");
14324
14783
  else if (input.stateHome) args.push("--state-home", input.stateHome);
14325
14784
  return args;
@@ -14341,7 +14800,7 @@ function checkProjectNotebookHooks(input = {}) {
14341
14800
  verifyProjectNotebookSkillExport(source);
14342
14801
  const home = env2.HOME;
14343
14802
  if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to check Project Notebook hooks");
14344
- const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join21(home, ".claude", "settings.json"));
14803
+ const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join22(home, ".claude", "settings.json"));
14345
14804
  const result2 = spawnSync10("/usr/bin/python3", ["-I", ...projectorArguments(source, "check", { target })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
14346
14805
  if (result2.status !== 0 && result2.status !== 1) throw new NotebookError("CONFLICT", (result2.stderr || "Project Notebook projector check failed").trim().slice(0, 512));
14347
14806
  try {
@@ -14359,14 +14818,14 @@ function installProjectNotebookIntegration(input = {}) {
14359
14818
  const source = realpathSync10(skill.path);
14360
14819
  const home = env2.HOME;
14361
14820
  if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to install Project Notebook hooks");
14362
- const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join21(home, ".claude", "settings.json"));
14363
- const stateHome = resolve15(env2.XDG_STATE_HOME || join21(home, ".local", "state"));
14821
+ const target = resolve15(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join22(home, ".claude", "settings.json"));
14822
+ const stateHome = resolve15(env2.XDG_STATE_HOME || join22(home, ".local", "state"));
14364
14823
  const result2 = spawnSync10("/usr/bin/python3", ["-I", ...projectorArguments(source, "install", { target, stateHome })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
14365
14824
  if (result2.status !== 0) {
14366
14825
  if (skill.installed) {
14367
14826
  try {
14368
- const stat = lstatSync15(skill.path);
14369
- if (stat.isSymbolicLink() && realpathSync10(skill.path) === source) unlinkSync8(skill.path);
14827
+ const stat = lstatSync16(skill.path);
14828
+ if (stat.isSymbolicLink() && realpathSync10(skill.path) === source) unlinkSync9(skill.path);
14370
14829
  } catch {
14371
14830
  }
14372
14831
  }
@@ -14383,10 +14842,10 @@ function assertNoSymlinkComponents(path, allowMissing = false) {
14383
14842
  const root = parse3(absolute).root;
14384
14843
  let cursor = root;
14385
14844
  for (const component of absolute.slice(root.length).split(sep7).filter(Boolean)) {
14386
- cursor = join21(cursor, component);
14845
+ cursor = join22(cursor, component);
14387
14846
  let stat;
14388
14847
  try {
14389
- stat = lstatSync15(cursor);
14848
+ stat = lstatSync16(cursor);
14390
14849
  } catch (error) {
14391
14850
  if (allowMissing && error.code === "ENOENT") return;
14392
14851
  throw error;
@@ -14992,17 +15451,17 @@ init_remote_mutation_journal();
14992
15451
  function integrationEnvironment(module, ctx) {
14993
15452
  const env2 = { ...module.environment, HOME: ctx.homeDir };
14994
15453
  if (module.environment.HOME !== ctx.homeDir) {
14995
- env2.XDG_DATA_HOME = join22(ctx.homeDir, ".local", "share");
14996
- env2.XDG_STATE_HOME = join22(ctx.homeDir, ".local", "state");
14997
- env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS = join22(ctx.homeDir, ".claude", "settings.json");
15454
+ env2.XDG_DATA_HOME = join23(ctx.homeDir, ".local", "share");
15455
+ env2.XDG_STATE_HOME = join23(ctx.homeDir, ".local", "state");
15456
+ env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS = join23(ctx.homeDir, ".claude", "settings.json");
14998
15457
  }
14999
15458
  return env2;
15000
15459
  }
15001
15460
  function resolvedForPlan(plan) {
15002
- const manifestPath = join22(plan.project.repo_path, ".project.json");
15461
+ const manifestPath = join23(plan.project.repo_path, ".project.json");
15003
15462
  let manifest = plan.manifest;
15004
15463
  if (existsSync17(manifestPath)) {
15005
- const parsed = JSON.parse(readFileSync19(manifestPath, "utf8"));
15464
+ const parsed = JSON.parse(readFileSync20(manifestPath, "utf8"));
15006
15465
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${manifestPath} must contain a JSON object`);
15007
15466
  manifest = parsed;
15008
15467
  }
@@ -15163,7 +15622,7 @@ function unsafeToRemove(targetDir) {
15163
15622
  while (!seen.has(cursor)) {
15164
15623
  seen.add(cursor);
15165
15624
  try {
15166
- const stat = lstatSync16(cursor);
15625
+ const stat = lstatSync17(cursor);
15167
15626
  if (stat.isSymbolicLink()) {
15168
15627
  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`;
15169
15628
  }
@@ -15191,12 +15650,12 @@ function publicMigration(report) {
15191
15650
  };
15192
15651
  }
15193
15652
  function hasGitRepository(runtime, targetDir) {
15194
- if (!existsSync18(join23(targetDir, ".git"))) return false;
15653
+ if (!existsSync18(join24(targetDir, ".git"))) return false;
15195
15654
  return runtime.runGit(targetDir, ["rev-parse", "--is-inside-work-tree"]).status === 0;
15196
15655
  }
15197
15656
  function refreshPlanFromCanonicalManifest(plan) {
15198
- const manifestPath = join23(plan.project.repo_path, ".project.json");
15199
- const manifest = JSON.parse(readFileSync20(manifestPath, "utf8"));
15657
+ const manifestPath = join24(plan.project.repo_path, ".project.json");
15658
+ const manifest = JSON.parse(readFileSync21(manifestPath, "utf8"));
15200
15659
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
15201
15660
  throw new Error(`${manifestPath} must contain a JSON object`);
15202
15661
  }
@@ -15410,12 +15869,12 @@ var ProjectRecipe = class extends Recipe {
15410
15869
  changedFiles: result2.status === "applied" || result2.status === "partial" ? result2.changedFiles : [],
15411
15870
  message: result2.summary
15412
15871
  })));
15413
- errors.push(...migrationReport.results.filter((result2) => result2.status === "blocked" || result2.status === "partial").map((result2) => `${result2.id}: ${result2.summary}`));
15872
+ errors.push(...migrationReport.results.filter((result2) => result2.status === "blocked" || result2.status === "partial").map((result2) => describeFinding(result2)));
15414
15873
  }
15415
15874
  const eligibilityAudit = errors.length === 0 ? publicAudit(await this.registry.auditRecipes({ ...transactionContext, dryRun: true })) : void 0;
15416
15875
  audit = eligibilityAudit;
15417
15876
  if (eligibilityAudit && !eligibilityAudit.ok) {
15418
- errors.push(...eligibilityAudit.rules.filter((finding2) => gatesProject(finding2)).map((finding2) => `${finding2.id}: ${finding2.summary}`));
15877
+ errors.push(...eligibilityAudit.rules.filter((finding2) => gatesProject(finding2)).map((finding2) => describeFinding(finding2)));
15419
15878
  }
15420
15879
  phases.push({
15421
15880
  id: "project.audit:eligibility",
@@ -15427,7 +15886,7 @@ var ProjectRecipe = class extends Recipe {
15427
15886
  if (hasGitRepository(this.runtime, targetDir)) {
15428
15887
  phases.push({ id: "project.git", status: "unchanged", changedFiles: [], message: "Git repository already initialized" });
15429
15888
  } else {
15430
- const gitPath = join23(targetDir, ".git");
15889
+ const gitPath = join24(targetDir, ".git");
15431
15890
  for (const { args, label, options } of [
15432
15891
  { args: ["init", "--initial-branch=main"], label: "git init" },
15433
15892
  { args: ["add", "-A"], label: "git add" },
@@ -15506,7 +15965,7 @@ var ProjectRecipe = class extends Recipe {
15506
15965
  transactionContext.notebookManifestBinding = projectedNotebook?.binding ? Object.freeze({ ...projectedNotebook.binding }) : null;
15507
15966
  transactionContext.notebookObservation = Object.freeze(await notebookRecipe.observeExternal(plan, notebookPlan));
15508
15967
  const candidateAudit = publicAudit(await this.registry.auditRecipes(transactionContext, ["notebook"]));
15509
- if (!candidateAudit.ok) errors.push(...candidateAudit.rules.filter((item) => item.status === "fail" || item.status === "warn").map((item) => `${item.id}: ${item.summary}`));
15968
+ if (!candidateAudit.ok) errors.push(...candidateAudit.rules.filter((item) => item.status === "fail" || item.status === "warn").map((item) => describeFinding(item)));
15510
15969
  }
15511
15970
  phases.push({
15512
15971
  id: "project.external:notebook",
@@ -15562,7 +16021,7 @@ var ProjectRecipe = class extends Recipe {
15562
16021
  }
15563
16022
  audit = errors.length === 0 ? publicAudit(await this.registry.auditRecipes({ ...transactionContext, dryRun: true })) : audit;
15564
16023
  if (errors.length === 0 && audit && !audit.ok) {
15565
- errors.push(...audit.rules.filter((finding2) => gatesProject(finding2)).map((finding2) => `${finding2.id}: ${finding2.summary}`));
16024
+ errors.push(...audit.rules.filter((finding2) => gatesProject(finding2)).map((finding2) => describeFinding(finding2)));
15566
16025
  }
15567
16026
  phases.push({
15568
16027
  id: "project.audit",
@@ -15638,12 +16097,12 @@ var ProjectRecipe = class extends Recipe {
15638
16097
  };
15639
16098
 
15640
16099
  // src/recipes/registry.ts
15641
- import { readFileSync as readFileSync21 } from "node:fs";
15642
- import { join as join24 } from "node:path";
16100
+ import { readFileSync as readFileSync22 } from "node:fs";
16101
+ import { join as join25 } from "node:path";
15643
16102
  function projectWaivers(repoRoot) {
15644
16103
  let waive;
15645
16104
  try {
15646
- waive = JSON.parse(readFileSync21(join24(repoRoot, ".project.json"), "utf8"))?.parity?.waive;
16105
+ waive = JSON.parse(readFileSync22(join25(repoRoot, ".project.json"), "utf8"))?.parity?.waive;
15647
16106
  } catch {
15648
16107
  return /* @__PURE__ */ new Map();
15649
16108
  }
@@ -15984,8 +16443,8 @@ var recipeRegistry = new RecipeRegistry([
15984
16443
 
15985
16444
  // src/commands/AgentHooksCommands.ts
15986
16445
  import { homedir as homedir8 } from "node:os";
15987
- import { join as join25, dirname as dirname18 } from "node:path";
15988
- import { existsSync as existsSync19, cpSync, mkdirSync as mkdirSync10, readFileSync as readFileSync22, writeFileSync as writeFileSync13 } from "node:fs";
16446
+ import { join as join26, dirname as dirname18 } from "node:path";
16447
+ import { existsSync as existsSync19, cpSync, mkdirSync as mkdirSync11, readFileSync as readFileSync23, writeFileSync as writeFileSync14 } from "node:fs";
15989
16448
  import { fileURLToPath as fileURLToPath4 } from "node:url";
15990
16449
  init_project();
15991
16450
  import { parse as parseToml4 } from "smol-toml";
@@ -15998,16 +16457,16 @@ function resolveTemplateRoot() {
15998
16457
  try {
15999
16458
  let dir = dirname18(fileURLToPath4(import.meta.url));
16000
16459
  for (let i = 0; i < 8; i++) {
16001
- candidates.push(join25(dir, "templates", "commonproject", "template"));
16460
+ candidates.push(join26(dir, "templates", "commonproject", "template"));
16002
16461
  const parent = dirname18(dir);
16003
16462
  if (parent === dir) break;
16004
16463
  dir = parent;
16005
16464
  }
16006
16465
  } catch {
16007
16466
  }
16008
- candidates.push(join25(homedir8(), "code", "pjangler", "templates", "commonproject", "template"));
16467
+ candidates.push(join26(homedir8(), "code", "pjangler", "templates", "commonproject", "template"));
16009
16468
  for (const c of candidates) {
16010
- if (existsSync19(join25(c, ".agents", "hooks", "hooks.master.json"))) return c;
16469
+ if (existsSync19(join26(c, ".agents", "hooks", "hooks.master.json"))) return c;
16011
16470
  }
16012
16471
  throw new Error(
16013
16472
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -16033,15 +16492,15 @@ var CopyAgentHooksTree = class extends Command {
16033
16492
  const created = [];
16034
16493
  const skipped = [];
16035
16494
  for (const { rel, dir } of items) {
16036
- const src = join25(templateRoot, rel);
16037
- const dest = join25(this.context.targetDir, rel);
16495
+ const src = join26(templateRoot, rel);
16496
+ const dest = join26(this.context.targetDir, rel);
16038
16497
  if (!existsSync19(src)) continue;
16039
16498
  if (existsSync19(dest) && !this.context.force) {
16040
16499
  skipped.push(rel);
16041
16500
  continue;
16042
16501
  }
16043
16502
  if (!this.context.dryRun) {
16044
- mkdirSync10(dirname18(dest), { recursive: true });
16503
+ mkdirSync11(dirname18(dest), { recursive: true });
16045
16504
  cpSync(src, dest, { recursive: dir, force: true });
16046
16505
  }
16047
16506
  created.push(rel);
@@ -16062,14 +16521,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
16062
16521
  if (!resolveAgentHooksLayer2()) {
16063
16522
  return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
16064
16523
  }
16065
- const misePath = join25(this.context.targetDir, "mise.toml");
16524
+ const misePath = join26(this.context.targetDir, "mise.toml");
16066
16525
  if (!existsSync19(misePath)) {
16067
16526
  return {
16068
16527
  success: false,
16069
16528
  message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj add mise` first, then re-run."
16070
16529
  };
16071
16530
  }
16072
- let content = readFileSync22(misePath, "utf8");
16531
+ let content = readFileSync23(misePath, "utf8");
16073
16532
  if (content.includes(_WireMiseAgentHooks.MARKER)) {
16074
16533
  return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
16075
16534
  }
@@ -16153,7 +16612,7 @@ ${leaveBlock}`);
16153
16612
  if (!verdict.ok) {
16154
16613
  return { success: false, message: `\u26A0\uFE0F mise.toml was not rewritten: ${verdict.reason}. Add the agent-hooks tasks by hand.` };
16155
16614
  }
16156
- if (!this.context.dryRun) writeFileSync13(misePath, content);
16615
+ if (!this.context.dryRun) writeFileSync14(misePath, content);
16157
16616
  if (wiredHooks) {
16158
16617
  return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
16159
16618
  }
@@ -16284,7 +16743,7 @@ if __name__ == "__main__":
16284
16743
 
16285
16744
  // src/commands/AddMiseCodegraphScript.ts
16286
16745
  import { chmodSync as chmodSync7 } from "fs";
16287
- import { join as join26 } from "path";
16746
+ import { join as join27 } from "path";
16288
16747
  var AddMiseCodegraphScript = class extends Command {
16289
16748
  async invoke() {
16290
16749
  const filePath = ".mise/scripts/codegraph.sh";
@@ -16338,7 +16797,7 @@ fi
16338
16797
  `;
16339
16798
  this.writeFile(filePath, content);
16340
16799
  if (!this.context.dryRun) {
16341
- chmodSync7(join26(this.context.targetDir, filePath), 493);
16800
+ chmodSync7(join27(this.context.targetDir, filePath), 493);
16342
16801
  }
16343
16802
  return {
16344
16803
  success: true,
@@ -16376,12 +16835,12 @@ SECRET_KEY=""
16376
16835
  // src/parity/index.ts
16377
16836
  import { existsSync as existsSync20 } from "node:fs";
16378
16837
  import { homedir as homedir9 } from "node:os";
16379
- import { dirname as dirname19, join as join27, resolve as resolve19 } from "node:path";
16838
+ import { dirname as dirname19, join as join28, resolve as resolve19 } from "node:path";
16380
16839
  import { fileURLToPath as fileURLToPath5 } from "node:url";
16381
16840
  function resolvePjanglerRoot2() {
16382
16841
  let dir = dirname19(fileURLToPath5(import.meta.url));
16383
16842
  while (dir !== dirname19(dir)) {
16384
- if (existsSync20(join27(dir, "package.json")) && existsSync20(join27(dir, "templates", "commonproject", "copier.yml"))) return dir;
16843
+ if (existsSync20(join28(dir, "package.json")) && existsSync20(join28(dir, "templates", "commonproject", "copier.yml"))) return dir;
16385
16844
  dir = dirname19(dir);
16386
16845
  }
16387
16846
  return resolve19(process.cwd());
@@ -16556,13 +17015,13 @@ init_version();
16556
17015
  // src/project/info.ts
16557
17016
  init_boardUrl();
16558
17017
  init_project();
16559
- import { readFileSync as readFileSync23 } from "node:fs";
16560
- import { join as join28, resolve as resolve20 } from "node:path";
17018
+ import { readFileSync as readFileSync24 } from "node:fs";
17019
+ import { join as join29, resolve as resolve20 } from "node:path";
16561
17020
  function readManifest2(root) {
16562
- const path = join28(root, ".project.json");
17021
+ const path = join29(root, ".project.json");
16563
17022
  let manifest;
16564
17023
  try {
16565
- const value = JSON.parse(readFileSync23(path, "utf8"));
17024
+ const value = JSON.parse(readFileSync24(path, "utf8"));
16566
17025
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected a JSON object");
16567
17026
  manifest = value;
16568
17027
  } catch (error) {
@@ -16603,7 +17062,7 @@ function readProjectInfo(input = {}) {
16603
17062
  ok: true,
16604
17063
  project_id: projectId,
16605
17064
  repo_path: resolve20(root),
16606
- manifest_path: join28(resolve20(root), ".project.json"),
17065
+ manifest_path: join29(resolve20(root), ".project.json"),
16607
17066
  manifest,
16608
17067
  registry: { location, status: "indexed" }
16609
17068
  };
@@ -16631,14 +17090,14 @@ function readProjectInfo(input = {}) {
16631
17090
  }
16632
17091
 
16633
17092
  // src/describe/index.ts
16634
- import { existsSync as existsSync21, readFileSync as readFileSync24, readdirSync as readdirSync11, statSync as statSync7 } from "node:fs";
16635
- import { join as join30, resolve as resolve21 } from "node:path";
17093
+ import { existsSync as existsSync21, readFileSync as readFileSync25, readdirSync as readdirSync12, statSync as statSync7 } from "node:fs";
17094
+ import { join as join31, resolve as resolve21 } from "node:path";
16636
17095
  init_project();
16637
17096
 
16638
17097
  // src/describe/activity.ts
16639
17098
  import { spawn as spawn2, spawnSync as spawnSync14 } from "node:child_process";
16640
17099
  import { statSync as statSync6 } from "node:fs";
16641
- import { join as join29 } from "node:path";
17100
+ import { join as join30 } from "node:path";
16642
17101
  var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
16643
17102
  var MAX_DIRTY_STATS = 500;
16644
17103
  var GIT_TIMEOUT_MS = 5e3;
@@ -16767,7 +17226,7 @@ function uncommittedSource(repo, paths) {
16767
17226
  let newest = 0;
16768
17227
  for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
16769
17228
  try {
16770
- const mtime = Math.floor(statSync6(join29(repo, path)).mtimeMs / 1e3);
17229
+ const mtime = Math.floor(statSync6(join30(repo, path)).mtimeMs / 1e3);
16771
17230
  if (mtime > newest) newest = mtime;
16772
17231
  } catch {
16773
17232
  }
@@ -16876,7 +17335,7 @@ var CONFIG_FILES = [
16876
17335
  ];
16877
17336
  function readJson(path) {
16878
17337
  try {
16879
- const parsed = JSON.parse(readFileSync24(path, "utf8"));
17338
+ const parsed = JSON.parse(readFileSync25(path, "utf8"));
16880
17339
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
16881
17340
  } catch {
16882
17341
  return void 0;
@@ -16898,23 +17357,23 @@ function describeType(repo) {
16898
17357
  const evidence = [];
16899
17358
  const note = (signal, file) => evidence.push(`${signal} (${file})`);
16900
17359
  for (const marker of LANGUAGE_MARKERS) {
16901
- if (!existsSync21(join30(repo, marker.file))) continue;
17360
+ if (!existsSync21(join31(repo, marker.file))) continue;
16902
17361
  if (!languages.includes(marker.language)) {
16903
17362
  languages.push(marker.language);
16904
17363
  note(marker.language, marker.file);
16905
17364
  }
16906
17365
  }
16907
17366
  try {
16908
- const dotnet = readdirSync11(repo).find((entry) => entry.endsWith(".csproj") || entry.endsWith(".sln"));
17367
+ const dotnet = readdirSync12(repo).find((entry) => entry.endsWith(".csproj") || entry.endsWith(".sln"));
16909
17368
  if (dotnet && !languages.includes("dotnet")) {
16910
17369
  languages.push("dotnet");
16911
17370
  note("dotnet", dotnet);
16912
17371
  }
16913
17372
  } catch {
16914
17373
  }
16915
- const pkg = readJson(join30(repo, "package.json"));
17374
+ const pkg = readJson(join31(repo, "package.json"));
16916
17375
  if (pkg) {
16917
- if (existsSync21(join30(repo, "tsconfig.json"))) {
17376
+ if (existsSync21(join31(repo, "tsconfig.json"))) {
16918
17377
  const index = languages.indexOf("javascript");
16919
17378
  if (index >= 0) languages.splice(index, 1);
16920
17379
  if (!languages.includes("typescript")) {
@@ -16948,14 +17407,14 @@ function describeType(repo) {
16948
17407
  ["hermes-fleet-host", "agents/hermes"]
16949
17408
  ];
16950
17409
  for (const [role, marker] of roleMarkers) {
16951
- if (!existsSync21(join30(repo, marker))) continue;
17410
+ if (!existsSync21(join31(repo, marker))) continue;
16952
17411
  roles.push(role);
16953
17412
  note(role, marker);
16954
17413
  }
16955
17414
  return { primaryLanguage: languages[0], languages, roles, evidence };
16956
17415
  }
16957
17416
  function describeIdentity(repo, registryPath) {
16958
- const manifestPath = join30(repo, ".project.json");
17417
+ const manifestPath = join31(repo, ".project.json");
16959
17418
  const manifest = readJson(manifestPath);
16960
17419
  const drift = [];
16961
17420
  let record3;
@@ -17048,12 +17507,12 @@ function describeSubsystems(repo, findings) {
17048
17507
  const markers = SUBSYSTEM_MARKERS[metadata.id] ?? [];
17049
17508
  const evidence = metadata.id === "notebook" ? (() => {
17050
17509
  try {
17051
- const manifest = JSON.parse(readFileSync24(join30(repo, ".project.json"), "utf8"));
17510
+ const manifest = JSON.parse(readFileSync25(join31(repo, ".project.json"), "utf8"));
17052
17511
  return manifest.notebook && typeof manifest.notebook === "object" ? [".project.json#notebook"] : [];
17053
17512
  } catch {
17054
17513
  return [];
17055
17514
  }
17056
- })() : markers.filter((marker) => existsSync21(join30(repo, marker)));
17515
+ })() : markers.filter((marker) => existsSync21(join31(repo, marker)));
17057
17516
  const rules = (byRecipe.get(metadata.id) ?? []).map((finding2) => ({
17058
17517
  id: finding2.id,
17059
17518
  title: finding2.title,
@@ -17072,7 +17531,7 @@ function describeNotebook(repo, registryPath) {
17072
17531
  try {
17073
17532
  const registry = loadProjectRegistry(registryPath);
17074
17533
  const project = Object.values(registry.projects).find((entry) => resolve21(entry.repo_path) === resolve21(repo));
17075
- const manifest = existsSync21(join30(repo, ".project.json")) ? JSON.parse(readFileSync24(join30(repo, ".project.json"), "utf8")) : void 0;
17534
+ const manifest = existsSync21(join31(repo, ".project.json")) ? JSON.parse(readFileSync25(join31(repo, ".project.json"), "utf8")) : void 0;
17076
17535
  const declared = Boolean(project?.notebook || manifest?.notebook && typeof manifest.notebook === "object");
17077
17536
  if (!declared) return { declared: false, bindingState: null, notebookId: null, overviewNoteId: null, health: null, remoteCheck: "skip", captureAdmission: null };
17078
17537
  const config = loadEffectiveNotebookConfig(repo, registryPath);
@@ -17090,7 +17549,7 @@ function describeNotebook(repo, registryPath) {
17090
17549
  }
17091
17550
  }
17092
17551
  function describeConfigFiles(repo) {
17093
- return CONFIG_FILES.filter((spec) => existsSync21(join30(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
17552
+ return CONFIG_FILES.filter((spec) => existsSync21(join31(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
17094
17553
  }
17095
17554
  function describeNextSteps(description, findings) {
17096
17555
  const steps = [];
@@ -17369,7 +17828,7 @@ function resolveTargetDir(targetDir) {
17369
17828
  function resolvePjanglerRoot3() {
17370
17829
  let dir = dirname20(fileURLToPath6(import.meta.url));
17371
17830
  while (dir !== dirname20(dir)) {
17372
- if (existsSync22(join31(dir, "package.json")) && existsSync22(join31(dir, "templates", "commonproject", "copier.yml"))) {
17831
+ if (existsSync22(join32(dir, "package.json")) && existsSync22(join32(dir, "templates", "commonproject", "copier.yml"))) {
17373
17832
  return dir;
17374
17833
  }
17375
17834
  dir = dirname20(dir);
@@ -17669,7 +18128,7 @@ server.registerTool(
17669
18128
  if (!existsSync22(parentDir) || !statSync8(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
17670
18129
  const targetDir = resolveContainedPath(
17671
18130
  parentDir,
17672
- explicitTargetDir ?? join31(parentDir, projectSlug),
18131
+ explicitTargetDir ?? join32(parentDir, projectSlug),
17673
18132
  "Bootstrap target"
17674
18133
  );
17675
18134
  const overwrite = input.overwrite ?? input.force ?? false;
@@ -17792,7 +18251,7 @@ server.registerTool(
17792
18251
  registryPath: input.registryPath,
17793
18252
  force: input.force ?? false,
17794
18253
  overwrite: input.force ?? false,
17795
- scaffold: !(input.targetDir && existsSync22(join31(resolve22(input.targetDir), ".git")))
18254
+ scaffold: !(input.targetDir && existsSync22(join32(resolve22(input.targetDir), ".git")))
17796
18255
  });
17797
18256
  if (!input.apply) return asText(publicCompositeProjectResponse(publicProjectPlan(plan), plan));
17798
18257
  const preflight = await preflightProjectApply(plan, resolvePjanglerRoot3());