@delorenj/pjangler 1.2.19 → 1.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { spawnSync as spawnSync7 } from "node:child_process";
5
- import { existsSync as existsSync10, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
6
- import { basename as basename4, join as join13, resolve as resolve3 } from "node:path";
5
+ import { existsSync as existsSync11, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
6
+ import { basename as basename4, join as join15, resolve as resolve3 } from "node:path";
7
7
  import { Command as Command3 } from "commander";
8
8
 
9
9
  // src/commands/hermes/types.ts
@@ -410,97 +410,284 @@ var AddMiseCodegraphScript = class extends Command {
410
410
  };
411
411
  }
412
412
  const content = `#!/usr/bin/env bash
413
- # Mise enter hook: ensure the CodeGraph CLI is available and initialize the
414
- # project index. If \`codegraph\` is not installed, this script installs it
415
- # non-interactively into the project-local .mise/bin directory and retries.
416
- #
417
- # This is intended to run from a mise enter hook so onboarding a new host is
418
- # fully automatic.
413
+ # Auto-generated by pjangler
414
+
415
+ REPO_ROOT="$(pwd)"
416
+ PROJECT_NAME="$(basename "$REPO_ROOT")"
417
+ CONTAINER_NAME="codegraph-mcp-$PROJECT_NAME"
418
+ CACHE_DIR="$REPO_ROOT/.codegraph"
419
+
420
+ # Deterministically generate a port based on the repository path
421
+ PORT=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}' | tr -d 'a-f' | cut -c1-4)
422
+ # Ensure port is > 1024
423
+ PORT=$(( (PORT % 60000) + 1025 ))
424
+
425
+ if ! docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
426
+ echo "\u{1F680} Starting CodeGraph MCP Server on port $PORT..."
427
+
428
+ # Ensure the container isn't lingering in a stopped state
429
+ docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
430
+
431
+ # Run the true Colby McHenry CodeGraph Docker image
432
+ # which we built locally as colbymchenry-codegraph-mcp:latest
433
+ docker run -d \\
434
+ --name "$CONTAINER_NAME" \\
435
+ --restart unless-stopped \\
436
+ -p "$PORT:8045" \\
437
+ -v "$REPO_ROOT:/repo" \\
438
+ colbymchenry-codegraph-mcp:latest >/dev/null
439
+
440
+ echo "\u2705 CodeGraph MCP running in background. SSE URL: http://localhost:$PORT/sse"
441
+ fi
442
+
443
+ # Run init inside the container to ensure the index is bootstrapped.
444
+ # We run this using the standard codegraph CLI inside the container
445
+ docker exec "$CONTAINER_NAME" codegraph init -i /repo >/dev/null 2>&1 || true
446
+
447
+ # Wire up the MCP server to local agents
448
+ WIRE_SCRIPT="$(dirname "$0")/codegraph-wire.sh"
449
+ if [ -x "$WIRE_SCRIPT" ]; then
450
+ "$WIRE_SCRIPT"
451
+ fi
452
+ `;
453
+ this.writeFile(filePath, content);
454
+ if (!this.context.dryRun) {
455
+ chmodSync(join3(this.context.targetDir, filePath), 493);
456
+ }
457
+ return {
458
+ success: true,
459
+ message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
460
+ filePath
461
+ };
462
+ }
463
+ };
464
+
465
+ // src/commands/AddMiseCodegraphWireScript.ts
466
+ import { chmodSync as chmodSync2 } from "fs";
467
+ import { join as join4 } from "path";
468
+ var AddMiseCodegraphWireScript = class extends Command {
469
+ async invoke() {
470
+ const filePath = ".mise/scripts/codegraph-wire.sh";
471
+ if (this.fileExists(filePath) && !this.context.force) {
472
+ return {
473
+ success: false,
474
+ message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph-wire.sh already exists"),
475
+ filePath
476
+ };
477
+ }
478
+ const content = `#!/usr/bin/env bash
479
+ # Mise enter hook: Auto-wire CodeGraph MCP Server to local agents.
480
+ # Detects Claude Code, Codex, Gemini, Kimi, and OpenCode and configures
481
+ # them to use the local SSE endpoint for the current project.
419
482
 
420
483
  set -euo pipefail
421
484
 
422
485
  REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
423
- PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
424
- mkdir -p "$PROJECT_BIN_DIR"
425
-
426
- # Install the CodeGraph CLI into the project-local bin directory.
427
- install_codegraph() {
428
- echo "[mise] codegraph not found. Installing non-interactively..."
429
- export CODEGRAPH_BIN_DIR="$PROJECT_BIN_DIR"
430
- curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
431
-
432
- if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
433
- export PATH="$PROJECT_BIN_DIR:$PATH"
434
- else
435
- echo "[mise] codegraph install did not place a binary at $PROJECT_BIN_DIR/codegraph" >&2
436
- return 1
486
+
487
+ # Compute the same port as the container script
488
+ PORT_HASH=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}')
489
+ PORT_DEC=$(printf "%d" "0x\${PORT_HASH:0:4}")
490
+ PORT=$(( 8045 + (PORT_DEC % 955) ))
491
+
492
+ SSE_URL="http://localhost:$PORT/sse"
493
+
494
+ inject_sse() {
495
+ local target="$1"
496
+ local agent="$2"
497
+
498
+ if [ ! -f "$target" ]; then
499
+ mkdir -p "$(dirname "$target")"
500
+ echo '{"mcpServers": {}}' > "$target"
501
+ fi
502
+
503
+ # Ensure the file is valid JSON (fail gracefully if it's garbled)
504
+ if ! jq . "$target" >/dev/null 2>&1; then
505
+ echo "[mise] WARNING: $target is not valid JSON, skipping $agent wiring" >&2
506
+ return
437
507
  fi
508
+
509
+ # Inject or update the codegraph server
510
+ jq --arg url "$SSE_URL" '.mcpServers.codegraph = {"type": "sse", "url": $url}' "$target" > "$target.tmp" && mv "$target.tmp" "$target"
511
+ echo "[mise] Wired CodeGraph SSE for $agent -> $target"
438
512
  }
439
513
 
440
- # Ensure a codegraph binary is available on PATH.
441
- ensure_codegraph() {
442
- if command -v codegraph >/dev/null 2>&1; then
443
- return 0
444
- fi
514
+ # 1. Claude Code (Project-scoped)
515
+ inject_sse "$REPO_ROOT/.claude.json" "Claude Code"
445
516
 
446
- # Check the project-local bin dir first (previous install from this hook).
447
- if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
448
- export PATH="$PROJECT_BIN_DIR:$PATH"
449
- return 0
450
- fi
517
+ # 2. Codex (Global)
518
+ inject_sse "$HOME/.codex/mcp.json" "Codex"
451
519
 
452
- # Check typical user-level install locations before fetching anything.
453
- for d in "$HOME/.local/bin" "$HOME/.codegraph/current/bin"; do
454
- if [ -x "$d/codegraph" ]; then
455
- export PATH="$d:$PATH"
456
- return 0
457
- fi
458
- done
520
+ # 3. Gemini (Global)
521
+ inject_sse "$HOME/.gemini/config/mcp.json" "Gemini"
459
522
 
460
- install_codegraph
461
- }
523
+ # 4. Kimi (Global)
524
+ inject_sse "$HOME/.kimi-code/mcp.json" "Kimi"
462
525
 
463
- # Attempt to initialize the project graph. If the command is missing, install
464
- # it and retry once.
465
- init_project() {
466
- local err_file
467
- err_file="$(mktemp)"
468
- trap 'rm -f "$err_file"' RETURN
526
+ # 5. OpenCode (Global)
527
+ inject_sse "$HOME/.opencode/mcp.json" "OpenCode"
469
528
 
470
- if codegraph init -i "$REPO_ROOT" 2>"$err_file"; then
471
- return 0
472
- fi
529
+ # 6. Cursor (Global)
530
+ inject_sse "$HOME/.cursor/mcp.json" "Cursor"
473
531
 
474
- # If the failure looks like a missing binary, install and retry.
475
- if grep -qiE 'command not found|not installed|No such file|executable file not found' "$err_file" 2>/dev/null; then
476
- ensure_codegraph
477
- codegraph init -i "$REPO_ROOT"
478
- return 0
479
- fi
532
+ # 7. VSCode native MCP (Global)
533
+ inject_sse "$HOME/.vscode/mcp.json" "VSCode"
480
534
 
481
- cat "$err_file" >&2
482
- return 1
483
- }
535
+ # 8. Cline (VSCode extension)
536
+ inject_sse "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" "Cline"
484
537
 
485
- init_project
538
+ # 9. Roo (VSCode extension)
539
+ inject_sse "$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json" "Roo Code"
486
540
  `;
487
541
  this.writeFile(filePath, content);
488
542
  if (!this.context.dryRun) {
489
- chmodSync(join3(this.context.targetDir, filePath), 493);
543
+ chmodSync2(join4(this.context.targetDir, filePath), 493);
490
544
  }
491
545
  return {
492
546
  success: true,
493
- message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
547
+ message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph-wire.sh" : "\u2705 Created .mise/scripts/codegraph-wire.sh"),
494
548
  filePath
495
549
  };
496
550
  }
497
551
  };
498
552
 
553
+ // src/commands/WireMiseOpInject.ts
554
+ import { join as join5 } from "node:path";
555
+ import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync3, chmodSync as chmodSync3 } from "node:fs";
556
+ var WireMiseOpInject = class _WireMiseOpInject extends Command {
557
+ static MARKER = "# pjangler:op-inject";
558
+ static CR = "{{config_root}}";
559
+ async invoke() {
560
+ const scriptPath = ".mise/scripts/inject-op-secrets.sh";
561
+ const fullScriptPath = join5(this.context.targetDir, scriptPath);
562
+ const scriptContent = `#!/usr/bin/env bash
563
+ # Resolve 1Password \`op://\` references in .env.op into .env.secrets, which mise
564
+ # loads via \`_.file\`.
565
+ #
566
+ # Runs from the mise enter hook: fail-open and non-interactive.
567
+ # No .env.op means no 1Password, so this exits before spending anything.
568
+
569
+ set -euo pipefail
570
+
571
+ REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
572
+ SRC="$REPO_ROOT/.env.op"
573
+ OUT="$REPO_ROOT/.env.secrets"
574
+
575
+ TTL_HOURS="\${OP_INJECT_TTL_HOURS:-12}"
576
+
577
+ [[ -f "$SRC" ]] || exit 0
578
+ command -v op >/dev/null 2>&1 || exit 0
579
+
580
+ if [[ -f "$OUT" && "$SRC" -ot "$OUT" ]]; then
581
+ if [[ "$TTL_HOURS" != "0" ]] && [[ -z "$(find "$OUT" -mmin "+$((TTL_HOURS * 60))" 2>/dev/null)" ]]; then
582
+ exit 0
583
+ fi
584
+ fi
585
+
586
+ op account list >/dev/null 2>&1 || exit 0
587
+
588
+ tmp="$(mktemp "\${OUT}.XXXXXX")"
589
+ trap 'rm -f "$tmp"' EXIT
590
+ chmod 600 "$tmp"
591
+
592
+ if op inject -i "$SRC" -o "$tmp" --force >/dev/null 2>&1; then
593
+ mv "$tmp" "$OUT"
594
+ trap - EXIT
595
+ else
596
+ echo "mise: op inject from .env.op failed \u2014 $(basename "$OUT") unchanged." >&2
597
+ echo " Check 'op signin', or that .env.op's op:// items are readable." >&2
598
+ echo " To disable: rm .env.op" >&2
599
+ fi
600
+ `;
601
+ this.writeFile(scriptPath, scriptContent);
602
+ if (!this.context.dryRun) {
603
+ chmodSync3(fullScriptPath, 493);
604
+ }
605
+ const misePath = join5(this.context.targetDir, "mise.toml");
606
+ if (!existsSync3(misePath)) {
607
+ return {
608
+ success: false,
609
+ message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj init mise` first, then re-run."
610
+ };
611
+ }
612
+ let content = readFileSync(misePath, "utf8");
613
+ if (content.includes(_WireMiseOpInject.MARKER)) {
614
+ return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for op-inject") };
615
+ }
616
+ const cr = _WireMiseOpInject.CR;
617
+ const envRe = /\[env\]\s*\n([\s\S]*?(?=\n\[|$))/;
618
+ if (envRe.test(content)) {
619
+ content = content.replace(envRe, (m) => {
620
+ let block = m;
621
+ if (!block.includes("_.file")) {
622
+ block += `
623
+ _.file = ['.env', '.env.secrets']
624
+ `;
625
+ } else if (!block.includes(".env.secrets")) {
626
+ block = block.replace(/(_\.file\s*=\s*\[)([^\]]*?)(\])/, (m2, prefix, files, suffix) => {
627
+ const added = files.trim() ? `${files}, '.env.secrets'` : `'.env.secrets'`;
628
+ return `${prefix}${added}${suffix}`;
629
+ });
630
+ }
631
+ return block;
632
+ });
633
+ } else {
634
+ const envBlock = `[env]
635
+ _.file = ['.env', '.env.secrets']
636
+
637
+ `;
638
+ if (content.includes("[tools]")) {
639
+ content = content.replace(/(\[tools\][\s\S]*?(?=\n\[|$))/, `$1
640
+ ${envBlock}`);
641
+ } else {
642
+ content = envBlock + content;
643
+ }
644
+ }
645
+ const enterAdds = ` '${cr}/.mise/scripts/inject-op-secrets.sh',`;
646
+ const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
647
+ if (enterRe.test(content)) {
648
+ content = content.replace(enterRe, (_m, head, close) => {
649
+ const sep = /[,[]\s*$/.test(head) ? "" : ",";
650
+ return `${head}${sep}
651
+ ${enterAdds}${close}`;
652
+ });
653
+ } else {
654
+ if (content.includes("[hooks]")) {
655
+ content = content.replace(/\[hooks\]/, `[hooks]
656
+ enter = [
657
+ ${enterAdds}
658
+ ]`);
659
+ } else {
660
+ content += `
661
+ [hooks]
662
+ enter = [
663
+ ${enterAdds}
664
+ ]
665
+ `;
666
+ }
667
+ }
668
+ const appended = [
669
+ "",
670
+ _WireMiseOpInject.MARKER,
671
+ "[tasks.secrets-inject]",
672
+ 'description = "Re-resolve .env.op secrets from 1Password into .env.secrets"',
673
+ `run = "OP_INJECT_TTL_HOURS=0 .mise/scripts/inject-op-secrets.sh"`,
674
+ _WireMiseOpInject.MARKER + ":end",
675
+ ""
676
+ ].join("\n");
677
+ content = content.replace(/\n*$/, "\n") + appended;
678
+ if (!this.context.dryRun) writeFileSync3(misePath, content);
679
+ return {
680
+ success: true,
681
+ message: this.formatMessage("\u2705 Wired mise.toml for op-inject (_.file, [hooks] enter, tasks.secrets-inject)")
682
+ };
683
+ }
684
+ };
685
+
499
686
  // src/recipes/MiseRecipe.ts
500
687
  var MiseRecipe = class extends Recipe {
501
688
  constructor(context) {
502
689
  super(context);
503
- this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
690
+ this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript).addIngredient(AddMiseCodegraphWireScript).addIngredient(WireMiseOpInject);
504
691
  }
505
692
  printNextSteps() {
506
693
  console.log("\u{1F389} Mise subsystem initialized successfully!");
@@ -728,12 +915,12 @@ var NodeRecipe = class extends Recipe {
728
915
  };
729
916
 
730
917
  // src/commands/hermes/PromptForAgentConfig.ts
731
- import { basename, join as join4 } from "node:path";
732
- import { readFileSync } from "node:fs";
918
+ import { basename, join as join6 } from "node:path";
919
+ import { readFileSync as readFileSync2 } from "node:fs";
733
920
  import * as p from "@clack/prompts";
734
921
  function detectTicketProvider(targetDir) {
735
922
  try {
736
- const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
923
+ const t = JSON.parse(readFileSync2(join6(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
737
924
  return t === "plane" || t === "trello" ? t : void 0;
738
925
  } catch {
739
926
  return void 0;
@@ -791,8 +978,8 @@ var PromptForAgentConfig = class extends Command {
791
978
  // src/commands/hermes/RunCopierTemplate.ts
792
979
  import { spawnSync } from "node:child_process";
793
980
  import { homedir as homedir2 } from "node:os";
794
- import { join as join5, dirname as dirname3 } from "node:path";
795
- import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
981
+ import { join as join7, dirname as dirname3 } from "node:path";
982
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
796
983
  import { fileURLToPath } from "node:url";
797
984
  import * as p2 from "@clack/prompts";
798
985
  function resolveVendoredTemplate(name) {
@@ -803,8 +990,8 @@ function resolveVendoredTemplate(name) {
803
990
  return void 0;
804
991
  }
805
992
  for (let i = 0; i < 8; i++) {
806
- const candidate = join5(dir, "templates", name);
807
- if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
993
+ const candidate = join7(dir, "templates", name);
994
+ if (existsSync4(join7(candidate, "copier.yml"))) return candidate;
808
995
  const parent = dirname3(dir);
809
996
  if (parent === dir) break;
810
997
  dir = parent;
@@ -823,7 +1010,7 @@ var RunCopierTemplate = class extends Command {
823
1010
  message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
824
1011
  };
825
1012
  }
826
- const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
1013
+ const roleDir = join7(ctx.targetDir, "agents", "hermes", role);
827
1014
  ctx.roleDir = roleDir;
828
1015
  ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
829
1016
  const which = spawnSync("which", ["copier"], { encoding: "utf8" });
@@ -833,7 +1020,7 @@ var RunCopierTemplate = class extends Command {
833
1020
  message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
834
1021
  };
835
1022
  }
836
- if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
1023
+ if (existsSync4(join7(roleDir, "role.yaml")) && !ctx.force) {
837
1024
  if (ctx.yes) {
838
1025
  ctx.force = true;
839
1026
  } else {
@@ -860,9 +1047,9 @@ var RunCopierTemplate = class extends Command {
860
1047
  SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
861
1048
  SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
862
1049
  };
863
- const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
1050
+ const LOCAL_TEMPLATE = join7(homedir2(), "code", "hermes-agent-template");
864
1051
  const vendored = resolveVendoredTemplate("hermes-agent");
865
- const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
1052
+ const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join7(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
866
1053
  const args = [
867
1054
  "copy",
868
1055
  templateSrc,
@@ -893,7 +1080,7 @@ var RunCopierTemplate = class extends Command {
893
1080
  message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
894
1081
  };
895
1082
  }
896
- mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
1083
+ mkdirSync3(join7(ctx.targetDir, "agents", "hermes"), { recursive: true });
897
1084
  const spinner4 = p2.spinner();
898
1085
  spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
899
1086
  const result = spawnSync("copier", args, {
@@ -917,14 +1104,14 @@ var RunCopierTemplate = class extends Command {
917
1104
  };
918
1105
 
919
1106
  // src/commands/hermes/UntrackHermesRuntimes.ts
920
- import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3, readdirSync } from "fs";
921
- import { join as join6 } from "path";
1107
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4, readdirSync } from "fs";
1108
+ import { join as join8 } from "path";
922
1109
  import { spawnSync as spawnSync2 } from "node:child_process";
923
1110
  var UntrackHermesRuntimes = class extends Command {
924
1111
  async invoke() {
925
1112
  const targetDir = this.context.targetDir;
926
- const rolesDir = join6(targetDir, "agents", "hermes");
927
- if (!existsSync4(rolesDir)) {
1113
+ const rolesDir = join8(targetDir, "agents", "hermes");
1114
+ if (!existsSync5(rolesDir)) {
928
1115
  return {
929
1116
  success: true,
930
1117
  message: "No Hermes agents found (no agents/hermes directory)."
@@ -940,9 +1127,9 @@ var UntrackHermesRuntimes = class extends Command {
940
1127
  let modifiedAny = false;
941
1128
  const details = [];
942
1129
  for (const role of roles) {
943
- const roleDir = join6("agents", "hermes", role);
944
- const runtimePath = join6(roleDir, "runtime");
945
- const gitignorePath = join6(roleDir, ".gitignore");
1130
+ const roleDir = join8("agents", "hermes", role);
1131
+ const runtimePath = join8(roleDir, "runtime");
1132
+ const gitignorePath = join8(roleDir, ".gitignore");
946
1133
  let isTracked = false;
947
1134
  const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
948
1135
  cwd: targetDir,
@@ -952,9 +1139,9 @@ var UntrackHermesRuntimes = class extends Command {
952
1139
  isTracked = true;
953
1140
  }
954
1141
  let isIgnored = false;
955
- const fullGitignorePath = join6(targetDir, gitignorePath);
956
- if (existsSync4(fullGitignorePath)) {
957
- const content = readFileSync2(fullGitignorePath, "utf8");
1142
+ const fullGitignorePath = join8(targetDir, gitignorePath);
1143
+ if (existsSync5(fullGitignorePath)) {
1144
+ const content = readFileSync3(fullGitignorePath, "utf8");
958
1145
  const lines = content.split(/\r?\n/).map((line) => line.trim());
959
1146
  isIgnored = lines.includes("runtime/") || lines.includes("runtime");
960
1147
  }
@@ -979,14 +1166,14 @@ var UntrackHermesRuntimes = class extends Command {
979
1166
  details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
980
1167
  if (!this.context.dryRun) {
981
1168
  let content = "";
982
- if (existsSync4(fullGitignorePath)) {
983
- content = readFileSync2(fullGitignorePath, "utf8");
1169
+ if (existsSync5(fullGitignorePath)) {
1170
+ content = readFileSync3(fullGitignorePath, "utf8");
984
1171
  }
985
1172
  if (content && !content.endsWith("\n")) {
986
1173
  content += "\n";
987
1174
  }
988
1175
  content += "runtime/\n";
989
- writeFileSync3(fullGitignorePath, content, "utf8");
1176
+ writeFileSync4(fullGitignorePath, content, "utf8");
990
1177
  }
991
1178
  }
992
1179
  }
@@ -1008,8 +1195,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
1008
1195
 
1009
1196
  // src/commands/hermes/WireTelegram.ts
1010
1197
  import { spawnSync as spawnSync3 } from "node:child_process";
1011
- import { join as join7 } from "node:path";
1012
- import { existsSync as existsSync5, unlinkSync } from "node:fs";
1198
+ import { join as join9 } from "node:path";
1199
+ import { existsSync as existsSync6, unlinkSync } from "node:fs";
1013
1200
  import * as p3 from "@clack/prompts";
1014
1201
  var WireTelegram = class extends Command {
1015
1202
  async invoke() {
@@ -1097,15 +1284,15 @@ var WireTelegram = class extends Command {
1097
1284
  if (p3.isCancel(allowedAnswer)) {
1098
1285
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
1099
1286
  }
1100
- const script = join7(roleDir, ".scripts", "30-telegram.sh");
1101
- if (!existsSync5(script)) {
1287
+ const script = join9(roleDir, ".scripts", "30-telegram.sh");
1288
+ if (!existsSync6(script)) {
1102
1289
  return {
1103
1290
  success: false,
1104
1291
  message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
1105
1292
  };
1106
1293
  }
1107
- const marker = join7(roleDir, ".scripts", ".done-30-telegram");
1108
- if (existsSync5(marker)) unlinkSync(marker);
1294
+ const marker = join9(roleDir, ".scripts", ".done-30-telegram");
1295
+ if (existsSync6(marker)) unlinkSync(marker);
1109
1296
  const spinner4 = p3.spinner();
1110
1297
  spinner4.start("Verifying token + wiring profile");
1111
1298
  const result = spawnSync3("bash", [script], {
@@ -1132,8 +1319,8 @@ function cap(s) {
1132
1319
 
1133
1320
  // src/commands/hermes/WireEmail.ts
1134
1321
  import { spawnSync as spawnSync4 } from "node:child_process";
1135
- import { join as join8 } from "node:path";
1136
- import { existsSync as existsSync6, unlinkSync as unlinkSync2 } from "node:fs";
1322
+ import { join as join10 } from "node:path";
1323
+ import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
1137
1324
  import * as p4 from "@clack/prompts";
1138
1325
  var WireEmail = class extends Command {
1139
1326
  async invoke() {
@@ -1148,8 +1335,8 @@ var WireEmail = class extends Command {
1148
1335
  if (!targetRepo || !role || !roleDir) {
1149
1336
  return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
1150
1337
  }
1151
- const script = join8(roleDir, ".scripts", "50-email.sh");
1152
- if (!existsSync6(script)) {
1338
+ const script = join10(roleDir, ".scripts", "50-email.sh");
1339
+ if (!existsSync7(script)) {
1153
1340
  return { success: false, message: `\u2717 ${script} not found` };
1154
1341
  }
1155
1342
  let token = process.env.CF_EMAIL_ROUTING_TOKEN;
@@ -1211,8 +1398,8 @@ var WireEmail = class extends Command {
1211
1398
  }
1212
1399
  }
1213
1400
  }
1214
- const marker = join8(roleDir, ".scripts", ".done-50-email");
1215
- if (existsSync6(marker)) unlinkSync2(marker);
1401
+ const marker = join10(roleDir, ".scripts", ".done-50-email");
1402
+ if (existsSync7(marker)) unlinkSync2(marker);
1216
1403
  const spinner4 = p4.spinner();
1217
1404
  spinner4.start("Creating Cloudflare Email Routing rule");
1218
1405
  const result = spawnSync4("bash", [script], {
@@ -1299,15 +1486,15 @@ var HermesAgentRecipe = class extends Recipe {
1299
1486
 
1300
1487
  // src/commands/AgentHooksCommands.ts
1301
1488
  import { homedir as homedir4 } from "node:os";
1302
- import { join as join10, dirname as dirname5 } from "node:path";
1303
- import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
1489
+ import { join as join12, dirname as dirname5 } from "node:path";
1490
+ import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
1304
1491
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1305
1492
 
1306
1493
  // src/project/index.ts
1307
1494
  import { spawnSync as spawnSync5 } from "node:child_process";
1308
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
1495
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
1309
1496
  import { homedir as homedir3 } from "node:os";
1310
- import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
1497
+ import { basename as basename2, delimiter, dirname as dirname4, join as join11, resolve } from "node:path";
1311
1498
  import YAML2 from "yaml";
1312
1499
 
1313
1500
  // src/project/RegistryStore.ts
@@ -1542,18 +1729,18 @@ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
1542
1729
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
1543
1730
  var DEFAULT_SOURCE_SKILL_ROOTS = [
1544
1731
  "/home/delorenj/code/skillex/all-skills",
1545
- join9(homedir3(), ".agents", "skills"),
1546
- join9(homedir3(), ".codex", "skills")
1732
+ join11(homedir3(), ".agents", "skills"),
1733
+ join11(homedir3(), ".codex", "skills")
1547
1734
  ];
1548
1735
  function projectRegistryPath(env2 = process.env) {
1549
- return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir3(), ".config", "pjangler", "projects.yaml"));
1736
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir3(), ".config", "pjangler", "projects.yaml"));
1550
1737
  }
1551
1738
  function emptyProjectRegistry() {
1552
1739
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
1553
1740
  }
1554
1741
  function loadProjectRegistry(path = projectRegistryPath()) {
1555
- if (!existsSync7(path)) return emptyProjectRegistry();
1556
- const raw = YAML2.parse(readFileSync3(path, "utf8"));
1742
+ if (!existsSync8(path)) return emptyProjectRegistry();
1743
+ const raw = YAML2.parse(readFileSync4(path, "utf8"));
1557
1744
  if (raw == null) return emptyProjectRegistry();
1558
1745
  if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
1559
1746
  const registry = raw;
@@ -1568,7 +1755,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1568
1755
  validateProjectRegistry(registry);
1569
1756
  mkdirSync4(dirname4(path), { recursive: true });
1570
1757
  const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
1571
- writeFileSync4(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
1758
+ writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
1572
1759
  renameSync(temp, path);
1573
1760
  }
1574
1761
  function validateProjectRegistry(registry) {
@@ -1650,7 +1837,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
1650
1837
  const override = env2.PJ_AGENT_HOOKS_LAYER;
1651
1838
  if (override === "0" || override === "false") return false;
1652
1839
  if (override === "1" || override === "true") return true;
1653
- return !existsSync7(join9(homedir3(), ".agents", "hooks"));
1840
+ return !existsSync8(join11(homedir3(), ".agents", "hooks"));
1654
1841
  }
1655
1842
  function jsonStable(value) {
1656
1843
  return JSON.stringify(value);
@@ -1681,12 +1868,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1681
1868
  if (!sourceSkill) return void 0;
1682
1869
  const expanded = expandHome(sourceSkill);
1683
1870
  const direct = resolve(expanded);
1684
- if (existsSync7(direct)) return direct;
1871
+ if (existsSync8(direct)) return direct;
1685
1872
  const name = basename2(sourceSkill);
1686
1873
  const roots = sourceSkillRoots(env2);
1687
1874
  for (const root of roots) {
1688
- const candidate = join9(root, name);
1689
- if (existsSync7(candidate)) return candidate;
1875
+ const candidate = join11(root, name);
1876
+ if (existsSync8(candidate)) return candidate;
1690
1877
  }
1691
1878
  const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
1692
1879
  const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
@@ -1767,7 +1954,7 @@ function planProjectInit(input) {
1767
1954
  }));
1768
1955
  }
1769
1956
  actions.push(
1770
- { kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
1957
+ { kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
1771
1958
  {
1772
1959
  kind: "ticket-provider.create-or-link",
1773
1960
  enabled: live,
@@ -1820,7 +2007,7 @@ async function executeProjectInitPlan(plan) {
1820
2007
  }
1821
2008
  if (result.status !== 0) {
1822
2009
  errors.push(`copier exited with status ${result.status ?? "unknown"}`);
1823
- if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
2010
+ if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
1824
2011
  break;
1825
2012
  }
1826
2013
  changedFiles.push(action.targetDir);
@@ -1828,9 +2015,9 @@ async function executeProjectInitPlan(plan) {
1828
2015
  mkdirSync4(dirname4(action.path), { recursive: true });
1829
2016
  const next = `${JSON.stringify(action.manifest, null, 2)}
1830
2017
  `;
1831
- const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
2018
+ const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
1832
2019
  if (current !== next) {
1833
- writeFileSync4(action.path, next, "utf8");
2020
+ writeFileSync5(action.path, next, "utf8");
1834
2021
  changedFiles.push(action.path);
1835
2022
  }
1836
2023
  } else if (action.kind === "registry.upsert") {
@@ -1933,16 +2120,16 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
1933
2120
  const registry = loadProjectRegistry(registryPath2);
1934
2121
  const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
1935
2122
  for (const [projectSlug, project] of projects) {
1936
- if (!existsSync7(project.repo_path)) {
2123
+ if (!existsSync8(project.repo_path)) {
1937
2124
  issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
1938
2125
  } else if (!statSync(project.repo_path).isDirectory()) {
1939
2126
  issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
1940
2127
  } else {
1941
- const manifestPath = join9(project.repo_path, ".project.json");
1942
- if (!existsSync7(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
2128
+ const manifestPath = join11(project.repo_path, ".project.json");
2129
+ if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
1943
2130
  }
1944
2131
  for (const artifact of project.source_artifacts) {
1945
- if (artifact.path && !existsSync7(artifact.path)) {
2132
+ if (artifact.path && !existsSync8(artifact.path)) {
1946
2133
  issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
1947
2134
  }
1948
2135
  }
@@ -1955,7 +2142,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
1955
2142
  };
1956
2143
  }
1957
2144
  function buildCommonProjectCopierAction(input) {
1958
- const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
2145
+ const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
1959
2146
  const data = {
1960
2147
  project_name: input.projectName,
1961
2148
  project_description: input.projectDescription ?? "",
@@ -1984,7 +2171,7 @@ function buildCommonProjectCopierAction(input) {
1984
2171
  function resolvePjanglerRoot() {
1985
2172
  let dir = dirname4(new URL(import.meta.url).pathname);
1986
2173
  while (dir !== dirname4(dir)) {
1987
- if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
2174
+ if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
1988
2175
  dir = dirname4(dir);
1989
2176
  }
1990
2177
  return resolve(process.cwd());
@@ -2016,7 +2203,7 @@ function validateProjectRecord(project, key) {
2016
2203
  }
2017
2204
  function expandHome(path) {
2018
2205
  if (path === "~") return homedir3();
2019
- if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
2206
+ if (path.startsWith("~/")) return join11(homedir3(), path.slice(2));
2020
2207
  return path;
2021
2208
  }
2022
2209
  function isRecord(value) {
@@ -2033,16 +2220,16 @@ function resolveTemplateRoot() {
2033
2220
  try {
2034
2221
  let dir = dirname5(fileURLToPath2(import.meta.url));
2035
2222
  for (let i = 0; i < 8; i++) {
2036
- candidates.push(join10(dir, "templates", "commonproject", "template"));
2223
+ candidates.push(join12(dir, "templates", "commonproject", "template"));
2037
2224
  const parent = dirname5(dir);
2038
2225
  if (parent === dir) break;
2039
2226
  dir = parent;
2040
2227
  }
2041
2228
  } catch {
2042
2229
  }
2043
- candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
2230
+ candidates.push(join12(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
2044
2231
  for (const c of candidates) {
2045
- if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
2232
+ if (existsSync9(join12(c, ".agents", "hooks", "hooks.master.json"))) return c;
2046
2233
  }
2047
2234
  throw new Error(
2048
2235
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -2061,18 +2248,17 @@ var CopyAgentHooksTree = class extends Command {
2061
2248
  }
2062
2249
  const items = [
2063
2250
  { rel: ".agents/hooks", dir: true },
2251
+ { rel: ".agents/skills.json", dir: false },
2064
2252
  { rel: ".agents/local.example.json", dir: false },
2065
- { rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
2066
- { rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
2067
2253
  { rel: ".mise/scripts/hindsight-setup.sh", dir: false }
2068
2254
  ];
2069
2255
  const created = [];
2070
2256
  const skipped = [];
2071
2257
  for (const { rel, dir } of items) {
2072
- const src = join10(templateRoot, rel);
2073
- const dest = join10(this.context.targetDir, rel);
2074
- if (!existsSync8(src)) continue;
2075
- if (existsSync8(dest) && !this.context.force) {
2258
+ const src = join12(templateRoot, rel);
2259
+ const dest = join12(this.context.targetDir, rel);
2260
+ if (!existsSync9(src)) continue;
2261
+ if (existsSync9(dest) && !this.context.force) {
2076
2262
  skipped.push(rel);
2077
2263
  continue;
2078
2264
  }
@@ -2098,25 +2284,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
2098
2284
  if (!resolveAgentHooksLayer()) {
2099
2285
  return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
2100
2286
  }
2101
- const misePath = join10(this.context.targetDir, "mise.toml");
2102
- if (!existsSync8(misePath)) {
2287
+ const misePath = join12(this.context.targetDir, "mise.toml");
2288
+ if (!existsSync9(misePath)) {
2103
2289
  return {
2104
2290
  success: false,
2105
2291
  message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
2106
2292
  };
2107
2293
  }
2108
- let content = readFileSync4(misePath, "utf8");
2294
+ let content = readFileSync5(misePath, "utf8");
2109
2295
  if (content.includes(_WireMiseAgentHooks.MARKER)) {
2110
2296
  return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
2111
2297
  }
2112
2298
  const cr = _WireMiseAgentHooks.CR;
2113
- const enterAdds = [
2114
- ` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
2115
- ` "${cr}/.agents/hooks/sync.py --install --quiet",`
2116
- ].join("\n");
2299
+ const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
2117
2300
  const leaveBlock = [
2118
2301
  "leave = [",
2119
- ` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
2120
2302
  ` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
2121
2303
  "]"
2122
2304
  ].join("\n");
@@ -2133,7 +2315,6 @@ ${enterAdds}${close}`;
2133
2315
  content = content.replace(leaveRe, (_m, head, close) => {
2134
2316
  const sep = /[,[]\s*$/.test(head) ? "" : ",";
2135
2317
  return `${head}${sep}
2136
- "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
2137
2318
  "${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
2138
2319
  });
2139
2320
  } else {
@@ -2146,6 +2327,14 @@ ${leaveBlock}`);
2146
2327
  "",
2147
2328
  _WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
2148
2329
  "[[watch_files]]",
2330
+ 'patterns = [".agents/skills.json"]',
2331
+ 'task = "skills-sync"',
2332
+ "",
2333
+ "[tasks.skills-sync]",
2334
+ 'description = "Sync skills from manifest to local CLI dirs"',
2335
+ 'run = "sync-skills.py --scope project"',
2336
+ "",
2337
+ "[[watch_files]]",
2149
2338
  'patterns = [".agents/hooks/hooks.master.json"]',
2150
2339
  'task = "hooks-sync"',
2151
2340
  "",
@@ -2161,18 +2350,6 @@ ${leaveBlock}`);
2161
2350
  'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
2162
2351
  `run = "${cr}/.agents/hooks/sync.py --uninstall"`,
2163
2352
  "",
2164
- "[tasks.link-project-skills-to-clis]",
2165
- 'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
2166
- `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
2167
- "",
2168
- "[tasks.unlink-project-skills-from-clis]",
2169
- 'description = "Remove project skill symlinks from shared per-CLI dirs"',
2170
- `run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
2171
- "",
2172
- "[tasks.skills-relink]",
2173
- 'description = "Re-fan the project skill set to all CLIs"',
2174
- `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
2175
- "",
2176
2353
  "[tasks.hindsight-setup]",
2177
2354
  `description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
2178
2355
  `run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
@@ -2181,7 +2358,7 @@ ${leaveBlock}`);
2181
2358
  ""
2182
2359
  ].join("\n");
2183
2360
  content = content.replace(/\n*$/, "\n") + appended;
2184
- if (!this.context.dryRun) writeFileSync5(misePath, content);
2361
+ if (!this.context.dryRun) writeFileSync6(misePath, content);
2185
2362
  if (wiredHooks) {
2186
2363
  return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
2187
2364
  }
@@ -2190,8 +2367,8 @@ ${leaveBlock}`);
2190
2367
  message: this.formatMessage(
2191
2368
  `\u2705 Added agent-hooks tasks to mise.toml.
2192
2369
  \u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
2193
- enter += "${cr}/.mise/scripts/link-project-skills-to-clis.sh", "${cr}/.agents/hooks/sync.py --install --quiet"
2194
- leave += "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh", "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
2370
+ enter += "sync-skills.py --scope project", "${cr}/.agents/hooks/sync.py --install --quiet"
2371
+ leave += "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
2195
2372
  )
2196
2373
  };
2197
2374
  }
@@ -2206,10 +2383,25 @@ var AgentHooksRecipe = class extends Recipe {
2206
2383
  printNextSteps() {
2207
2384
  console.log("\u{1FA9D} Agent-hooks layer installed!");
2208
2385
  console.log(" Next steps:");
2209
- console.log(" 1. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
2210
- console.log(" 2. git add .claude/settings.json .agents/hooks && commit (codex/kimi/hermes are per-dev)");
2211
- console.log(" 3. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
2212
- console.log(` 4. If you run a global agent system: echo '{"skills":{"defer_to_global":true}}' > .agents/local.json`);
2386
+ console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
2387
+ console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
2388
+ console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
2389
+ console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
2390
+ console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
2391
+ }
2392
+ };
2393
+
2394
+ // src/recipes/MiseOpInjectRecipe.ts
2395
+ var MiseOpInjectRecipe = class extends Recipe {
2396
+ constructor(context) {
2397
+ super(context);
2398
+ this.addIngredient(WireMiseOpInject);
2399
+ }
2400
+ printNextSteps() {
2401
+ console.log("\u{1F389} Wired up .env.op 1Password resolution via mise!");
2402
+ console.log(" Next steps:");
2403
+ console.log(" 1. Create .env.op with your op:// secret references");
2404
+ console.log(" 2. Run `mise run secrets-inject` or simply cd out and back in to trigger the hook");
2213
2405
  }
2214
2406
  };
2215
2407
 
@@ -2252,6 +2444,12 @@ var RECIPE_REGISTRY = {
2252
2444
  description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
2253
2445
  class: AgentHooksRecipe,
2254
2446
  commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
2447
+ },
2448
+ "mise-op-inject": {
2449
+ name: "mise-op-inject",
2450
+ description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
2451
+ class: MiseOpInjectRecipe,
2452
+ commands: ["WireMiseOpInject"]
2255
2453
  }
2256
2454
  };
2257
2455
  var COMMAND_REGISTRY = {
@@ -2320,6 +2518,12 @@ var COMMAND_REGISTRY = {
2320
2518
  description: "Create .env.example file",
2321
2519
  group: "environment",
2322
2520
  class: AddDotenv
2521
+ },
2522
+ WireMiseOpInject: {
2523
+ name: "WireMiseOpInject",
2524
+ description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
2525
+ group: "mise",
2526
+ class: WireMiseOpInject
2323
2527
  }
2324
2528
  };
2325
2529
  function getRecipeNames() {
@@ -2354,29 +2558,39 @@ function createRecipe(name, context) {
2354
2558
  import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
2355
2559
 
2356
2560
  // src/parity/index.ts
2357
- import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync as chmodSync2, copyFileSync } from "node:fs";
2358
- import { basename as basename3, dirname as dirname6, join as join11, relative, resolve as resolve2 } from "node:path";
2561
+ import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync4, copyFileSync, rmSync } from "node:fs";
2562
+ import { basename as basename3, dirname as dirname6, join as join13, relative, resolve as resolve2 } from "node:path";
2359
2563
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2360
2564
  import { homedir as homedir5 } from "node:os";
2361
2565
  import { spawnSync as spawnSync6 } from "node:child_process";
2362
2566
  import YAML3 from "yaml";
2363
2567
  var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
2364
2568
  var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
2569
+ var SYNC_SKILLS_SCRIPT = "sync-skills.py --scope project";
2365
2570
  var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
2571
+ var SKILLS_REGISTRY_URL = "https://github.com/delorenj/skillex.git";
2366
2572
  var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
2367
2573
  # agent files to the main AGENTS.md file.
2368
2574
  #
2369
2575
  # TODO: Ensure this works for all levels of nesting.
2370
2576
  # i.e. All linked agent files MUST be siblings at
2371
2577
  # any given level of nesting.`;
2372
- var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
2578
+ var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT, SYNC_SKILLS_SCRIPT];
2373
2579
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
2374
2580
  patterns = ["AGENTS.md"]
2375
2581
  task = "link-agentfiles"
2376
2582
 
2583
+ [[watch_files]]
2584
+ patterns = [".agents/skills.json"]
2585
+ task = "skills-sync"
2586
+
2377
2587
  [tasks.link-agentfiles]
2378
2588
  description = "Symlink all agent files to AGENTS.md"
2379
- run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"`;
2589
+ run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
2590
+
2591
+ [tasks.skills-sync]
2592
+ description = "Sync skills from manifest to local CLI dirs"
2593
+ run = "sync-skills.py --scope project"`;
2380
2594
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
2381
2595
  [tasks."version"]
2382
2596
  description = "Print the current version (vX.Y.Z)"
@@ -2406,7 +2620,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
2406
2620
  function resolvePjanglerRoot2() {
2407
2621
  let dir = dirname6(fileURLToPath3(import.meta.url));
2408
2622
  while (dir !== dirname6(dir)) {
2409
- if (existsSync9(join11(dir, "package.json")) && existsSync9(join11(dir, "templates", "commonproject", "copier.yml"))) {
2623
+ if (existsSync10(join13(dir, "package.json")) && existsSync10(join13(dir, "templates", "commonproject", "copier.yml"))) {
2410
2624
  return dir;
2411
2625
  }
2412
2626
  dir = dirname6(dir);
@@ -2417,17 +2631,17 @@ function normalizeNewlines(value) {
2417
2631
  return value.replace(/\r\n/g, "\n");
2418
2632
  }
2419
2633
  function readText(path) {
2420
- return normalizeNewlines(readFileSync5(path, "utf8"));
2634
+ return normalizeNewlines(readFileSync6(path, "utf8"));
2421
2635
  }
2422
2636
  function safeReadText(path) {
2423
- return existsSync9(path) ? readText(path) : null;
2637
+ return existsSync10(path) ? readText(path) : null;
2424
2638
  }
2425
2639
  function ensureParent(path) {
2426
2640
  mkdirSync6(dirname6(path), { recursive: true });
2427
2641
  }
2428
2642
  function writeText(path, content) {
2429
2643
  ensureParent(path);
2430
- writeFileSync6(path, content);
2644
+ writeFileSync7(path, content);
2431
2645
  }
2432
2646
  function tryParseJson(text3) {
2433
2647
  if (!text3) return null;
@@ -2444,7 +2658,7 @@ function titleCaseSlug(slug) {
2444
2658
  return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2445
2659
  }
2446
2660
  function readSymlinkTarget(path) {
2447
- if (!existsSync9(path)) return null;
2661
+ if (!existsSync10(path)) return null;
2448
2662
  try {
2449
2663
  return readlinkSync(path);
2450
2664
  } catch {
@@ -2452,7 +2666,7 @@ function readSymlinkTarget(path) {
2452
2666
  }
2453
2667
  }
2454
2668
  function ensureSymlink(path, target, dryRun) {
2455
- if (existsSync9(path)) {
2669
+ if (existsSync10(path)) {
2456
2670
  const stat = lstatSync(path);
2457
2671
  if (stat.isSymbolicLink()) {
2458
2672
  const current = readSymlinkTarget(path);
@@ -2469,11 +2683,11 @@ function ensureSymlink(path, target, dryRun) {
2469
2683
  return { changed: true };
2470
2684
  }
2471
2685
  function bootstrapAgentsFile(repoRoot, dryRun) {
2472
- const agentsPath = join11(repoRoot, "AGENTS.md");
2473
- if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
2686
+ const agentsPath = join13(repoRoot, "AGENTS.md");
2687
+ if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
2474
2688
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2475
- const source = join11(repoRoot, file);
2476
- if (!existsSync9(source)) continue;
2689
+ const source = join13(repoRoot, file);
2690
+ if (!existsSync10(source)) continue;
2477
2691
  const stat = lstatSync(source);
2478
2692
  if (stat.isSymbolicLink()) continue;
2479
2693
  if (stat.isFile()) {
@@ -2482,8 +2696,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
2482
2696
  }
2483
2697
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
2484
2698
  }
2485
- const readmePath = join11(repoRoot, "README.md");
2486
- if (existsSync9(readmePath)) {
2699
+ const readmePath = join13(repoRoot, "README.md");
2700
+ if (existsSync10(readmePath)) {
2487
2701
  const stat = lstatSync(readmePath);
2488
2702
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
2489
2703
  if (!dryRun) copyFileSync(readmePath, agentsPath);
@@ -2522,12 +2736,12 @@ function yamlGet(text3, keyPath) {
2522
2736
  return "";
2523
2737
  }
2524
2738
  function discoverRoles(repoRoot) {
2525
- const rolesDir = join11(repoRoot, "agents", "hermes");
2526
- if (!existsSync9(rolesDir)) return [];
2739
+ const rolesDir = join13(repoRoot, "agents", "hermes");
2740
+ if (!existsSync10(rolesDir)) return [];
2527
2741
  return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
2528
- const roleDir = join11(rolesDir, entry.name);
2529
- const roleYamlPath = join11(roleDir, "role.yaml");
2530
- if (!existsSync9(roleYamlPath)) return null;
2742
+ const roleDir = join13(rolesDir, entry.name);
2743
+ const roleYamlPath = join13(roleDir, "role.yaml");
2744
+ if (!existsSync10(roleYamlPath)) return null;
2531
2745
  const text3 = readText(roleYamlPath);
2532
2746
  const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
2533
2747
  return {
@@ -2555,7 +2769,7 @@ function discoverRoles(repoRoot) {
2555
2769
  }).filter((value) => Boolean(value));
2556
2770
  }
2557
2771
  function registryPath(homeDir) {
2558
- return join11(homeDir, ".hermes", "agents-registry.yaml");
2772
+ return join13(homeDir, ".hermes", "agents-registry.yaml");
2559
2773
  }
2560
2774
  function systemctlUser(args) {
2561
2775
  const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
@@ -2566,8 +2780,8 @@ function systemctlUser(args) {
2566
2780
  };
2567
2781
  }
2568
2782
  function templateScript(ctx, name) {
2569
- const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
2570
- return existsSync9(source) ? readText(source) : void 0;
2783
+ const source = join13(ctx.pjanglerRoot, ".mise", "scripts", name);
2784
+ return existsSync10(source) ? readText(source) : void 0;
2571
2785
  }
2572
2786
  function templateVersioningScript(ctx) {
2573
2787
  return templateScript(ctx, "versioning.sh");
@@ -2579,8 +2793,8 @@ function resolveAgentHooksLayer2(ctx) {
2579
2793
  const override = process.env.PJ_AGENT_HOOKS_LAYER;
2580
2794
  if (override === "0" || override === "false") return false;
2581
2795
  if (override === "1" || override === "true") return true;
2582
- if (existsSync9(join11(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2583
- return !existsSync9(join11(ctx.homeDir, ".agents", "hooks"));
2796
+ if (existsSync10(join13(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2797
+ return !existsSync10(join13(ctx.homeDir, ".agents", "hooks"));
2584
2798
  }
2585
2799
  function evaluateMiseConditionals(template, agentHooksLayer) {
2586
2800
  const out = [];
@@ -2610,19 +2824,36 @@ function renderGeneratedProjectMiseToml(ctx, template) {
2610
2824
  return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2611
2825
  }
2612
2826
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2613
- const targetPath = join11(ctx.repoRoot, "mise.toml");
2614
- if (existsSync9(targetPath)) return false;
2615
- const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2616
- if (!existsSync9(sourcePath)) return false;
2827
+ const targetPath = join13(ctx.repoRoot, "mise.toml");
2828
+ if (existsSync10(targetPath)) return false;
2829
+ const sourcePath = join13(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2830
+ if (!existsSync10(sourcePath)) return false;
2617
2831
  changedFiles.push(targetPath);
2618
2832
  if (!ctx.dryRun) {
2619
2833
  writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
2620
2834
  }
2621
2835
  return true;
2622
2836
  }
2837
+ function templateCommonProjectText(ctx, rel) {
2838
+ const path = join13(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
2839
+ return existsSync10(path) ? readText(path) : void 0;
2840
+ }
2841
+ function canonicalSkillsManifest() {
2842
+ return `${JSON.stringify(
2843
+ {
2844
+ $schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
2845
+ inherit_global: true,
2846
+ registry: SKILLS_REGISTRY_URL,
2847
+ skills: []
2848
+ },
2849
+ null,
2850
+ 2
2851
+ )}
2852
+ `;
2853
+ }
2623
2854
  function templateVersionFilesConf(ctx, repoRoot) {
2624
- const packageJson = join11(repoRoot, "package.json");
2625
- return existsSync9(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";
2855
+ const packageJson = join13(repoRoot, "package.json");
2856
+ return existsSync10(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";
2626
2857
  }
2627
2858
  function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
2628
2859
  if (startMarker.test(text3)) {
@@ -2646,7 +2877,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
2646
2877
  function requiredMisePathEntries(ctx) {
2647
2878
  const required = [...BASE_MISE_PATH_ENTRIES];
2648
2879
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
2649
- if (existsSync9(join11(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
2880
+ if (existsSync10(join13(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
2650
2881
  }
2651
2882
  return required;
2652
2883
  }
@@ -2747,6 +2978,9 @@ function stripTomlStringsAndComments(line) {
2747
2978
  function isManagedHookEntry(value) {
2748
2979
  const trimmed = value.trim();
2749
2980
  if (trimmed === OP_INJECT_SCRIPT) return true;
2981
+ if (trimmed === SYNC_SKILLS_SCRIPT) return true;
2982
+ if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
2983
+ if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
2750
2984
  return /link-agentfiles\.sh'?\s*$/.test(trimmed);
2751
2985
  }
2752
2986
  function normalizeHookScript(script) {
@@ -2849,12 +3083,17 @@ function upsertLinkAgentfilesHooks(text3) {
2849
3083
  function upsertLinkAgentfilesBlock(text3, ctx) {
2850
3084
  const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
2851
3085
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
3086
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
3087
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
3088
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
3089
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
2852
3090
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
3091
+ cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
2853
3092
  cleaned = upsertLinkAgentfilesHooks(cleaned);
2854
3093
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
2855
3094
  }
2856
3095
  function readProjectJson(ctx) {
2857
- return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
3096
+ return tryParseJson(safeReadText(join13(ctx.repoRoot, ".project.json")));
2858
3097
  }
2859
3098
  function boolSetting(value, fallback) {
2860
3099
  if (typeof value === "boolean") return value;
@@ -2929,12 +3168,12 @@ function canonicalProjectJson(ctx) {
2929
3168
  };
2930
3169
  }
2931
3170
  function projectJsonFinding(ctx) {
2932
- const projectPath = join11(ctx.repoRoot, ".project.json");
2933
- const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
3171
+ const projectPath = join13(ctx.repoRoot, ".project.json");
3172
+ const planeJsonPath = join13(ctx.repoRoot, ".plane.json");
2934
3173
  const details = [];
2935
3174
  const data = readProjectJson(ctx);
2936
3175
  const roles = discoverRoles(ctx.repoRoot);
2937
- if (!existsSync9(projectPath)) {
3176
+ if (!existsSync10(projectPath)) {
2938
3177
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
2939
3178
  }
2940
3179
  if (!data) {
@@ -2969,7 +3208,7 @@ function projectJsonFinding(ctx) {
2969
3208
  for (const key of ["enabled", "grace_hours", "auto_review"]) {
2970
3209
  if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
2971
3210
  }
2972
- if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
3211
+ if (existsSync10(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2973
3212
  return {
2974
3213
  id: "sot.project-json",
2975
3214
  title: "Canonical .project.json",
@@ -3050,17 +3289,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
3050
3289
  `.replace(/\u0010/g, "$");
3051
3290
  }
3052
3291
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
3053
- if (!existsSync9(sourceDir)) return;
3292
+ if (!existsSync10(sourceDir)) return;
3054
3293
  mkdirSync6(targetDir, { recursive: true });
3055
3294
  for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
3056
- const sourcePath = join11(sourceDir, entry.name);
3295
+ const sourcePath = join13(sourceDir, entry.name);
3057
3296
  if (skip?.(sourcePath)) continue;
3058
- const targetPath = join11(targetDir, entry.name);
3297
+ const targetPath = join13(targetDir, entry.name);
3059
3298
  if (entry.isDirectory()) {
3060
3299
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
3061
3300
  continue;
3062
3301
  }
3063
- if (existsSync9(targetPath)) continue;
3302
+ if (existsSync10(targetPath)) continue;
3064
3303
  changedFiles.push(targetPath);
3065
3304
  if (!dryRun) {
3066
3305
  ensureParent(targetPath);
@@ -3069,7 +3308,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
3069
3308
  }
3070
3309
  }
3071
3310
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
3072
- const gitmodulesPath = join11(repoRoot, ".gitmodules");
3311
+ const gitmodulesPath = join13(repoRoot, ".gitmodules");
3073
3312
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
3074
3313
  const owner = role.runtimeOwner || "delorenj";
3075
3314
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -3236,7 +3475,7 @@ function runBmadInstall(repoRoot) {
3236
3475
  return { ok: true };
3237
3476
  }
3238
3477
  function readInstalledBmadVersion(repoRoot) {
3239
- const raw = safeReadText(join11(repoRoot, "_bmad", "_config", "manifest.yaml"));
3478
+ const raw = safeReadText(join13(repoRoot, "_bmad", "_config", "manifest.yaml"));
3240
3479
  if (!raw) return void 0;
3241
3480
  try {
3242
3481
  const parsed = YAML3.parse(raw);
@@ -3247,8 +3486,8 @@ function readInstalledBmadVersion(repoRoot) {
3247
3486
  }
3248
3487
  }
3249
3488
  function bmadCachePath(homeDir) {
3250
- const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join11(homeDir, ".cache");
3251
- return join11(cacheRoot, "pjangler", "bmad-dist-tags.json");
3489
+ const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join13(homeDir, ".cache");
3490
+ return join13(cacheRoot, "pjangler", "bmad-dist-tags.json");
3252
3491
  }
3253
3492
  function readBmadDistTagsCache(homeDir) {
3254
3493
  const raw = safeReadText(bmadCachePath(homeDir));
@@ -3291,7 +3530,7 @@ function resolveBmadDistTags(homeDir) {
3291
3530
  try {
3292
3531
  const path = bmadCachePath(homeDir);
3293
3532
  mkdirSync6(dirname6(path), { recursive: true });
3294
- writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
3533
+ writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
3295
3534
  } catch {
3296
3535
  }
3297
3536
  return { distTags: fetched, stale: false };
@@ -3336,14 +3575,14 @@ var RULES = [
3336
3575
  id: "mise.config-root",
3337
3576
  title: "mise config_root + AGENTS link hooks",
3338
3577
  audit: (ctx) => {
3339
- const misePath = join11(ctx.repoRoot, "mise.toml");
3340
- if (!existsSync9(misePath)) {
3578
+ const misePath = join13(ctx.repoRoot, "mise.toml");
3579
+ if (!existsSync10(misePath)) {
3341
3580
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
3342
3581
  }
3343
3582
  const text3 = readText(misePath);
3344
3583
  const details = [];
3345
- const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3346
- if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
3584
+ const linkAgentfilesPath = join13(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3585
+ if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
3347
3586
  const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
3348
3587
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
3349
3588
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
@@ -3361,10 +3600,10 @@ var RULES = [
3361
3600
  };
3362
3601
  },
3363
3602
  migrate: (ctx, finding) => {
3364
- const path = join11(ctx.repoRoot, "mise.toml");
3603
+ const path = join13(ctx.repoRoot, "mise.toml");
3365
3604
  const changedFiles = [];
3366
3605
  const details = [];
3367
- if (!existsSync9(path)) {
3606
+ if (!existsSync10(path)) {
3368
3607
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3369
3608
  return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
3370
3609
  }
@@ -3380,7 +3619,7 @@ var RULES = [
3380
3619
  if (!ctx.dryRun) writeText(path, next);
3381
3620
  text3 = next;
3382
3621
  }
3383
- const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3622
+ const linkAgentfilesPath = join13(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3384
3623
  const expectedScript = templateLinkAgentfilesScript(ctx);
3385
3624
  if (expectedScript === void 0) {
3386
3625
  return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/link-agentfiles.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
@@ -3389,7 +3628,7 @@ var RULES = [
3389
3628
  changedFiles.push(linkAgentfilesPath);
3390
3629
  if (!ctx.dryRun) {
3391
3630
  writeText(linkAgentfilesPath, expectedScript);
3392
- chmodSync2(linkAgentfilesPath, 493);
3631
+ chmodSync4(linkAgentfilesPath, 493);
3393
3632
  }
3394
3633
  }
3395
3634
  return {
@@ -3407,13 +3646,13 @@ var RULES = [
3407
3646
  title: "managed mise versioning block",
3408
3647
  audit: (ctx) => {
3409
3648
  const details = [];
3410
- const misePath = join11(ctx.repoRoot, "mise.toml");
3411
- const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3412
- const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
3649
+ const misePath = join13(ctx.repoRoot, "mise.toml");
3650
+ const versioningPath = join13(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3651
+ const manifestPath = join13(ctx.repoRoot, ".mise", "version-files.conf");
3413
3652
  const text3 = safeReadText(misePath);
3414
3653
  if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
3415
- if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
3416
- if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
3654
+ if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
3655
+ if (!existsSync10(manifestPath)) details.push(".mise/version-files.conf missing");
3417
3656
  return {
3418
3657
  id: "mise.versioning",
3419
3658
  title: "managed mise versioning block",
@@ -3426,8 +3665,8 @@ var RULES = [
3426
3665
  migrate: (ctx, finding) => {
3427
3666
  const changedFiles = [];
3428
3667
  const details = [];
3429
- const misePath = join11(ctx.repoRoot, "mise.toml");
3430
- if (!existsSync9(misePath)) {
3668
+ const misePath = join13(ctx.repoRoot, "mise.toml");
3669
+ if (!existsSync10(misePath)) {
3431
3670
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3432
3671
  return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
3433
3672
  }
@@ -3451,7 +3690,7 @@ var RULES = [
3451
3690
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
3452
3691
  if (!ctx.dryRun) writeText(misePath, nextMise);
3453
3692
  }
3454
- const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3693
+ const versioningPath = join13(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3455
3694
  const expectedScript = templateVersioningScript(ctx);
3456
3695
  if (expectedScript === void 0) {
3457
3696
  return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/versioning.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
@@ -3460,10 +3699,10 @@ var RULES = [
3460
3699
  changedFiles.push(versioningPath);
3461
3700
  if (!ctx.dryRun) {
3462
3701
  writeText(versioningPath, expectedScript);
3463
- chmodSync2(versioningPath, 493);
3702
+ chmodSync4(versioningPath, 493);
3464
3703
  }
3465
3704
  }
3466
- const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
3705
+ const manifestPath = join13(ctx.repoRoot, ".mise", "version-files.conf");
3467
3706
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
3468
3707
  if (safeReadText(manifestPath) !== expectedManifest) {
3469
3708
  changedFiles.push(manifestPath);
@@ -3479,13 +3718,125 @@ var RULES = [
3479
3718
  };
3480
3719
  }
3481
3720
  },
3721
+ {
3722
+ id: "skills.project-manifest",
3723
+ title: "Skillex project skills manifest",
3724
+ audit: (ctx) => {
3725
+ const details = [];
3726
+ const manifestPath = join13(ctx.repoRoot, ".agents", "skills.json");
3727
+ const legacyDir = join13(ctx.repoRoot, ".agents", "skills");
3728
+ const localExamplePath = join13(ctx.repoRoot, ".agents", "local.example.json");
3729
+ const misePath = join13(ctx.repoRoot, "mise.toml");
3730
+ let fixable = true;
3731
+ const manifest = tryParseJson(safeReadText(manifestPath));
3732
+ if (!manifest) {
3733
+ details.push(".agents/skills.json missing or invalid JSON");
3734
+ } else {
3735
+ if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
3736
+ if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
3737
+ if (!Array.isArray(manifest.skills)) details.push(".agents/skills.json should define a skills array");
3738
+ }
3739
+ if (existsSync10(legacyDir)) {
3740
+ const entries = readdirSync2(legacyDir);
3741
+ if (entries.length > 0) {
3742
+ details.push(".agents/skills/ still contains legacy committed skills; map them into .agents/skills.json before migrating");
3743
+ fixable = false;
3744
+ } else {
3745
+ details.push(".agents/skills/ legacy directory should be removed");
3746
+ }
3747
+ }
3748
+ for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
3749
+ if (existsSync10(join13(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
3750
+ }
3751
+ const localExample = tryParseJson(safeReadText(localExamplePath));
3752
+ if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
3753
+ details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
3754
+ }
3755
+ const mise = safeReadText(misePath);
3756
+ if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run sync-skills.py --scope project on enter");
3757
+ if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
3758
+ if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
3759
+ if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
3760
+ details.push("mise.toml still contains legacy skill-link wiring");
3761
+ }
3762
+ return {
3763
+ id: "skills.project-manifest",
3764
+ title: "Skillex project skills manifest",
3765
+ status: details.length === 0 ? "pass" : "fail",
3766
+ summary: details.length === 0 ? "Skillex skills manifest parity verified" : `${details.length} Skillex migration issue(s) detected`,
3767
+ details,
3768
+ fixable
3769
+ };
3770
+ },
3771
+ migrate: (ctx, finding) => {
3772
+ const changedFiles = [];
3773
+ const details = [];
3774
+ const manifestPath = join13(ctx.repoRoot, ".agents", "skills.json");
3775
+ const legacyDir = join13(ctx.repoRoot, ".agents", "skills");
3776
+ const localExamplePath = join13(ctx.repoRoot, ".agents", "local.example.json");
3777
+ const misePath = join13(ctx.repoRoot, "mise.toml");
3778
+ if (existsSync10(legacyDir)) {
3779
+ const entries = readdirSync2(legacyDir);
3780
+ if (entries.length > 0) {
3781
+ return {
3782
+ id: finding.id,
3783
+ title: finding.title,
3784
+ status: "blocked",
3785
+ summary: "Legacy .agents/skills/ still contains committed skills; migrate them into .agents/skills.json manually first",
3786
+ changedFiles,
3787
+ details: entries.map((entry) => `.agents/skills/${entry}`)
3788
+ };
3789
+ }
3790
+ changedFiles.push(legacyDir);
3791
+ if (!ctx.dryRun) rmSync(legacyDir, { recursive: true, force: true });
3792
+ }
3793
+ const expectedManifest = canonicalSkillsManifest();
3794
+ if (safeReadText(manifestPath) !== expectedManifest) {
3795
+ changedFiles.push(manifestPath);
3796
+ if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
3797
+ details.push("Wrote canonical .agents/skills.json manifest");
3798
+ }
3799
+ for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
3800
+ const path = join13(ctx.repoRoot, rel);
3801
+ if (existsSync10(path)) {
3802
+ changedFiles.push(path);
3803
+ if (!ctx.dryRun) unlinkSync3(path);
3804
+ }
3805
+ }
3806
+ const templateLocalExample = templateCommonProjectText(ctx, ".agents/local.example.json");
3807
+ const currentLocalExample = safeReadText(localExamplePath);
3808
+ if (templateLocalExample && currentLocalExample && currentLocalExample !== templateLocalExample) {
3809
+ changedFiles.push(localExamplePath);
3810
+ if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
3811
+ }
3812
+ if (!existsSync10(misePath)) {
3813
+ if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3814
+ return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details };
3815
+ }
3816
+ }
3817
+ const currentMise = readText(misePath);
3818
+ const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
3819
+ if (nextMise !== currentMise) {
3820
+ if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
3821
+ if (!ctx.dryRun) writeText(misePath, nextMise);
3822
+ }
3823
+ return {
3824
+ id: finding.id,
3825
+ title: finding.title,
3826
+ status: changedFiles.length ? "applied" : "noop",
3827
+ summary: changedFiles.length ? "Skillex skills manifest contract normalized" : "No changes required",
3828
+ changedFiles,
3829
+ details
3830
+ };
3831
+ }
3832
+ },
3482
3833
  {
3483
3834
  id: "sot.agent-symlinks",
3484
3835
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
3485
3836
  audit: (ctx) => {
3486
- const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
3487
- if (!existsSync9(agentsPath)) {
3488
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync9(join11(ctx.repoRoot, file)));
3837
+ const agentsPath = join13(ctx.repoRoot, "AGENTS.md");
3838
+ if (!existsSync10(agentsPath)) {
3839
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join13(ctx.repoRoot, file)));
3489
3840
  if (fallbackSources.length === 0) {
3490
3841
  return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
3491
3842
  }
@@ -3500,7 +3851,7 @@ var RULES = [
3500
3851
  }
3501
3852
  const details = [];
3502
3853
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
3503
- const full = join11(ctx.repoRoot, file);
3854
+ const full = join13(ctx.repoRoot, file);
3504
3855
  const target = readSymlinkTarget(full);
3505
3856
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
3506
3857
  }
@@ -3524,7 +3875,7 @@ var RULES = [
3524
3875
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
3525
3876
  }
3526
3877
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
3527
- const full = join11(ctx.repoRoot, file);
3878
+ const full = join13(ctx.repoRoot, file);
3528
3879
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
3529
3880
  if (result.blocked) blockedDetails.push(result.blocked);
3530
3881
  if (result.changed) changedFiles.push(full);
@@ -3546,7 +3897,7 @@ var RULES = [
3546
3897
  migrate: (ctx, finding) => {
3547
3898
  const changedFiles = [];
3548
3899
  const details = [];
3549
- const path = join11(ctx.repoRoot, ".project.json");
3900
+ const path = join13(ctx.repoRoot, ".project.json");
3550
3901
  const existing = readProjectJson(ctx) ?? {};
3551
3902
  const canonical = canonicalProjectJson(ctx);
3552
3903
  const merged = { ...existing, ...canonical };
@@ -3556,10 +3907,10 @@ var RULES = [
3556
3907
  changedFiles.push(path);
3557
3908
  if (!ctx.dryRun) writeText(path, expected);
3558
3909
  }
3559
- const planeJson = join11(ctx.repoRoot, ".plane.json");
3560
- if (existsSync9(planeJson)) {
3910
+ const planeJson = join13(ctx.repoRoot, ".plane.json");
3911
+ if (existsSync10(planeJson)) {
3561
3912
  const backup = `${planeJson}.migrated-backup`;
3562
- if (existsSync9(backup)) {
3913
+ if (existsSync10(backup)) {
3563
3914
  details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
3564
3915
  } else {
3565
3916
  changedFiles.push(backup);
@@ -3581,8 +3932,8 @@ var RULES = [
3581
3932
  title: ".env.op + gitignore secrets contract",
3582
3933
  audit: (ctx) => {
3583
3934
  const details = [];
3584
- const envOp = safeReadText(join11(ctx.repoRoot, ".env.op"));
3585
- const gitignore = safeReadText(join11(ctx.repoRoot, ".gitignore"));
3935
+ const envOp = safeReadText(join13(ctx.repoRoot, ".env.op"));
3936
+ const gitignore = safeReadText(join13(ctx.repoRoot, ".gitignore"));
3586
3937
  if (!envOp) {
3587
3938
  details.push(".env.op missing");
3588
3939
  } else {
@@ -3608,12 +3959,12 @@ var RULES = [
3608
3959
  migrate: (ctx, finding) => {
3609
3960
  const changedFiles = [];
3610
3961
  const details = [];
3611
- const envOpPath = join11(ctx.repoRoot, ".env.op");
3612
- if (!existsSync9(envOpPath)) {
3962
+ const envOpPath = join13(ctx.repoRoot, ".env.op");
3963
+ if (!existsSync10(envOpPath)) {
3613
3964
  changedFiles.push(envOpPath);
3614
- if (!ctx.dryRun) writeText(envOpPath, readText(join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
3965
+ if (!ctx.dryRun) writeText(envOpPath, readText(join13(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
3615
3966
  }
3616
- const gitignorePath = join11(ctx.repoRoot, ".gitignore");
3967
+ const gitignorePath = join13(ctx.repoRoot, ".gitignore");
3617
3968
  const gitignore = safeReadText(gitignorePath) ?? "";
3618
3969
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
3619
3970
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -3640,7 +3991,7 @@ var RULES = [
3640
3991
  title: ".copier-answers.yml provenance + drift report",
3641
3992
  audit: (ctx) => {
3642
3993
  const details = [];
3643
- const path = join11(ctx.repoRoot, ".copier-answers.yml");
3994
+ const path = join13(ctx.repoRoot, ".copier-answers.yml");
3644
3995
  const text3 = safeReadText(path);
3645
3996
  const project = readProjectJson(ctx);
3646
3997
  if (!text3) {
@@ -3671,12 +4022,12 @@ var RULES = [
3671
4022
  const changedFiles = [];
3672
4023
  const project = canonicalProjectJson(ctx);
3673
4024
  const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
3674
- _src_path: ${join11(ctx.pjanglerRoot, "templates", "commonproject")}
4025
+ _src_path: ${join13(ctx.pjanglerRoot, "templates", "commonproject")}
3675
4026
  project_description: ${String(project.project_description)}
3676
4027
  project_name: ${String(project.project_name)}
3677
4028
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3678
4029
  `;
3679
- const path = join11(ctx.repoRoot, ".copier-answers.yml");
4030
+ const path = join13(ctx.repoRoot, ".copier-answers.yml");
3680
4031
  if (safeReadText(path) !== text3) {
3681
4032
  changedFiles.push(path);
3682
4033
  if (!ctx.dryRun) writeText(path, text3);
@@ -3695,14 +4046,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3695
4046
  id: "bmad.scaffold",
3696
4047
  title: "BMAD modules/docs scaffold",
3697
4048
  audit: (ctx) => {
3698
- const targetRoot = join11(ctx.repoRoot, "_bmad");
4049
+ const targetRoot = join13(ctx.repoRoot, "_bmad");
3699
4050
  const sentinels = [
3700
- join11("core", "config.yaml"),
3701
- join11("config.toml"),
3702
- join11("_config", "manifest.yaml"),
3703
- join11("bmm", "config.yaml")
4051
+ join13("core", "config.yaml"),
4052
+ join13("config.toml"),
4053
+ join13("_config", "manifest.yaml"),
4054
+ join13("bmm", "config.yaml")
3704
4055
  ];
3705
- const missing = sentinels.filter((file) => !existsSync9(join11(targetRoot, file)));
4056
+ const missing = sentinels.filter((file) => !existsSync10(join13(targetRoot, file)));
3706
4057
  return {
3707
4058
  id: "bmad.scaffold",
3708
4059
  title: "BMAD modules/docs scaffold",
@@ -3716,7 +4067,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3716
4067
  const changedFiles = [];
3717
4068
  if (ctx.dryRun) {
3718
4069
  for (const detail of finding.details) {
3719
- changedFiles.push(join11(ctx.repoRoot, detail));
4070
+ changedFiles.push(join13(ctx.repoRoot, detail));
3720
4071
  }
3721
4072
  return {
3722
4073
  id: finding.id,
@@ -3741,8 +4092,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3741
4092
  };
3742
4093
  }
3743
4094
  for (const detail of finding.details) {
3744
- if (existsSync9(join11(ctx.repoRoot, detail))) {
3745
- changedFiles.push(join11(ctx.repoRoot, detail));
4095
+ if (existsSync10(join13(ctx.repoRoot, detail))) {
4096
+ changedFiles.push(join13(ctx.repoRoot, detail));
3746
4097
  }
3747
4098
  }
3748
4099
  return {
@@ -3765,7 +4116,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3765
4116
  id: "bmad.version",
3766
4117
  title: "BMAD version currency",
3767
4118
  status: "skip",
3768
- summary: existsSync9(join11(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
4119
+ summary: existsSync10(join13(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
3769
4120
  details: [],
3770
4121
  fixable: false
3771
4122
  };
@@ -3820,7 +4171,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3820
4171
  }
3821
4172
  const installed = readInstalledBmadVersion(ctx.repoRoot);
3822
4173
  const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
3823
- const manifestPath = join11(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
4174
+ const manifestPath = join13(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
3824
4175
  if (ctx.dryRun) {
3825
4176
  return {
3826
4177
  id: finding.id,
@@ -3867,11 +4218,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3867
4218
  }
3868
4219
  const details = [];
3869
4220
  for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
3870
- if (!existsSync9(join11(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join11(role.roleDir, rel))}`);
4221
+ if (!existsSync10(join13(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join13(role.roleDir, rel))}`);
3871
4222
  }
3872
- const gitmodules = safeReadText(join11(ctx.repoRoot, ".gitmodules")) ?? "";
4223
+ const gitmodules = safeReadText(join13(ctx.repoRoot, ".gitmodules")) ?? "";
3873
4224
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
3874
- if (!profileMetaInheritsDefault(join11(role.roleDir, "runtime", "profile.yaml"))) {
4225
+ if (!profileMetaInheritsDefault(join13(role.roleDir, "runtime", "profile.yaml"))) {
3875
4226
  details.push("runtime/profile.yaml missing inherited default config metadata");
3876
4227
  }
3877
4228
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -3892,21 +4243,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3892
4243
  if (!role) {
3893
4244
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
3894
4245
  }
3895
- const templateRoleDir = join11(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
3896
- writeIfDifferent(join11(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
3897
- writeIfDifferent(join11(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
3898
- writeIfDifferent(join11(role.roleDir, ".gitignore"), readText(join11(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
3899
- copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
3900
- copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
3901
- copyMissingRecursive(join11(templateRoleDir, ".scripts"), join11(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
3902
- const promptSource = join11(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
3903
- const promptTarget = join11(role.roleDir, ".scripts", "sentinel.prompt.md");
3904
- if (existsSync9(promptSource) && !existsSync9(promptTarget)) {
4246
+ const templateRoleDir = join13(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
4247
+ writeIfDifferent(join13(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
4248
+ writeIfDifferent(join13(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
4249
+ writeIfDifferent(join13(role.roleDir, ".gitignore"), readText(join13(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
4250
+ copyMissingRecursive(join13(templateRoleDir, ".runtime-scaffold"), join13(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
4251
+ copyMissingRecursive(join13(templateRoleDir, ".runtime-scaffold"), join13(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
4252
+ copyMissingRecursive(join13(templateRoleDir, ".scripts"), join13(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
4253
+ const promptSource = join13(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
4254
+ const promptTarget = join13(role.roleDir, ".scripts", "sentinel.prompt.md");
4255
+ if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
3905
4256
  const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo).replace(/\{\{ display_name \}\}/g, role.displayName || role.agentId);
3906
4257
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
3907
4258
  }
3908
4259
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
3909
- const profileMetaUpdated = upsertInheritedProfileMeta(join11(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
4260
+ const profileMetaUpdated = upsertInheritedProfileMeta(join13(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
3910
4261
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
3911
4262
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
3912
4263
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -3938,7 +4289,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3938
4289
  const details = [];
3939
4290
  for (const role of roles) {
3940
4291
  const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3941
- const runtimeRelPath = join11(roleRelDir, "runtime");
4292
+ const runtimeRelPath = join13(roleRelDir, "runtime");
3942
4293
  const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3943
4294
  cwd: ctx.repoRoot,
3944
4295
  encoding: "utf8"
@@ -3946,8 +4297,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3946
4297
  if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
3947
4298
  details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
3948
4299
  }
3949
- const gitignorePath = join11(role.roleDir, ".gitignore");
3950
- if (existsSync9(gitignorePath)) {
4300
+ const gitignorePath = join13(role.roleDir, ".gitignore");
4301
+ if (existsSync10(gitignorePath)) {
3951
4302
  const content = safeReadText(gitignorePath) ?? "";
3952
4303
  const lines = content.split(/\r?\n/).map((line) => line.trim());
3953
4304
  if (!lines.includes("runtime/") && !lines.includes("runtime")) {
@@ -3972,7 +4323,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3972
4323
  const details = [];
3973
4324
  for (const role of roles) {
3974
4325
  const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3975
- const runtimeRelPath = join11(roleRelDir, "runtime");
4326
+ const runtimeRelPath = join13(roleRelDir, "runtime");
3976
4327
  const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3977
4328
  cwd: ctx.repoRoot,
3978
4329
  encoding: "utf8"
@@ -3987,10 +4338,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3987
4338
  });
3988
4339
  }
3989
4340
  }
3990
- const gitignorePath = join11(role.roleDir, ".gitignore");
4341
+ const gitignorePath = join13(role.roleDir, ".gitignore");
3991
4342
  let content = "";
3992
4343
  let isIgnored = false;
3993
- if (existsSync9(gitignorePath)) {
4344
+ if (existsSync10(gitignorePath)) {
3994
4345
  content = safeReadText(gitignorePath) ?? "";
3995
4346
  const lines = content.split(/\r?\n/).map((line) => line.trim());
3996
4347
  isIgnored = lines.includes("runtime/") || lines.includes("runtime");
@@ -4057,9 +4408,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
4057
4408
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
4058
4409
  }
4059
4410
  for (const role of roles) {
4060
- const sysDir = join11(ctx.homeDir, ".config", "systemd", "user");
4411
+ const sysDir = join13(ctx.homeDir, ".config", "systemd", "user");
4061
4412
  const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
4062
- const allUnitsPresent = units.every((unit) => existsSync9(join11(sysDir, unit)));
4413
+ const allUnitsPresent = units.every((unit) => existsSync10(join13(sysDir, unit)));
4063
4414
  if (allUnitsPresent) {
4064
4415
  if (ctx.dryRun) {
4065
4416
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -4071,8 +4422,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
4071
4422
  }
4072
4423
  continue;
4073
4424
  }
4074
- for (const script of [join11(role.roleDir, ".scripts", "70-systemd.sh")]) {
4075
- if (!script || !existsSync9(script)) continue;
4425
+ for (const script of [join13(role.roleDir, ".scripts", "70-systemd.sh")]) {
4426
+ if (!script || !existsSync10(script)) continue;
4076
4427
  if (ctx.dryRun) {
4077
4428
  details.push(`would run: bash ${script}`);
4078
4429
  } else {
@@ -4099,7 +4450,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
4099
4450
  changedFiles.push(path);
4100
4451
  if (!dryRun) {
4101
4452
  writeText(path, normalized);
4102
- if (mode) chmodSync2(path, mode);
4453
+ if (mode) chmodSync4(path, mode);
4103
4454
  }
4104
4455
  }
4105
4456
  function getParityRuleIds() {
@@ -4211,15 +4562,15 @@ function formatMigrationReport(report) {
4211
4562
  }
4212
4563
 
4213
4564
  // src/utils/version.ts
4214
- import { readFileSync as readFileSync6 } from "node:fs";
4215
- import { dirname as dirname7, join as join12 } from "node:path";
4565
+ import { readFileSync as readFileSync7 } from "node:fs";
4566
+ import { dirname as dirname7, join as join14 } from "node:path";
4216
4567
  import { fileURLToPath as fileURLToPath4 } from "node:url";
4217
4568
  var PJANGLER_VERSION = (() => {
4218
4569
  try {
4219
4570
  let dir = dirname7(fileURLToPath4(import.meta.url));
4220
4571
  for (let i = 0; i < 4; i++) {
4221
4572
  try {
4222
- const raw = readFileSync6(join12(dir, "package.json"), "utf8");
4573
+ const raw = readFileSync7(join14(dir, "package.json"), "utf8");
4223
4574
  return JSON.parse(raw).version ?? "0.0.0";
4224
4575
  } catch {
4225
4576
  const parent = dirname7(dir);
@@ -4262,9 +4613,9 @@ async function promptForRuleIds(rules) {
4262
4613
  return selected;
4263
4614
  }
4264
4615
  function readJson(path) {
4265
- if (!existsSync10(path)) return void 0;
4616
+ if (!existsSync11(path)) return void 0;
4266
4617
  try {
4267
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
4618
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
4268
4619
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
4269
4620
  } catch {
4270
4621
  return void 0;
@@ -4281,8 +4632,8 @@ function packageNameToProjectName(value) {
4281
4632
  return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
4282
4633
  }
4283
4634
  function deriveProjectDefaults(targetDir) {
4284
- const manifest = readJson(join13(targetDir, ".project.json"));
4285
- const pkg = readJson(join13(targetDir, "package.json"));
4635
+ const manifest = readJson(join15(targetDir, ".project.json"));
4636
+ const pkg = readJson(join15(targetDir, "package.json"));
4286
4637
  const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
4287
4638
  const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
4288
4639
  return {
@@ -4338,7 +4689,7 @@ function actionNeedsRun(plan, kind, syncMode) {
4338
4689
  if (!action || action.kind !== "project.write-manifest") return false;
4339
4690
  const next = `${JSON.stringify(action.manifest, null, 2)}
4340
4691
  `;
4341
- return !existsSync10(action.path) || readFileSync7(action.path, "utf8") !== next;
4692
+ return !existsSync11(action.path) || readFileSync8(action.path, "utf8") !== next;
4342
4693
  }
4343
4694
  if (kind === "copier.copy.commonproject") return true;
4344
4695
  if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
@@ -4393,7 +4744,7 @@ async function resolveProjectInitTarget(name, options) {
4393
4744
  if (!targetDir && interactive) {
4394
4745
  const defaultName = name ?? basename4(cwd);
4395
4746
  const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
4396
- const defaultDir = join13(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
4747
+ const defaultDir = join15(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
4397
4748
  targetDir = await promptTextValue("Project directory", defaultDir);
4398
4749
  name = promptedName;
4399
4750
  }
@@ -4401,7 +4752,7 @@ async function resolveProjectInitTarget(name, options) {
4401
4752
  if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
4402
4753
  targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
4403
4754
  }
4404
- const targetExists = existsSync10(targetDir);
4755
+ const targetExists = existsSync11(targetDir);
4405
4756
  if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
4406
4757
  const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
4407
4758
  const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));